njs в NGINX: где JavaScript действительно работает, а где — нет
njs добавляет JavaScript в NGINX, но не превращает его в Node.js. Код выполняется только в строго заданных точках обработки запроса и должен укладываться в миллисекунды. Два движка — устаревший встроенный njs и современный QuickJS — различаются по скорости и возможностям.
njs: где JavaScript в NGINX действительно работает, а где — нет
njs не превращает NGINX в Node.js. Он позволяет написать несколько строк JavaScript, которые выполнятся в строго определённый момент: когда NGINX решает, пустить ли запрос дальше, какой ответ вернуть или какие заголовки добавить. Контекст живёт только во время обработки запроса или таймера, а затем исчезает. Никакого постоянного процесса, никакого event loop — только микро-вспышки кода, которые должны уложиться в миллисекунды. Если скрипт зависнет хоть на секунду, замрёт весь воркер. Это не гибкость, а жёсткое ограничение.
Два движка: QuickJS против устаревшего встроенного njs
njs поддерживает два JavaScript-движка. Встроенный njs работает по ES5.1 и объявлен deprecated с версии 1.0.0 — он лишён современных фич, но быстрее создаёт контексты. QuickJS — это современный движок с поддержкой ES2023, модулей, асинхронных генераторов и BigInt. Переключение между ними делается одной строчкой в конфигурации NGINX:
js_engine qjs;
QuickJS медленнее создаёт контексты — это критично для систем с короткими запросами и высокой нагрузкой. В документации нет бенчмарков, зато есть предупреждение: если гонять скрипты с QuickJS на сотнях запросов в секунду, задержки вырастут.
Ещё один нюанс — переиспользование контекстов. При js_context_reuse on; QuickJS может брать готовые контексты из пула, но глобальные переменные при этом остаются между запросами. Если забыть их сбросить, один запрос испортит следующий. Это не баг, а следствие устройства движка: контекст живёт дольше одного запроса, но не гарантирует чистоту.
Пять операционных деталей, которые решают исход
-
Объект
r— единственный интерфейс к NGINX. Он даёт доступ к переменным (r.variables), заголовкам (r.headersIn,r.headersOut), а также позволяет запускать подзапросы (r.subrequest()) и выполнять внутренние редиректы (r.internalRedirect()). -
Точки интеграции жёстко заданы. Например,
js_header_filterиjs_body_filterне поддерживают асинхронность — они должны вернуть результат сразу. Аjs_accessобязан завершиться мгновенно, иначе блокирует обработку запроса. -
Shared-словарь (
js_shared_dict_zone) — единственный способ делиться состоянием между воркерами NGINX. Без него данные между запросами не сохраняются. -
Нет доступа к файловой системе за пределами встроенных модулей. Хотите прочитать конфиг? Придётся делать это через переменные NGINX. Работа с большими файлами невозможна без предварительной загрузки в память.
-
Нет поддержки npm-пакетов и потоков. Долгий синхронный код блокирует весь воркер, даже если обёрнут в
async/await.
Три примера, которые можно копировать и адаптировать
1. Динамическая маршрутизация для A/B-тестирования или canary-релизов
function choose_upstream(r) {
const mark = (r.headersIn['X-Exp'] || getCookie(r, 'exp') || '').toLowerCase();
if (mark === 'v2') return 'app_v2';
const bucket = Math.abs(hashCode(r.variables.request_id)) % 10;
return bucket === 0 ? 'app_v2' : 'app_v1';
}
Ограничение: если r.variables.request_id нестабилен, распределение по бакетам сломается. Для продакшн-кода стоит использовать более надёжный идентификатор, например, хэш от r.remoteAddress + r.uri.
2. Канонизация URL: убираем www, переводим на HTTPS, чистим параметры
function canonical_redirect(r) {
const host = (r.headersIn['Host'] || '').toLowerCase();
if (host.startsWith('www.')) {
r.return(301, `https://${host.slice(4)}${r.uri}${r.args ? '?' + r.args : ''}`);
return;
}
if (r.variables.scheme !== 'https') {
r.return(301, `https://${host}${r.uri}${r.args ? '?' + r.args : ''}`);
return;
}
r.internalRedirect('@upstream');
}
Edge case: если заголовок Host отсутствует или искажён, скрипт упадёт. В продакшн-коде стоит добавить проверку и fallback.
3. Безопасность на границе сети: добавляем заголовки CSP и HSTS
function add_security_headers(r) {
r.headersOut['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains';
r.headersOut['Content-Security-Policy'] = "default-src 'self'";
}
Ограничение: если r.headersOut уже содержит заголовки, они будут перезаписаны. Для сохранения существующих заголовков нужно сначала их прочитать и объединить.
Что njs не потянет: три причины не строить на нём бизнес-логику
-
Однопоточность. Долгий синхронный код блокирует весь воркер. Даже с
async/awaitэто не спасёт, если операция реально долгая. njs не Node.js: здесь нет отдельного event loop. -
Нет нормального доступа к данным. Нет работы с файловой системой, нет npm-пакетов, нет потоков. Хотите обработать большой JSON-файл? Придётся тащить его в память через переменные NGINX — что не всегда возможно.
-
Нет изоляции между воркерами. Shared-словарь — единственный способ делиться состоянием, но он не атомарен и не транзакционен. Race condition между воркерами — типичная проблема.
Практическое правило: используйте njs для микро-обработки, а не для приложений
njs — это инструмент для тонкой настройки на границе сети. Он удобен для редиректов, фильтрации заголовков, простой маршрутизации или добавления безопасности. Но если ваша задача требует сложной бизнес-логики, долгих вычислений или работы с большими данными, лучше вынести её за NGINX.
QuickJS удобнее для современного кода, но медленнее в создании контекстов. Встроенный njs быстрее, но deprecated. Выбор движка — это trade-off между удобством и производительностью.
Если решитесь использовать njs, начните с малого: возьмите простой скрипт для редиректов или добавления заголовков. Убедитесь, что он не блокирует воркер. И только потом расширяйте логику. Иначе рискуете обнаружить, что ваш "микро-скрипт" стал узким местом.