Imagine trying to build a modern web application but being constrained to use only the tools available a decade ago. This scenario has been a reality for many developers working with NGINX’s JavaScript module (njs), who have been limited by an ES5-based engine with only select ES6 extensions. While this constraint initially kept njs lightweight and focused, the growing complexity of server-side JavaScript requirements has highlighted the need for more modern language features and broader compatibility.
Today, we’re excited to announce the introduction of QuickJS engine support in njs, available since version 0.9.1, bringing ES2023 compatibility and opening new possibilities for JavaScript scripting within NGINX environments.
The Challenge: Balancing Engine Development with Integration Excellence
njs was designed with a clear philosophy: provide a fast, lightweight JavaScript engine optimized for NGINX’s specific use cases. The custom njs engine delivered on this promise, offering excellent performance for short-lived scripts and minimal memory overhead. However, this approach came with significant trade-offs.
We found ourselves spending considerable time maintaining and extending our custom JavaScript engine rather than focusing on what we do best – creating seamless integrations between JavaScript and NGINX. As web development evolved and developers increasingly relied on modern JavaScript features, the limitations became more pronounced:
- Language limitations – The ES5 foundation with limited ES6 extensions meant developers couldn’t leverage modern JavaScript patterns or contemporary library ecosystems that assume newer language standards.
- Development focus – Maintaining a custom JavaScript engine required substantial engineering resources that could have been directed toward improving NGINX integration capabilities, performance optimizations, and feature development.
- Ecosystem compatibility – Many existing JavaScript libraries and tools expected more complete ES6+ support, creating barriers for developers who wanted to reuse proven solutions in their NGINX configurations.
The result was a tension between njs’s original design goals and the evolving needs of the JavaScript development community.
Introducing QuickJS: Full ES2023 Support for njs
Rather than continuing to extend our custom engine, we made a strategic decision: integrate QuickJS as an alternative JavaScript engine while preserving the existing njs engine for backward compatibility.
QuickJS, developed by Fabrice Bellard and Charlie Gordon, emerged as the ideal candidate for this integration. This lightweight, embeddable JavaScript engine offers several compelling advantages:
- Complete ES2023 compatibility – QuickJS supports the full ES2023 specification, including modules, asynchronous generators, proxies, and BigInt—features that modern JavaScript developers expect as standard.
- Minimal Footprint – Despite its comprehensive feature set, QuickJS remains remarkably small and easily embeddable, requiring just a few C files with no external dependencies. The complete x86 code footprint is only 367 KiB for a simple program.
- Active Development – As an open-source project with broader community involvement, QuickJS benefits from continuous development and testing across diverse use cases, reducing the maintenance burden on the njs team.
- Drop-in compatibility – We implemented QuickJS support as a drop-in replacement, meaning existing njs scripts should work with minimal or no modifications.
Getting Started: Configuring QuickJS in NGINX
Switching to the QuickJS engine is straightforward and requires only a single configuration directive. The js_engine
directive, available for both HTTP and stream contexts, allows you to specify which JavaScript engine to use.
Basic HTTP Configuration
Here is the basic HTTP configration:
// nginx.conf
load_module modules/ngx_http_js_module.so;
events {}
http {
js_import main from js/main.js;
server {
listen 8000;
location /njs {
js_content main.handler;
}
location /qjs {
js_engine qjs;
js_content main.handler;
}
}
}
// js/main.js
function handler(r) {
r.return(200, `Hello from ${njs.engine}`);
}
export default { handler };
Testing the Configuration
You can easily test both engines with simple curl commands, as seen below.
$ curl http://127.0.0.1:8000/njs
Hello from njs
$ curl http://127.0.0.1:8000/qjs
Hello from QuickJS
Advanced ES2023 Features Example
Here’s a more sophisticated example showcasing modern JavaScript capabilities available with QuickJS:
// nginx.conf
http {
js_engine qjs;
js_import analytics from js/analytics.js;
server {
listen 8000;
location /analytics {
js_content analytics.processRequest;
}
}
}
// js/analytics.js
class RequestAnalytics {
// Generator function for processing headers
*getHeaderMetrics(headers) {
for (const [key, value] of Object.entries(headers)) {
yield {
header: key.toLowerCase(),
size: key.length + value.length,
type: key.startsWith('x-') ? 'custom' : 'standard'
};
}
}
processRequest(r) {
// Destructuring with default values
const {
method = 'GET',
uri = '/',
httpVersion = '1.0'
} = r;
// Process headers using generator
const headerStats = [];
for (const metric of this.getHeaderMetrics(r.headersIn)) {
headerStats.push(metric);
}
const timestamp = BigInt(Date.now()); // BigInt for precise timestamps
const headerCount = headerStats.length;
const customHeaders = headerStats.filter(({ type }) => type === 'custom').length;
r.return(200, JSON.stringify({
message: `Request processed at ${timestamp}`,
stats: { headerCount, customHeaders },
serverInfo: `${method} ${uri} HTTP/${httpVersion}`
}, null, 2));
}
}
const analytics = new RequestAnalytics();
export default { processRequest: (r) => analytics.processRequest(r) };
Testing the Advanced Example
curl 'http://127.0.0.1:8000/analytics' -H "x-foo: bar"
{
"message": "Request processed at 1747881165336",
"stats": {
"headerCount": 4,
"customHeaders": 1
},
"serverInfo": "GET /analytics HTTP/1.1"
}
The default engine remains njs for backward compatibility, ensuring that existing configurations continue to work without modification. You can specify the engine at different configuration levels—globally in the http or stream context, or per-server block as needed.
Performance Considerations and Migration Guidelines
When considering migration to QuickJS, it’s important to understand the performance characteristics. While QuickJS excels at executing complex JavaScript code and provides full ES2023 compatibility, there are trade-offs to consider:
- Instance creation – QuickJS takes longer to create new JavaScript contexts compared to the njs engine. For applications that create many short-lived contexts, this could impact performance.
- Long-running scripts – QuickJS performs excellently for longer-running scripts and complex logic, where its comprehensive feature set and garbage collection provide advantages.
- Memory management – QuickJS includes a garbage collector, which helps with memory management for complex applications but adds some overhead compared to njs’s simpler memory model.
- Context reuse optimization – We have addressed the context creation performance concern through the js_context_reuse directive, available since version 0.8.6. This directive sets the maximum number of JS contexts to be pooled and reused for the QuickJS engine, with a default value of 128. Each context handles a single request, and finished contexts are placed in a reusable pool rather than being destroyed immediately.
The trade-off is between performance and memory usage:
- Higher context reuse (default: 128) – Better performance due to reduced context creation overhead, but higher memory consumption
- Lower context reuse (or disabled with js_context_reuse 0) – Lower memory footprint but increased latency due to frequent context creation
Important Considerations for Context Reuse
In regards to context reuse, these are some points to consider:
- Memory growth – Memory consumption can grow if request handlers store data in the global object, as this data persists across requests within the same reused context
- No cross-request state – Do not put data in the global object expecting it to be available for subsequent requests, as contexts are assigned randomly from the pool
- Persistent data – If you need data persistence across requests, use the shared dictionary feature instead of global variables
Performance Comparison Configuration
// nginx.conf
load_module modules/ngx_http_js_module.so;
events {}
http {
js_import main from js/main.js;
server {
listen 8000;
# njs engine (baseline)
location /njs {
js_content main.handler;
}
# QuickJS with context reuse (default)
location /qjs {
js_engine qjs;
js_content main.handler;
}
# QuickJS without context reuse
location /qjs_no_context_reuse {
js_engine qjs;
js_context_reuse 0;
js_content main.handler;
}
}
}
// js/main.js
function handler(r) {
r.return(200, `Hello from ${njs.engine}`);
}
export default { handler };
Benchmark Results
The performance impact of context reuse is dramatic, as demonstrated by these benchmark results using wrk -c4 -t4 –d10
:
Configuration | Requests/sec per worker | Latency (avg) | Performance vs njs |
njs engine | 93,915 | 42.64μs | Baseline |
QuickJS (context reuse: 128) | 94,518 | 43.07μs | +0.6% |
QuickJS (context reuse: 0) | 5,363 | 742.18μs | -94.3% |
These results clearly demonstrate that QuickJS with context reuse enabled (the default) performs on par with the njs engine, while disabling context reuse results in a significant performance penalty due to the overhead of creating new contexts for each request.
The Future of JavaScript in NGINX
The introduction of QuickJS support represents a significant evolution in njs capabilities while preserving the strengths that made the original engine valuable. The njs engine remains the default choice and will continue to be maintained, but we do not plan to develop it indefinitely. In the future, we plan to switch to QuickJS as the default engine, though this transition may take some time and will depend heavily on user feedback and real-world adoption patterns.
We encourage users to gradually migrate to QuickJS and write new JavaScript code targeting the QuickJS engine. This approach allows you to take advantage of modern JavaScript features and ensures better future compatibility as the ecosystem evolves toward QuickJS as the primary engine.
We continue to optimize both engines and expand QuickJS module compatibility. Future enhancements may include additional NGINX-specific APIs and performance optimizations based on real-world usage patterns.
Try It Out and Share Your Feedback
QuickJS engine support is available starting from njs version 0.8.6, with ongoing enhancements in subsequent releases. We encourage you to experiment with both engines to determine which best fits your specific use cases.
To get started:
- Review the complete njs documentation for comprehensive guidance
- Learn about JavaScript engine configuration and compatibility considerations
Your feedback is invaluable in shaping the future development of JavaScript capabilities in NGINX. Whether you’re building simple request transformations or complex server-side logic, we want to hear about your experiences with both engines.
As always, if you have suggestions, encounter issues, or want to request additional features, please share them through GitHub Issues. Your input helps us continue improving JavaScript integration in NGINX environments.
