Замена process.env.NODE_ENV

Переменная process.env.NODE_ENV давно стала стандартным механизмом определения режима работы JavaScript-приложения. Большинство библиотек экосистемы автоматически меняют поведение в зависимости от её значения:

  • development — режим разработки;
  • production — режим продакшена;
  • test — режим тестирования.

Во время сборки приложения значение этой переменной обычно подставляется непосредственно в исходный код. Благодаря этому инструменты оптимизации способны удалять ненужные ветви кода и уменьшать размер итогового бандла.

Пример:

if (process.env.NODE_ENV === 'development') {
  console.log('Режим разработки');
}

После подстановки значения:

if ('production' === 'development') {
  console.log('Режим разработки');
}

Так как условие всегда ложно, Esbuild сможет полностью удалить этот блок из итоговой сборки.


Почему требуется замена переменной

В браузере объект process отсутствует.

Код:

console.log(process.env.NODE_ENV);

выполнится корректно в Node.js, но в браузере приведёт к ошибке:

ReferenceError: process is not defined

Поэтому при создании фронтенд-приложений необходимо заменить обращение к переменной окружения на конкретное строковое значение ещё на этапе сборки.

Esbuild предоставляет встроенный механизм такой подстановки.


Механизм define

Для замены выражений используется параметр define.

Простейший пример:

await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/app.js',
  define: {
    'process.env.NODE_ENV': '"production"'
  }
});

После сборки каждое вхождение:

process.env.NODE_ENV

будет заменено на:

"production"

Важно обратить внимание на двойное экранирование строкового литерала.

Правильно:

define: {
  'process.env.NODE_ENV': '"production"'
}

Неправильно:

define: {
  'process.env.NODE_ENV': 'production'
}

Во втором случае Esbuild будет воспринимать значение как JavaScript-идентификатор, а не как строку.


Пример работы подстановки

Исходный код:

if (process.env.NODE_ENV === 'production') {
  enableAnalytics();
} else {
  enableDebugTools();
}

Конфигурация:

define: {
  'process.env.NODE_ENV': '"production"'
}

После преобразования:

if ("production" === 'production') {
  enableAnalytics();
} else {
  enableDebugTools();
}

После оптимизации:

enableAnalytics();

Ветка разработки полностью исчезнет.


Использование через CLI

Настройка может выполняться без JavaScript-конфигурации.

Пример:

esbuild src/index.js \
  --bundle \
  --outfile=dist/app.js \
  --define:process.env.NODE_ENV=\"production\"

Для режима разработки:

esbuild src/index.js \
  --bundle \
  --outfile=dist/app.js \
  --define:process.env.NODE_ENV=\"development\"

Это особенно удобно при создании npm-скриптов.

Пример файла package.json:

{
  "scripts": {
    "dev": "esbuild src/index.js --bundle --outfile=dist/app.js --define:process.env.NODE_ENV=\\\"development\\\"",
    "build": "esbuild src/index.js --bundle --minify --outfile=dist/app.js --define:process.env.NODE_ENV=\\\"production\\\""
  }
}

Использование с API сборщика

Чаще всего значение зависит от окружения запуска.

const production = process.env.NODE_ENV === 'production';

await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/app.js',
  define: {
    'process.env.NODE_ENV': JSON.stringify(
      production ? 'production' : 'development'
    )
  }
});

Функция JSON.stringify() позволяет автоматически формировать корректный строковый литерал.

Фактически код превращается в:

define: {
  'process.env.NODE_ENV': '"production"'
}

или

define: {
  'process.env.NODE_ENV': '"development"'
}

Влияние на Tree Shaking

Одно из главных преимуществ замены переменной окружения связано с механизмом Tree Shaking.

Рассмотрим пример:

export function logger(message) {
  if (process.env.NODE_ENV !== 'production') {
    console.log(message);
  }
}

После подстановки:

export function logger(message) {
  if ("production" !== 'production') {
    console.log(message);
  }
}

Условие становится заведомо ложным.

После минимизации:

export function logger() {}

или даже полностью удаляется, если функция нигде не используется.

В крупных проектах подобная оптимизация позволяет существенно уменьшить объём итогового JavaScript-кода.


Поддержка популярных библиотек

