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

Что я ожидаю от бандлера?

Используя последние ~6 лет Webpack я сильно привязался к его экосистеме и возможностям. В частности, я ожидаю от бандлера:

Возможность работы через CLI и Node интерфейсы, динамическую модель имен выходных файлов (включая [contenthash] для решения проблем с кешированием), tree-shaking, генерацию source maps, минификацию и сжатие файлов в gzip / brotli / webp, приведение синтаксиса JS к целевым браузерам из browserslist и автоматический полифиллинг отсутствующих в браузерах интерфейсов и css-префиксов, поддержку TS-типизации для конфигов, возможность обрабатывать каждый файл отдельно с помощью функции-загрузчика, поддержку CSS Modules и Sass и вынесение стилей в отдельные файлы, автоматическое включение ссылок на выходные файлы в HTML, внедрение ссылок для прелоадинга (шрифтов, например), удобный инструмент для анализа размера файлов и их влияния на выходной файл, возможность внедрения сторонних библиотек в файловые вотчеры (таких, как файлогенератор из этой статьи), возможность разбиение кода на чанки для ленивой подгрузки, поддержку SSR, добавление комментариев в файлы, определение глобальных переменных, трансформацию TS и JSX.

И, разумеется, высокую скорость.

Экосистема Webpack позволяет справиться со всем этим, но большим трудом - конфиг довольно запутанный, а количество внешних зависимостей в виде loaders и плагинов зашкаливает, достаточно посмотреть на текущий конфиг, который я использую в работе. В этом плане esbuild, о котором дальше пойдет речь, намного эффективней.

Что может esbuild без плагинов?

Практически половину. Базовый конфиг вида

import { BuildOptions } from 'esbuild';
import { env } from '../env';

const config: BuildOptions = {
  entryPoints: ['src/client.tsx'],
  bundle: true,
  logLevel: 'warning',
  format: 'iife',
  publicPath: '/',
  assetNames: env.FILENAME_HASH ? '[name]-[hash]' : '[name]',
  outdir: paths.build,
  metafile: true,
  minify: true,
  treeShaking: true,
  sourcemap: 'linked',
  banner: {
    js: `/* @env ${env.NODE_ENV} @commit ${env.GIT_COMMIT} */`,
    css: `/* @env ${env.NODE_ENV} @commit ${env.GIT_COMMIT} */`,
  },
  legalComments: 'external',
  platform: 'browser',
  target: 'chrome100',
  define: {
    IS_CLIENT: JSON.stringify(true),
    process: JSON.stringify({
      env: { NODE_ENV: env.NODE_ENV, GIT_COMMIT: env.GIT_COMMIT },
    }),
    'process.env.NODE_ENV': JSON.stringify(env.NODE_ENV),
  },
  resolveExtensions: ['.js', '.ts', '.tsx'],
  loader: {
    '.svg': 'text',
    '.png': 'file',
    '.woff': 'file',
    '.ttf': 'file',
  }
}

уже даст на выходе готовые файлы с хешами, source maps, минификацией, внедренными переменными и синтаксисом, понятным целевому браузеру. При этом размер выходного файла будет фактически идентичным тому, что выдает Webpack. Единственным неудобством здесь является дублирующее определение process.env.NODE_ENV, так как define работает со строками и не может заменить переменную без явного определения. То есть без 'process.env.NODE_ENV': JSON.stringify(env.NODE_ENV) в итоговый код включается две версии React - production + development. Плагины esbuild-plugin-define, esbuild-plugin-env и esbuild-plugin-environment, к сожалению, ситуацию не исправляют - после ряда попыток мне не удалось найти решение без дубляжа, поэтому оставил текущее решение без плагинов.

Browserslist

Esbuild не читает browserslist, находящиеся в package.json, и имеет другой синтаксис для определения target. На помощь приходит плагин esbuild-plugin-browserslist:

import browserslist from 'browserslist';
import { resolveToEsbuildTarget } from 'esbuild-plugin-browserslist';

config.target = resolveToEsbuildTarget(
  browserslist(), 
  { printUnknownTargets: true }
)

