Сборка с разными конфигурациями для dev и prod

Архитектура сборки в esbuild строится вокруг идеи быстрых инкрементальных пересборок и минимальной конфигурационной сложности. При этом реальные проекты неизбежно требуют различного поведения в режиме разработки и в продакшене: разные уровни оптимизации, отладочные инструменты, стратегия работы с исходным кодом и ассетами.


Базовый принцип разделения конфигураций

Ключевая идея заключается в том, что конфигурация не должна быть монолитной. Вместо этого она разделяется на:

  • общую часть (shared config) — повторно используемые настройки
  • dev-конфигурацию — оптимизированную под скорость и отладку
  • prod-конфигурацию — оптимизированную под размер, производительность и стабильность

В esbuild это реализуется либо через программный API, либо через скрипты CLI с параметризацией.


Общая конфигурация как основа

Общий слой включает параметры, которые не зависят от окружения:

// build/base.js
export const baseConfig = {
  entryPoints: ['src/index.ts'],
  bundle: true,
  platform: 'browser',
  format: 'esm',
  target: ['es2020'],
  outdir: 'dist',
  loader: {
    '.png': 'file',
    '.svg': 'file',
    '.css': 'css'
  }
};

Этот слой определяет фундамент:

  • точку входа
  • режим бандлинга
  • платформу выполнения
  • формат модулей
  • базовые правила обработки файлов

Dev-конфигурация: скорость и обратная связь

Режим разработки в esbuild ориентирован на минимальное время цикла «изменение → результат».

Основные характеристики dev-сборки

  • отключён минификатор
  • включены source maps
  • активен watch-режим
  • используется incremental rebuild
  • добавляется dev-режим переменных окружения

Пример конфигурации dev

// build/dev.js
import esbuild from 'esbuild';
import { baseConfig } from './base.js';

esbuild.context({
  ...baseConfig,
  sourcemap: true,
  minify: false,
  define: {
    'process.env.NODE_ENV': '"development"'
  }
}).then(async (ctx) => {
  await ctx.watch();

  await ctx.serve({
    port: 3000,
    servedir: 'public'
  });
});

Особенности dev-режима

Source maps

Отладка становится прозрачной за счёт карт исходников:

sourcemap: true

Варианты:

  • true — отдельные файлы sourcemap
  • inline — встроенные в JS
  • external — отдельные .map файлы

Watch-режим

Esbuild пересобирает только изменённые модули:

await ctx.watch();

Механизм основан на инкрементальной компиляции, что обеспечивает минимальные задержки.


Dev-сервер

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

await ctx.serve({
  port: 3000,
  servedir: 'public'
});

Он не является полноценным backend-сервером, но достаточен для фронтенд-разработки.


Incremental build

Дополнительное ускорение достигается за счёт кеширования:

esbuild.build({
  ...baseConfig,
  incremental: true
});

Prod-конфигурация: оптимизация и стабильность

Продакшен-сборка ориентирована на минимальный размер и максимальную производительность.

Основные характеристики prod-сборки

  • включена минификация
  • удаляются неиспользуемые участки кода (tree shaking)
  • активируется агрессивная оптимизация
  • фиксируются версии target
  • отключается watch

Пример prod-конфигурации

// build/prod.js
import esbuild from 'esbuild';
import { baseConfig } from './base.js';

esbuild.build({
  ...baseConfig,
  minify: true,
  sourcemap: false,
  splitting: true,
  format: 'esm',
  define: {
    'process.env.NODE_ENV': '"production"'
  },
  metafile: true,
  chunkNames: 'chunks/[name]-[hash]'
});

Минификация и её влияние

Минификация включает:

  • удаление пробелов
  • сокращение идентификаторов
  • упрощение выражений
  • удаление dead code
minify: true

В esbuild минификация реализована на уровне Go-движка, что делает её значительно быстрее большинства JS-минификаторов.


Tree shaking и устранение мёртвого кода

При включённом бандлинге esbuild автоматически удаляет неиспользуемые экспорты:

bundle: true

Особенно эффективно при использовании ES Modules:

export const used = () => {};
export const unused = () => {};

Если unused не импортируется, он исключается из финального бандла.


Разделение кода (code splitting)

Для крупных приложений используется разделение чанков:

splitting: true,
format: 'esm'

Важно:

  • работает только с esm
  • требует outdir, а не outfile

Анализ сборки через metafile

Metafile позволяет анализировать структуру бандла:

import fs from 'fs';

const result = await esbuild.build({
  ...baseConfig,
  metafile: true,
  minify: true
});

fs.writeFileSync(
  'meta.json',
  JSON.stringify(result.metafile)
);

Использование metafile:

  • анализ зависимостей
  • поиск тяжёлых модулей
  • оптимизация структуры импорта

Разделение конфигурации через env-переменные

Часто используется единый входной файл с переключением режима:

const mode = process.env.NODE_ENV;

const isProd = mode === 'production';

esbuild.build({
  ...baseConfig,
  minify: isProd,
  sourcemap: !isProd,
  define: {
    'process.env.NODE_ENV': JSON.stringify(mode)
  }
});

CLI-подход к dev/prod

Dev-сборка:

esbuild src/index.ts --bundle --servedir=public --sourcemap --watch

Prod-сборка:

esbuild src/index.ts --bundle --minify --outdir=dist --format=esm

CLI используется для простых проектов или CI-скриптов без сложной логики.


Подход с конфигурационными файлами

Структура проекта часто выглядит так:

build/
  base.js
  dev.js
  prod.js

И единый вход:

import './build/dev.js';

или

import './build/prod.js';

Обработка CSS и ассетов в разных режимах

CSS в dev:

  • без минификации
  • с sourcemap

CSS в prod:

  • объединение в один файл
  • минификация
loader: {
  '.css': 'css'
},
minify: true

Управление target и совместимостью

В prod часто фиксируется более строгий target:

target: ['es2019']

Это влияет на:

  • синтаксис output-кода
  • полифиллы (если используются)
  • размер бандла

Dev может использовать более современный target:

target: ['esnext']

External зависимости

В некоторых случаях зависимости исключаются из бандла:

external: ['react', 'react-dom']

Используется в prod при:

  • CDN-подключениях
  • микрофронтендах
  • серверном рендеринге

Инкрементальные сборки и кеширование

Dev-режим активно использует кеш:

let ctx = await esbuild.context(baseConfig);

await ctx.watch();

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

  • повторная сборка использует AST-кеш
  • изменяются только затронутые модули
  • время пересборки измеряется миллисекундами

Частые ошибки при разделении конфигураций

Смешивание dev и prod флагов

Нежелательно включать одновременно:

  • watch и minify
  • sourcemap: true и splitting без ESM
  • разные target без необходимости

Неверный формат при splitting

format: 'cjs' // не поддерживает splitting

Правильно:

format: 'esm'

Использование outfile с code splitting

outfile: 'dist/app.js' // конфликтует со splitting

Нужно:

outdir: 'dist'

Стратегия организации конфигураций в крупных проектах

Типовая структура:

  • единая базовая конфигурация
  • функции-генераторы конфигов
  • переключение через NODE_ENV
export function createConfig(mode) {
  return {
    ...baseConfig,
    minify: mode === 'production',
    sourcemap: mode !== 'production'
  };
}

Такой подход обеспечивает:

  • масштабируемость
  • предсказуемость
  • изоляцию окружений