Por fin: JavaScript moderno en NGINX (y podemos olvidarnos de LUA)
Cuando leí el anuncio de NGINX sobre el soporte de QuickJS en njs, no pude evitar sonreír. Por fin puedo dejar de pelearme con LUA.
Como alguien que ha configurado más servidores NGINX de los que puedo recordar (desde mis tiempos en Arrakis hasta ahora en Carto), siempre me ha molestado la limitación de tener que usar LUA para lógica compleja en NGINX. No es que LUA sea malo, pero… ¿por qué aprender otro lenguaje cuando ya domino JavaScript?
El equipo de NGINX acaba de solucionar este problema introduciendo soporte para QuickJS en njs version 0.9.1, que nos da compatibilidad completa con ES2023. Y sinceramente, es un cambio que necesitábamos hace tiempo.
El problema que todos hemos vivido
Si has trabajado con NGINX en serio, conoces la frustración. Tienes dos opciones cuando necesitas lógica avanzada:
- njs con ES5 limitado: Funcional pero arcaico
- OpenResty con LUA: Potente pero… ¿en serio tengo que aprender LUA?
Como dice el equipo de NGINX en su artículo, era como “intentar construir una aplicación web moderna pero estar limitado a herramientas de hace una década”. Exactamente eso.
Durante años hemos tenido que hacer malabares entre simplicidad y funcionalidad. Yo personalmente he evitado lógica compleja en NGINX precisamente por esto. Era más fácil delegarlo a la aplicación backend que pelearme con LUA.
QuickJS: El motor que necesitábamos
La decisión de integrar QuickJS es brillante. Desarrollado por Fabrice Bellard (sí, el mismo de QEMU y FFmpeg), QuickJS ofrece:
- ES2023 completo: Módulos, async/await, destructuring, BigInt… todo lo que esperarías
- Footprint mínimo: Solo 367 KiB para un programa simple
- Sin dependencias externas: Apenas unos archivos C
- Compatibilidad drop-in: Tus scripts njs existentes funcionan sin cambios
Lo mejor es que mantiene la filosofía original de njs: ligero y enfocado, pero ahora con las herramientas del JavaScript moderno.
Configuración: Más simple de lo que esperaba
Cambiar al motor QuickJS es tan simple como añadir una directiva:
# nginx.conf
load_module modules/ngx_http_js_module.so;
events {}
http {
js_import main from js/main.js;
server {
listen 8000;
# Motor tradicional njs (ES5)
location /njs {
js_content main.handler;
}
# Motor QuickJS (ES2023)
location /qjs {
js_engine qjs;
js_content main.handler;
}
}
}
Y en tu script JavaScript:
// js/main.js
function handler(r) {
r.return(200, `Hello from ${njs.engine}`);
}
export default { handler };
¡Es así de simple! Puedes tener ambos motores corriendo simultáneamente durante la migración.
Un ejemplo real: Análisis de headers con ES2023
Aquí es donde la cosa se pone interesante. Con QuickJS puedes usar características modernas de JavaScript que hacen el código mucho más limpio:
// js/analytics.js
class RequestAnalytics {
// Generator function para procesar 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 con valores por defecto
const {
method = 'GET',
uri = '/',
httpVersion = '1.0'
} = r;
// Usar el generator
const headerStats = [];
for (const metric of this.getHeaderMetrics(r.headersIn)) {
headerStats.push(metric);
}
const timestamp = BigInt(Date.now()); // BigInt nativo
const headerCount = headerStats.length;
const customHeaders = headerStats.filter(({ type }) => type === 'custom').length;
r.return(200, JSON.stringify({
message: `Request procesado en ${timestamp}`,
stats: { headerCount, customHeaders },
serverInfo: `${method} ${uri} HTTP/${httpVersion}`
}, null, 2));
}
}
const analytics = new RequestAnalytics();
export default { processRequest: (r) => analytics.processRequest(r) };
Esto antes era imposible con njs tradicional. Classes, generators, destructuring, BigInt… todo funciona perfectamente.
Performance: Lo que necesitas saber
Como siempre en infraestructura, hay trade-offs. Según las pruebas de NGINX:
Configuración | Requests/sec | Latencia | vs njs tradicional |
---|---|---|---|
njs (ES5) | 93,915 | 42.64μs | Baseline |
QuickJS (context reuse: 128) | 94,518 | 43.07μs | +0.6% |
QuickJS (sin context reuse) | 5,363 | 742.18μs | -94.3% |
La clave está en js_context_reuse
(habilitado por defecto). Con reutilización de contextos, QuickJS rinde igual que njs tradicional. Sin ella, es inutilizable para alta carga.
La configuración por defecto es perfecta para la mayoría de casos:
http {
js_engine qjs;
js_context_reuse 128; # 128 contextos reutilizables (default)
# Tu configuración...
}
Mi plan de migración personal
Como alguien que gestiona varias infraestructuras, mi estrategia será:
Fase 1: Experimentos en desarrollo
- Configurar QuickJS en entornos de dev
- Migrar scripts simples para probar compatibilidad
- Medir performance en cargas reales
Fase 2: Casos de uso específicos
- Rate limiting inteligente: Usando Maps y Sets de ES6
- Request routing dinámico: Con async/await para llamadas a APIs
- Logging estructurado: Con template literals y JSON moderno
Fase 3: Nueva funcionalidad
- Aprovechar características que antes evitaba por limitaciones de njs
- Eliminar dependencias de LUA (¡por fin!)
- Consolidar toda la lógica NGINX en JavaScript
Casos de uso donde esto brillará
Con JavaScript moderno disponible, nuevas posibilidades se abren:
API Gateway avanzado
// Routing dinámico con async/await
async function routeRequest(r) {
const config = await loadRoutingConfig();
const route = config.routes.find(r =>
r.pattern.test(r.uri) && r.method === r.method
);
if (route?.requiresAuth) {
const isValid = await validateToken(r.headersIn.authorization);
if (!isValid) {
r.return(401, 'Unauthorized');
return;
}
}
r.internalRedirect(route.target);
}
Cache inteligente con headers
// Usando Maps para cache strategies
const cacheStrategies = new Map([
['api', { ttl: 300, vary: ['authorization'] }],
['assets', { ttl: 86400, vary: [] }],
['dynamic', { ttl: 60, vary: ['user-agent', 'accept-language'] }]
]);
function setCacheHeaders(r) {
const path = r.uri;
const strategy = [...cacheStrategies.entries()]
.find(([pattern]) => path.includes(pattern))?.[1];
if (strategy) {
r.headersOut['Cache-Control'] = `max-age=${strategy.ttl}`;
if (strategy.vary.length > 0) {
r.headersOut['Vary'] = strategy.vary.join(', ');
}
}
}
Por qué esto cambia las reglas del juego
Durante años, la elección era:
- NGINX simple: Rápido pero limitado
- NGINX + LUA: Potente pero complejo
- Proxy todo al backend: Simple pero ineficiente
Ahora tenemos una tercera opción: NGINX + JavaScript moderno. Para alguien como yo, que viene de casi 30 años tocando diferentes tecnologías, esto es un game changer.
No más LUA. No más lógica delegada innecesariamente al backend. Solo JavaScript que ya conocemos.
Consideraciones importantes
Antes de salir corriendo a migrar todo:
Gestión de memoria
# Con context reuse habilitado
http {
js_engine qjs;
js_context_reuse 128; # Balance memoria/performance
# No almacenes datos en el objeto global
# Usa diccionarios compartidos para persistencia
}
Compatibilidad hacia atrás
# Puedes usar ambos motores simultáneamente
server {
# Legacy scripts
location /old {
js_content legacy.handler; # njs por defecto
}
# Nuevos scripts
location /new {
js_engine qjs;
js_content modern.handler;
}
}
Depuración
El debugging es más fácil con JavaScript familiar, pero recuerda que estás en contexto NGINX, no en Node.js o browser.
El futuro que se avecina
Según NGINX, el plan es eventualmente hacer QuickJS el motor por defecto. Esto tiene sentido total: ¿para qué mantener un motor ES5 custom cuando tienes uno ES2023 completo?
Para nosotros, los que gestionamos infraestructura, significa:
- Un lenguaje menos que aprender (adiós LUA)
- Más funcionalidad en edge (menos load en backend)
- Configuraciones más expresivas (JavaScript > declarativo)
- Mejor debugging (herramientas familiares)
Mi experiencia hasta ahora
He estado probando QuickJS desde que salió la versión 0.9.1, y honestamente, es lo que siempre quise que fuera njs. El salto de ES5 limitado a ES2023 completo es como pasar de una bicicleta a un Tesla.
Poder usar async/await
, destructuring, template literals, y todas las características modernas en NGINX… Por fin siento que puedo expresar lógica compleja sin comprometerme.
¿Merece la pena el cambio?
Para casos nuevos: Absolutamente. No hay razón para empezar un proyecto nuevo con njs tradicional.
Para migración de existentes: Depende. Si tienes scripts njs simples funcionando, no hay prisa. Pero si estás limitado por ES5 o considerando LUA, QuickJS es la respuesta.
Para eliminar LUA: Si tienes lógica en OpenResty que podrías hacer en NGINX puro… ahora puedes.
¿Qué opináis? ¿Habéis tenido que lidiar con las limitaciones de njs? ¿Usáis LUA en OpenResty y estáis hartos?
Personalmente, creo que esto va a cambiar cómo diseñamos arquitecturas web. Poder hacer más en edge con un lenguaje familiar es un win-win. Era hora de que NGINX se pusiera al día con JavaScript moderno.
PD: Si queréis experimentar, necesitáis njs 0.9.1 o superior. La documentación oficial sobre motores JavaScript está bastante completa. Vale la pena echarle un vistazo.
Comentarios