Poprzednia dekada przyniosła programistom rozkwit narządzi typu "bundler".  W okolicach roku 2010 można było jeszcze spotkać, że pliki strony czy appek webowych były przygotowywane ręcznie przez programistów na produkcje.  W roku 2021 żaden chyba profesjonalny developer nie robi manualnie takich procesów jak minimalizacja js, css, konwertowanie js w es6 na es5 czy innych transformacji związanych w tworzeniu tak zwanego "bundle" produkcyjnego.  

Narzędzia takie jak grunt, gulp przyczyniły się do rozwoju obecnie chyba najbardziej wykorzystywanego "bundlera" czyli webpack'a. Webpack i familia  to naprawdę potężna i dobrze działająca machina. Na pewno dobry wybór nawet i do corporate level app, z dobrą dokumentacją oraz bardzo dużym ekosystemem.

Webpack jako "stary wół" jest jednak czasem dość powolny, następne iteracje webpacka wydają się nie wpływać na jego performance, a wręcz przeciwnie jego performance spada tak  jak to sugeruje  strona główna esbuild.

Enter the esbuild

https://esbuild.github.io/

Natrafiłem na esbuild'a już jakiś czas temu, nowej generacji "bundler" o "niesamowitych" osiągach.  Jako że w tamtym czasie był dopiero na początku swojej drogi, nie poświeciłem mu zbytnio dużo czasu.  Pracując ostatnio nad optymalizacją buildu (webpack), w konfiguracji loadera plików ts podmieniłem ts-loader na esbuild-loader, 40%-50% szybszy build time okazał się dużym zaskoczeniem.

Zachęcony wynikami, postanowiłem sprawdzić czy da rade porzucić webpacka na esbuild i jak rożni się praca pomiędzy młodym zawodnikiem (esbuild) a wysłużonym weteranem (webpack v5).

Na cel objąłem full stackowy setup node.js (express) + react.

Pierwsze kroki przygotowania bundle poszły dość bezproblemowo, dość ogarnięta dokumentacja esbuilda pomaga. Esbuild integruje się z TS bez zbędnych pluginów, works out of the box wraz z pobieraniem aliasów z tsconfig.json (np @components)

const { build } = require('esbuild');
const fs = require('fs-extra');
const postCss = require('esbuild-plugin-postcss2').default;
const autoprefixer = require('autoprefixer');
const tailwindcss = require('tailwindcss');

const generateBuild = async () => {
  await fs.rmdirSync('./build/public/static', { recursive: true });

  await build({
    // Bundles JavaScript.
    bundle: true,
    sourcemap: true,
    loader: { '.svg': 'dataurl', '.png': 'dataurl' },
    // Defines env variables for bundled JavaScript; here `process.env.NODE_ENV`
    // is propagated with a fallback.
    define: {
      'process.env.NODE_ENV': JSON.stringify(
        process.env.NODE_ENV || 'development',
      ),
    },
    // Bundles JavaScript from (see `outfile`).
    entryPoints: ['./src/client/index.tsx'],
    // Uses incremental compilation (see `chokidar.on`).
    incremental: true,
    // Removes whitespace, etc. depending on `NODE_ENV=...`.
    minify: true,
    // Bundles JavaScript to (see `entryPoints`).
    outdir: './build/static/',
    plugins: [
      postCss({
        plugins: [autoprefixer, tailwindcss],
      }),
    ],
  }).catch(() => process.exit(1));

  process.exit(0);
};

generateBuild();
esbuild posiada wbudowane opcje env, możliwość podłączenia "community plugins", udało także się podłączyć postCSS i tailwindCSS wraz z czyszczeniem stylów kaskadowych (purge css). 
(async () => {
  // `esbuild` bundler for JavaScript / TypeScript.
  await build({
    // Bundles JavaScript.
    bundle: true,
    sourcemap: false,
    // set platform for node, default is browser
    platform: 'node',
    loader: { '.svg': 'dataurl', '.png': 'dataurl' },
    // Defines env variables for bundled JavaScript; here `process.env.NODE_ENV`
    // is propagated with a fallback.
    define: {
      'process.env.NODE_ENV': JSON.stringify(
        process.env.NODE_ENV || 'Production',
      ),
    },
    // Bundles JavaScript from (see `outfile`).
    entryPoints: ['./src/server/index.ts'],
    // Uses incremental compilation (see `chokidar.on`).
    incremental: true,
    // Removes whitespace, etc. depending on `NODE_ENV=...`.
    minify: true,
    // Bundles JavaScript to (see `entryPoints`).
    outdir: './build/',
  }).catch(() => process.exit(1));

  process.exit(0);
})();
esbuild build function potrzebuje tylko flagę z platformą node aby, zkompliować podstawowy serwer express.js, napisany w typsecriptcie. Tutaj także trzeba przyznać, że konfiguracja przeszła szybko i bezproblemowo.

Webpack-dev-server ?

Ale praca "na projekcie" to nie sam build, o ile nodemon w sprawie serwera daje sobie  spokojnie radę, to pytanie co z HMR, Live relead dla Reacta ? Esbuild może jest i szybki,  ale żaden z devów nie chce klikać refresh co chwilę, dodatkowo gubiąc stan komponentów.

Niestety brakuje esbuildowi rozsądnego rozwiązania czy pluginu z community w temacie live developmentu, na pewno brakuje tutaj jakieś wskazówki, "oficjalnej" konkretnej ścieżki jak można by było osiągnąć hot refreash, ja w końcu zdecydowałem się na bibliotekę "servor", rozgrzewa się na pewno trochę dłużej niż webpack-dev-server ale zdaje egzamin w  sprawie live reloadu.

lukejacksonn/servor
Dependency free file server for single page app development - lukejacksonn/servor
r

esbuild jest na razie bardzo eksperymentalnym projektem, z bardzo ograniczonym ekosystemem pluginów.  Brakuje mu dużo stabilności i dojrzałości, jest jednak nadzieja na nowej generacji bundler, który jeszcze bardziej przyczyni się do poprawy wydajności pracy programisty w przyszłości.

Do tego czasu, na razie proponuje jednak spróbować esbuild-loader w  waszych ustawieniach webpacka.

Przykładowe repo z ustawieniem esbuild only

MassivDash/typescript-react-express-esbuild
Express.js + React + Typescript + Esbuild + tailwindCss - MassivDash/typescript-react-express-esbuild