NGINX njs ahora soporta QuickJS: Adiós LUA, hola JavaScript moderno
6 min de lectura

NGINX njs ahora soporta QuickJS: Adiós LUA, hola JavaScript moderno

1239 palabras

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:

  1. njs con ES5 limitado: Funcional pero arcaico
  2. 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ónRequests/segLatenciavs njs tradicional
njs (ES5)93,91542.64μsLínea base
QuickJS (reuso contexto: 128)94,51843.07μs+0.6%
QuickJS (sin reuso contexto)5,363742.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

Últimas Entradas

6 min

1229 palabras

Finally: Modern JavaScript in NGINX (and we can forget about LUA)

When I read the NGINX announcement about QuickJS support in njs, I couldn’t help but smile. Finally I can stop struggling with LUA.

As someone who has configured more NGINX servers than I can remember (from my time at Arrakis to now at Carto), I’ve always been annoyed by the limitation of having to use LUA for complex logic in NGINX. It’s not that LUA is bad, but… why learn another language when I already master JavaScript?