Обработка содержимого файлов и __dirname

Хотя есть плагины esbuild-plugin-fileloc и esbuild-plugin-replace-regex, при их совместном использовании один из них не срабатывает. В целом довольно многие плагины не совместимы друг с другом. Хотя сообщество пытается решить эту проблему через pipe-паттерны esbuild-plugin-transform или esbuild-plugin-pipe, у меня сохранялись ошибки и при их использовании. Также в esbuild-plugin-fileloc поведение замены __dirname не соответствовало тому, как преобразует Webpack с настройкой { node: { __filename: true, __dirname: true } } , поэтому пришлось написать собственный плагин для обработки dk-esbuild-plugin-replace.

CSS Modules и Sass

Плагин esbuild-sass-plugin прекрасно справился с этой задачей, включая возможность сокращения путей импортов через loadPaths (чтобы можно было делать @import "mixins"). Вендорные префиксы добавляются esbuild автоматически исходя из target, а вывод в отдельные css файлы делается одной строчкой вместо возни с MiniCssExtractPlugin и порядком лоадеров, как в Webpack.

import { postcssModules, sassPlugin } from 'esbuild-sass-plugin';

// глобальные стили
config.plugins.push(sassPlugin({ 
  filter: /(global)\.scss$/, 
  type: 'css', 
  loadPaths: ['./src/styles'] 
}));

// модульные стили
config.plugins.push(sassPlugin({
  filter: /\.scss$/i,
  type: 'css',
  loadPaths: ['./src/styles'],

  // https://github.com/madyankin/postcss-modules
  transform: postcssModules({ generateScopedName: '[path][local]' }),
}));

Вставка ссылок на ресурсы в html-файл

Аналог html-webpack-plugin в esbuild это esbuild-plugin-html.

import { htmlPlugin } from '@craftamap/esbuild-plugin-html';

config.plugins.push(htmlPlugin({
  files: [{
    entryPoints: ['src/client.tsx'],
    filename: 'template.html',
    scriptLoading: 'defer',
    htmlTemplate: fs.readFileSync(path.resolve('./src/templates/template.html'), 'utf-8'),
  }],
}));

Он справился с задачей, однако плагина для вставки preload ссылок я не нашел, поэтому снова написал плагин под эту задачу esbuild-plugin-inject-preload. Этот функционал критичен для ряда проектов, в которых используется определение ширины динамических блоков, растягиваемых контентом. К примеру, для задачи "показывать вариант меню, который вмещается в ширину браузера" можно использовать либо ручной способ через описание @media (max-width: 600px) либо js-код. При втором варианте, если шрифты не успели загрузиться, то ширина блока будет высчитываться исходя из дефолтного шрифта, что будет расходиться с размерами после загрузки шрифтов. Также прелоадинг некоторых ресурсов, в том числе шрифтов, положительно отражается на UX и перфомансе отрисовки контента (не будет прыгающих строк и элементов).

import { pluginInjectPreload } from 'esbuild-plugin-inject-preload';

config.plugins.push(pluginInjectPreload({
  ext: '.woff',
  linkType: 'font',
  templatePath: path.resolve(paths.build, 'template.html'),
  replaceString: '<!-- FONT_PRELOAD -->',
}));

Сжатие в gzip / brotli

С задачей хорошо справился плагин esbuild-plugin-compress, однако пришлось повозиться с условиями для micromatch, чтобы сжимались только js и css файлы. Также он не умеет обращаться с файлами, разложенными по разным папкам условием assetNames: '[ext]/[name]-[hash]'.

import { compress } from 'esbuild-plugin-compress';

config.write = false;
config.assetNames = '[ext]/[name]-[hash]'; // не работает
config.assetNames = '[name]-[hash]'; // работает

config.plugins.push(compress({
  gzip: true,
  gzipOptions: { level: 9 },
  brotli: true,
  emitOrigin: true,
  // https://github.com/micromatch/micromatch
  exclude: ['!(**/*.@(js|css))'],
}));

Автоматический полифиллинг