Многие библиотеки ориентируются именно на значение process.env.NODE_ENV.

Например:

if (process.env.NODE_ENV !== 'production') {
  warning();
}

Подобные конструкции используются в:

  • React;
  • Redux;
  • MobX;
  • Vue (в отдельных версиях);
  • различных утилитах логирования;
  • библиотеках разработки и отладки.

Без корректной подстановки переменной итоговый код может содержать дополнительные проверки и отладочную функциональность.


Автоматическая замена при использовании minify

При включённой минификации Esbuild может автоматически подставлять значение переменной в некоторых сценариях.

Пример:

await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  minify: true,
  outfile: 'dist/app.js'
});

Однако полагаться на автоматическое поведение не рекомендуется.

Явное указание:

define: {
  'process.env.NODE_ENV': '"production"'
}

делает конфигурацию предсказуемой и одинаковой во всех версиях сборщика.


Создание отдельных конфигураций

Часто используются разные параметры для разработки и продакшена.

Конфигурация разработки

await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  sourcemap: true,
  outfile: 'dist/app.js',
  define: {
    'process.env.NODE_ENV': '"development"'
  }
});

Особенности:

  • включены Source Maps;
  • сохранены предупреждения;
  • отсутствует минификация;
  • доступны инструменты отладки.

Конфигурация продакшена

await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  minify: true,
  outfile: 'dist/app.js',
  define: {
    'process.env.NODE_ENV': '"production"'
  }
});

Особенности:

  • минимальный размер файлов;
  • удаление лишнего кода;
  • отключение отладочных возможностей;
  • улучшенная производительность загрузки.

Использование дополнительных переменных окружения

Механизм define не ограничивается только NODE_ENV.

Пример:

define: {
  'process.env.NODE_ENV': '"production"',
  'process.env.API_URL': '"https://api.example.com"',
  'process.env.APP_VERSION': '"1.5.0"'
}

Код:

fetch(process.env.API_URL + '/users');

превратится в:

fetch("https://api.example.com" + '/users');

А значение версии:

console.log(process.env.APP_VERSION);

станет:

console.log("1.5.0");

Использование объекта конфигурации

Для большого количества переменных удобно формировать объект динамически.

const define = {};

for (const [key, value] of Object.entries(process.env)) {
  define[`process.env.${key}`] = JSON.stringify(value);
}

Далее:

await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  outfile: 'dist/app.js',
  define
});

Такой подход напоминает работу webpack-плагина DefinePlugin.


Ограничения подхода

define выполняет простую текстовую замену выражений на этапе компиляции.

Например:

process.env.NODE_ENV

заменится успешно.

Однако код:

process.env['NODE_ENV']

не будет обработан.

Аналогично:

const key = 'NODE_ENV';

process.env[key];

не подлежит замене, поскольку значение свойства определяется динамически.

Для корректной работы рекомендуется использовать прямое обращение:

process.env.NODE_ENV

Замена объекта process

Иногда сторонние библиотеки ожидают существование объекта process.

Вместо подстановки отдельных переменных можно определить весь объект:

define: {
  process: JSON.stringify({
    env: {
      NODE_ENV: 'production'
    }
  })
}

После преобразования:

console.log(process.env.NODE_ENV);

станет:

console.log({
  env: {
    NODE_ENV: 'production'
  }
}.env.NODE_ENV);

Подобный подход используется редко, поскольку увеличивает размер бандла и уступает точечной замене по эффективности.


Практический шаблон для большинства проектов

Наиболее распространённая конфигурация выглядит следующим образом:

import * as esbuild from 'esbuild';

const isProduction = process.argv.includes('--production');

await esbuild.build({
  entryPoints: ['src/index.js'],
  bundle: true,
  minify: isProduction,
  sourcemap: !isProduction,
  outfile: 'dist/app.js',
  define: {
    'process.env.NODE_ENV': JSON.stringify(
      isProduction ? 'production' : 'development'
    )
  }
});

Преимущества такого решения:

  • единая конфигурация для всех режимов;
  • корректная работа библиотек экосистемы;
  • эффективное удаление отладочного кода;
  • улучшенная работа Tree Shaking;
  • уменьшение размера итогового бандла;
  • отсутствие ошибок, связанных с отсутствием объекта process в браузере.