Por fin: JavaScript moderno en NGINX (y podemos olvidarnos de LUA)
Cuando leí el anuncio de NGINX sobre el soporte QuickJS en njs, no pude evitar sonreír. Por fin puedo dejar de luchar con LUA.
Como alguien que ha configurado más servidores NGINX de los que puedo recordar (desde mi época 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 resolver este problema introduciendo el soporte QuickJS en njs versión 0.9.1, dándonos compatibilidad completa con ES2023. Y honestamente, este es un cambio que necesitábamos desde hace tiempo.
El problema que todos hemos sufrido
Si has trabajado seriamente con NGINX, 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 malabarear entre simplicidad y funcionalidad. Personalmente, he evitado la lógica compleja en NGINX precisamente por esto. Era más fácil delegar a la aplicación backend que luchar con LUA.
QuickJS: El motor que necesitábamos
La decisión de integrar QuickJS es brillante. Desarrollado por Fabrice Bellard (sí, el mismo tipo de QEMU y FFmpeg), QuickJS ofrece:
- ES2023 completo: Módulos, async/await, destructuring, BigInt… todo lo que esperarías
- Huella mínima: Solo 367 KiB para un programa simple
- Sin dependencias externas: Solo unos pocos 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 herramientas modernas de JavaScript.
Configuración: Más simple de lo esperado
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 njs tradicional (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 ejecutándose simultáneamente durante la migración.
Un ejemplo real: Análisis de headers con ES2023
Aquí es donde las cosas se ponen interesantes. Con QuickJS, puedes usar características modernas de JavaScript que hacen el código mucho más limpio:
// js/analytics.js
class RequestAnalytics {
// Función generadora 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 generador
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 processed in ${timestamp}`,
stats: { headerCount, customHeaders },
serverInfo: `${method} ${uri} HTTP/${httpVersion}`
}, null, 2));
}
}
const analytics = new RequestAnalytics();
export default { processRequest: (r) => analytics.processRequest(r) };
Esto era imposible previamente con njs tradicional. Clases, generadores, destructuring, BigInt… todo funciona perfectamente.
Rendimiento: Lo que necesitas saber
Como siempre en infraestructura, hay compensaciones. Según los tests de NGINX:
| Configuración | Requests/seg | Latencia | vs njs tradicional |
|---|---|---|---|
| njs (ES5) | 93,915 | 42.64μs | Línea base |
| QuickJS (reuso contexto: 128) | 94,518 | 43.07μs | +0.6% |
| QuickJS (sin reuso contexto) | 5,363 | 742.18μs | -94.3% |
La clave es js_context_reuse (habilitado por defecto). Con reuso de contexto, QuickJS funciona igual que njs tradicional. Sin él, es inusable para carga alta.
La configuración por defecto es perfecta para la mayoría de casos:
http {
js_engine qjs;
js_context_reuse 128; # 128 contextos reusables (por defecto)
# 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 desarrollo
- Migrar scripts simples para probar compatibilidad
- Medir rendimiento bajo carga real
Fase 2: Casos de uso específicos
- Rate limiting inteligente: Usando Maps y Sets de ES6
- Enrutamiento dinámico de requests: Con async/await para llamadas API
- Logging estructurado: Con template literals y JSON moderno
Fase 3: Nueva funcionalidad
- Aprovechar características que previamente evité por limitaciones de njs
- Eliminar dependencias de LUA (¡por fin!)
- Consolidar toda la lógica de NGINX en JavaScript
Casos de uso donde esto brillará
Con JavaScript moderno disponible, nuevas posibilidades se abren:
API Gateway avanzado
// Enrutamiento 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);
}
Caché inteligente con headers
// Usando Maps para estrategias de caché
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 el 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, con casi 30 años trabajando con diferentes tecnologías, esto es un cambio radical.
No más LUA. No más lógica innecesariamente delegada al backend. Solo JavaScript que ya conocemos.
Consideraciones importantes
Antes de migrar todo corriendo:
Gestión de memoria
# Con reuso de contexto habilitado
http {
js_engine qjs;
js_context_reuse 128; # Equilibrio memoria/rendimiento
# No almacenar datos en el objeto global
# Usar diccionarios compartidos para persistencia
}
Compatibilidad hacia atrás
# Puedes usar ambos motores simultáneamente
server {
# Scripts legacy
location /old {
js_content legacy.handler; # njs por defecto
}
# Scripts nuevos
location /new {
js_engine qjs;
js_content modern.handler;
}
}
El futuro que nos espera
Según NGINX, el plan es eventualmente hacer QuickJS el motor por defecto. Esto tiene mucho sentido: ¿por qué mantener un motor ES5 personal cuando tienes uno ES2023 completo?
Para nosotros los de infraestructura, esto significa:
- Un lenguaje menos que aprender (adiós LUA)
- Más funcionalidad en el edge (menos carga en backend)
- Configuraciones más expresivas (JavaScript > declarativo)
- Mejor debugging (herramientas familiares)
¿Vale la pena el cambio?
Para casos nuevos: Absolutamente. No hay razón para empezar un proyecto nuevo con njs tradicional.
Para migrar 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é te parece? ¿Has tenido que lidiar con limitaciones de njs? ¿Usas LUA en OpenResty y estás cansado de él?
Personalmente, creo que esto cambiará cómo diseñamos arquitecturas web. Poder hacer más en el edge con un lenguaje familiar es una situación win-win. Era hora de que NGINX se actualizara con JavaScript moderno.
PD: Si quieres experimentar, necesitas njs 0.9.1 o superior. La documentación oficial sobre motores JavaScript está bastante completa. Merece la pena revisarla.








Comentarios