К сожалению, этой возможности сейчас в esbuild нет, однако всегда можно написать плагин. Так, я привык использовать SWC в связке с Webpack, так как он быстрее Babel и поддерживает автоматический полифиллинг. Для его интеграции есть медленный и неподдерживающий автополифиллинг плагин esbuild-plugin-swc, так что пришлось сделать свою версию esbuild-plugin-swc2. В итоге я получил синтаксис, к которому привык за последние годы и с недостатками которого умею справляться, а также уверенность, что внезапно не выстрелит что-то вроде String.padStart is not a function.

import { pluginSwc } from 'esbuild-plugin-swc2';

config.plugins.push(pluginSwc({
  jsc: {
    parser: { tsx: true, syntax: 'typescript' },
    transform: { react: { runtime: 'automatic', useBuiltins: false } },
  },
  env: { 
    mode: 'usage', 
    targets: JSON.parse(fs.readFileSync('package.json', 'utf-8')).browserslist,
  },
}));

Однако я столкнулся с тем, что минимальный синтаксис, к которому может привести связка esbuild + SWC - это es5. То есть при попытке создать бандл, поддерживающий Firefox 50, он упадет с ошибками

ERROR: Transforming destructuring to the configured target environment 
("firefox50") is not supported yet 
ERROR: Transforming const to the configured target environment 
("firefox50") is not supported yet

Таким образом, хотя полифиллинг работает корректно и код бы работал в этом браузере, сам esbuild пока не поддерживает трансформацию в такой синтаксис. Возможно, поможет использование esbuild-plugin-babel, а не SWC, но для текущих проектов мне не была нужна поддержка устаревших браузеров, поэтому этот вариант я не исследовал.

Анализ бандла

Я привык использовать прекрасный инструмент webpack-bundle-analyzer, однако его порта для esbuild нет. В документации рекомендуется сделать вывод мета-файла и грузить его в https://esbuild.github.io/analyze/ или https://bundle-buddy.com/ , которые, к сожалению, и близко не такие удобные + требуется ручная работа по загрузке файла в онлайн-инструменты.

https://esbuild.github.io/analyze/

Есть плагин esbuild-visualizer, который тоже требует сначала сохранить мета-файл, затем отдельной командой сгенерировать html-файл с отчетом, который можно открыть в браузере.

esbuild-visualizer
esbuild-visualizer

Избалованный удобством webpack-bundle-analyzer, я набросал очередной плагин для его интеграции esbuild-plugin-webpack-analyzer. Пока что он работает только в базовом виде (выводит только stats-размеры файлов и единственную точку входа), но в перспективе я планирую доработать его функционал под все сценарии.

esbuild-plugin-webpack-analyzer
esbuild-plugin-webpack-analyzer

Разбиение кода на чанки через асинхронные импорты

Этот функционал в esbuild есть только в зачаточном состоянии с esm модулями и багами https://esbuild.github.io/api/#splitting , но использовать этот режим у меня не получилось - после разбора нескольких ошибок в браузере от esm-модулей я сдался (в основном ругалось на сторонние библиотеки). Будем ждать, когда этот функционал достигнет удобства Webpack и @loadable/component.

Интеграция с файлогенератором

В Webpack мне пришлось не один месяц возиться, чтобы стабильно встроить файлогенератор. У Webpack есть либо режим "холодной" сборки, либо watch-режим. В первом случае скорость билда низкая, а во втором нет возможности отложить перебилд до тех пор, пока файлогенератор не обновит файлы (есть только статичный aggregation timeout, который не подходит для этой цели). Пришлось внедряться в его файловую систему с помощью conditional-aggregate-webpack-plugin и подавать сигнал "аггрегировать измененные файлы и продолжить сборку". Для создания стабильной схемы пришлось пройти семь кругов ада.

Однако esbuild имеет еще один режим - "горячая" сборка через метод rebuild(). Скорость такой пересборки сравнима с режимом watch, поэтому интеграция файлогенератора стала тривиальной задачей.

import { generateFiles } from 'dk-file-generator';

const buildContext = await esbuild.context(config);

buildContext.rebuild(); // "холодная" сборка 0.8s

generateFiles({
  configs: generatorConfigs,
  watch: {
    paths: [paths.source],
    aggregationTimeout: 600,
    onFinish: () => buildContext.rebuild() // "горячая" сборка 0.3s
      .then(() => reloadBrowser()), // сигнал браузеру обновить страницу
  },
})

За этот механизм мой низкий поклон команде esbuild. Теперь можно не опираться на watch-механизм бандлера и интегрироваться в него, а пересобирать, когда это нужно, исходя из внешнего механизма слежения за файлами.

Сборка js-сервера

Конфиг для сборки сервера мало отличается от конфига для фронтенда. Достаточно добавить

config.packages = 'external';
config.target = 'node18';
config.platform = 'node';

и сделать соответствующие правки для обработки CSS Modules.

Скорость сборки

Хотя я считал, что связка Webpack + SWC очень эффективна в плане скорости сборки, все познается в сравнении. Вот усредненные результаты после прогона одного и того же проекта. Dev - версия для разработки, без полифиллов, минификации, сжатия в gzip+brotli. Prod - соответственно со всеми оптимизациями, если не помечено в скобках.

Webpack + SWC dev 3.8s
Webpack + SWC dev (watch rebuild) 0.2s
Webpack + SWC prod 7.8s

Esbuild + SWC dev 1s
Esbuild + SWC dev (watch rebuild) 0.35s
Esbuild + SWC prod 2.6s

Esbuild dev 0.8s
Esbuild dev (hot rebuild) 0.3s
Esbuild prod (no polyfills) 2.3s

По размеру выходные файлы во всех режимах примерно одного размера, за исключением режима Esbuild prod (no polyfills) - он, разумеется, меньше.

Выводы

Наконец, в этом году мне удалось на 95% воспроизвести на esbuild все, что нужно от бандлера в моих проектах. Я не затронул в статье только тему SSR и сжатия изображений в webp, но с этим не должно быть особых проблем. Также я не использую css-in-js и микрофронтенды, поэтому эти темы тоже за рамками статьи.

Мне крайне понравилась лаконичность конфига (в итоге он в разы меньше, чем в Webpack), множество встроенных инструментов, за счет чего количество внешних зависимостей тоже соратилось в несколько раз (в основном за счет отсутствия необходимости устанавливать loaders). API для написания плагинов и для запуска сборки превосходный. Скорость сборки в среднем ускорилась в 3 раза. Казалось бы, бочка меда - но не обошлось и без ложки дегтя.

Существующие плагины, ссылки на которые можно найти здесь https://github.com/esbuild/community-plugins , часто несовместимы друг с другом, непроизводительны или работают некорректно. Еще и отсутствие стабильного механизма асинхронных импортов с разбиением на чанки и конвертации кода под старые браузеры.

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

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

config.ts
import path from 'path';
import fs from 'fs';

import { postcssModules, sassPlugin } from 'esbuild-sass-plugin';
import { htmlPlugin } from '@craftamap/esbuild-plugin-html';
import { compress } from 'esbuild-plugin-compress';
import { BuildOptions } from 'esbuild';
import browserslist from 'browserslist';
import { resolveToEsbuildTarget } from 'esbuild-plugin-browserslist';
import { pluginReplace } from 'dk-esbuild-plugin-replace';
import { pluginInjectPreload } from 'esbuild-plugin-inject-preload';
import { pluginWebpackAnalyzer } from 'esbuild-plugin-webpack-analyzer';
import { pluginSwc } from 'esbuild-plugin-swc2';

import { excludeFalsy } from '../src/utils/tsUtils/excludeFalsy';
import { env } from '../env';
import { paths } from '../paths';

const list = JSON.parse(fs.readFileSync(paths.package, 'utf-8')).browserslist;
const template = fs.readFileSync(path.resolve('./src/templates/templateEs.html'), 'utf-8');

export const configClient: BuildOptions = {
  entryPoints: ['src/client.tsx'],
  bundle: true,
  logLevel: 'warning',
  format: 'iife',
  publicPath: '/',
  // assetNames: '[ext]/[name]-[hash]', // not working with compress plugin
  assetNames: env.FILENAME_HASH ? '[name]-[hash]' : '[name]',
  outdir: paths.build,
  write: false,
  metafile: true,
  minify: env.MINIMIZE_CLIENT,
  treeShaking: true,
  sourcemap: 'linked',
  banner: {
    js: `/* @env ${env.NODE_ENV} @commit ${env.GIT_COMMIT} */`,
    css: `/* @env ${env.NODE_ENV} @commit ${env.GIT_COMMIT} */`,
  },
  legalComments: 'external',
  platform: 'browser',
  // https://github.com/nihalgonsalves/esbuild-plugin-browserslist
  target: resolveToEsbuildTarget(browserslist(), { printUnknownTargets: false }),
  define: {
    IS_CLIENT: JSON.stringify(true),
    process: JSON.stringify({
      env: {
        NODE_ENV: env.NODE_ENV,
        GIT_COMMIT: env.GIT_COMMIT,
      },
    }),
    'process.env.NODE_ENV': JSON.stringify(env.NODE_ENV)
  },
  resolveExtensions: ['.js', '.ts', '.tsx'],
  loader: {
    '.svg': 'text',
    '.png': 'file',
    '.woff': 'file',
    '.ttf': 'file',
  },
  plugins: [
    pluginReplace({ filter: /\.(tsx?)$/, rootDir: paths.root }),

    env.SWC_ENABLED &&
      pluginSwc({
        jsc: {
          parser: { tsx: true, syntax: 'typescript' },
          transform: {
            react: { runtime: 'automatic', useBuiltins: false },
          },
        },
        env: env.POLYFILLING ? { mode: 'usage', targets: list } : undefined,
      }),

    // https://github.com/glromeo/esbuild-sass-plugin
    sassPlugin({ filter: /(global)\.scss$/, type: 'css', loadPaths: ['./src/styles'] }),
    sassPlugin({
      filter: /\.scss$/i,
      type: 'css',
      loadPaths: ['./src/styles'],

      // https://github.com/madyankin/postcss-modules
      transform: postcssModules({ generateScopedName: '[path][local]' }),
    }),

    // https://github.com/craftamap/esbuild-plugin-html
    htmlPlugin({
      files: [
        {
          entryPoints: ['src/client.tsx'],
          filename: 'template.html',
          scriptLoading: 'defer',
          define: { env: env.NODE_ENV, commitHash: env.GIT_COMMIT },
          htmlTemplate: template,
        },
      ],
    }),
    pluginInjectPreload({
      ext: '.woff',
      linkType: 'font',
      templatePath: path.resolve(paths.build, 'template.html'),
      replaceString: '<!-- FONT_PRELOAD -->',
    }),

    // https://github.com/LinbuduLab/esbuild-plugins/tree/main/packages/esbuild-plugin-compress
    env.GENERATE_COMPRESSED &&
      compress({
        gzip: true,
        gzipOptions: { level: 9 },
        brotli: true,
        emitOrigin: true,
        // https://github.com/micromatch/micromatch
        exclude: ['!(**/*.@(js|css))'],
      }),

    env.BUNDLE_ANALYZER &&
      pluginWebpackAnalyzer({
        port: env.BUNDLE_ANALYZER_PORT,
        open: false,
      }),
  ].filter(excludeFalsy),
};

Update 13.11.23

Разделение на чанки все-таки удалось запустить. Возможно, чего-то не хватало в конфигурации, которую тестировал раньше.

config.splitting = true;
config.format = 'esm';

htmlPlugin({ files: [{ scriptLoading: 'module', ...rest }] };

В итоге используется ESM схема для импортов и, насколько я смог протестировать в текущем проекте, все работает корректно. Также доработал esbuild-plugin-webpack-analyzer, чтобы он корректно отображал выходные чанки и их содержимое.

esbuild-plugin-webpack-analyzer + code splitting
esbuild-plugin-webpack-analyzer + code splitting

К сожалению, вывод parsed+gzip размеров в рамках этого плагина реализовать не получилось - Webpack Bundle Analyzer использует очень хитрую схему для сбора строк из итогового бандла и заточен чисто под то, как делает вывод Webpack. Без форка самой библиотеки и, возможно, изменений в esbuild, сделать полную интеграцию, видимо, не получится.

Однако оригинальные выводы статьи остаются теми же, пока документация не признает этот функционал стабильным и нет схемы без ESM, для клиентских сайтов я бы не стал пока использовать этот бандлер.

Комментарии (23)


  1. mikegordan
    11.11.2023 13:53
    +1

    внутри vite используется esbuild ?


    1. gmtd
      11.11.2023 13:53

      Только для dev режима


      1. mikegordan
        11.11.2023 13:53

        а для прода?


    1. DmitryKazakov8 Автор
      11.11.2023 13:53
      +2

      Я не сторонник подхода, когда для разработки используется одна экосистема, а для production-билда - другая, и легко можно поймать на проде ошибки, которых локально не было. В Vite это именно так, поэтому в коммерческих проектах его не использую. Также перфоманс production сборки через Rollup (его использует Vite) хуже, чем моя текущая связка Webpack+SWC. Конкретные цифры не приведу, но в районе 20%, как и при чистом Rollup. А в дев-режиме ускорение с 0.2с (Webpack+SWC) до 0.1с (Vite) совершенно недостаточно, чтобы сподвигнуть на переход.

      К Vite я тоже делал 3 подхода, но каждый раз натыкался на недостаточный функционал и низкий перфоманс, если не использовать ESM-dev-схему, которая мне не подходит, как написал выше. Поэтому тщательно изучать Vite после перехода на esbuild не вижу пока необходимости - скорее более интересно будущее Turbopack.


      1. gmtd
        11.11.2023 13:53

        Вроде Эван заявил на конференции недавно, что все силы сейчас на Vite брошены - переписывают частично Rollup на Rust, будет один сборщик на оба режима


        1. DmitryKazakov8 Автор
          11.11.2023 13:53
          -1

          Лучше бы esbuild и для продакшен-сборки использовали и добавили в него нормальное разделение на чанки, чем новый сборщик по факту делать...


          1. gmtd
            11.11.2023 13:53
            +2

            And during build time Vite currently uses Rollup where bundling size and having access to a wide ecosystem of plugins are more important than raw speed.

            Vite документация


          1. jodaka
            11.11.2023 13:53
            +1

            Конкуренция — это хорошо, и если rollup (который умеет нормально разбивать на чанки) смогут в несколько раз ускорить, то это можно будет тащить в продакшн, а не ждать несколько лет (я про неработающее разбиение на чанки в esbuild).


  1. Kenya-West
    11.11.2023 13:53
    +3

    Angular 17 использует ESBuild под капотом, на чанки всё спокойно распиливается. Видимо, они как-то всё-таки его осилили.

    Бонусом, само собой, выросла скоро пересборки и, возможно, не в последнюю очередь благодаря этому бандл на Angular теперь производительнее бандла на React!


  1. fransua
    11.11.2023 13:53
    +1

    Тоже недавно перешел на esbuild, но с rollup. Скорость сборки впечатляет. Большое спасибо за плагины, буду использовать.
    Чанки собирался сделать через external и importmaps, должно сработать


    1. DmitryKazakov8 Автор
      11.11.2023 13:53

      Вручную такой механизм, не залезая во внутрянку esbuild, будет сложно сделать. Я тоже думал о такой схеме:

      • до билда смотрим все файлы на предмет асинхронных импортов и собираем новый entry points массив из них

      • продолжаем сборку со всеми этими entry points

      • смотрим в метафайле на общие зависимости всех entry points и выносим их в отдельный entry point и прописываем в externals весь этот список

      • пересобираем

      Но как видно из схемы, общие зависимости придется грузить сразу вместе с базовым выходным файлом. Как ни кручу в голове разные сценарии, пока сам esbuild не сможет создавать чанки с соответствующими ссылками на используемые и пересекающиеся зависимости, этот функционал корректно не реализовать. Возможно получится сделать схему из 2-3 последовательных ребилдов, но это не то. Если получится - было бы интересно узнать, как


      1. fransua
        11.11.2023 13:53

        Я думал по регулярке выделять чанки вроде src/pages/* или src/async/* и собирать их отдельно. Импорт остается тот же самый.
        Если хочется на автомате, то это сложнее задача, так сходу и не придумать.


        1. DmitryKazakov8 Автор
          11.11.2023 13:53

          А куда их выделять, в entry массив? Если так - то если в каждом чанке импортится скажем MUI или lodash, то они будут включаться в каждый чанк, и в итоге мы не сэкономим трафик пользователя, а значительно увеличим его, получив еще и проблемы коллизий.


          1. fransua
            11.11.2023 13:53

            Да, понял проблему. Вручную это сложно будет сделать.
            Там ведь еще непонятно, что называть общими зависимостями - те которые для всех чанков нужны или хотя бы для двух. В первом случае грузим слишком много, во втором оверхед из-за большого числа чанков.
            Как вариант вынести большие зависимости вроде mui или реакта в externals по-дефолту. А на остальные общие зависимости закрыть глаза


            1. DmitryKazakov8 Автор
              11.11.2023 13:53

              Полумеры для production не подойдут) Либо делать качественную обработку, либо пожертвовать чанками. Никто не будет следить вручную за всеми зависимостями, которые используют чанки - раз сделаешь импорт lodash - прибавится 500кб в несжатом виде, условно. Поэтому и написал в статье, что для тех проектов, в которых нужно минимальное количество js на старте, esbuild не подходит


  1. LabEG
    11.11.2023 13:53

    У esbuild есть существенный недостаток, он не поддерживает все возможности typescript. Как альтернативу рекомендую использовать связку rollup + swc + post-css. Скорость даже чуть лучше, и полная поддержка typescript.


    1. DmitryKazakov8 Автор
      11.11.2023 13:53

      Rollup+swc это аналог webpack+swc. Postcss как я указал в статье уже используется, то есть с css проблем не будет. Скорость однозначно лучше у esbuild, а на счет поддержки ts - можно же использовать esbuild+swc, тогда все, что поддерживает swc, можно использовать.


    1. artemmalko
      11.11.2023 13:53

      Подскажите, а какие возможности typescript не поддерживает esbuild?


  1. Finesse
    11.11.2023 13:53

    Сжатие в gzip / brotli

    Извините за оффтоп. Как сжатие работает в контексте сборки фронта (Webpack, Esbuild и т.п.)? Насколько я знаю, файлы хранятся не сжатые, и сервер сжимает их непосредственно при отправки браузеру. Серверы умеют работать с заранее сжатыми файлами?


    1. iliazeus
      11.11.2023 13:53
      +2

      Да, есть серверы, которые могут работать с заранее сжатыми файлами. Например, nginx.


    1. kubk
      11.11.2023 13:53
      +4

      Например Nginx умеет как генерировать gzip налету, так и отдавать уже сжатые файлы, что предотвращает лишнюю работу по сжатию. Для этого нужно придерживаться конвенции index.js -> index.js.gz и использовать директиву gzip_static on внутри конфига Nginx: https://nginx.org/en/docs/http/ngx_http_gzip_static_module.html

      На серверах в версии Nginx для Ubuntu этот модуль обычно уже включён.


      1. Finesse
        11.11.2023 13:53

        Спасибо, не знал


    1. DmitryKazakov8 Автор
      11.11.2023 13:53

      В дополнение к предыдущим комментаторам, всегда можно добавить соответствующие редиректы вручную. Для node js например таким кодом:

          if (req.url.endsWith('.js') || req.url.endsWith('.css')) {
            const acceptedCompression = getAcceptedCompression(req);
      
            if (acceptedCompression) {
              req.url = `${req.url}.${acceptedCompression.extension}`;
            }
          }

      Полный пример - тут.