esbuild-loader как альтернатива babel-loader

esbuild-loader — загрузчик для Webpack, использующий компилятор esbuild вместо Babel для транспиляции JavaScript и TypeScript-кода. Основная причина его популярности — чрезвычайно высокая скорость сборки.

esbuild написан на языке Go и выполняет компиляцию значительно быстрее традиционной связки babel-loader + @babel/core. На крупных проектах разница во времени сборки может измеряться не процентами, а десятками секунд.

Типичная схема работы:

Исходный код
    ↓
esbuild-loader
    ↓
Webpack
    ↓
Готовый bundle

Вместо:

Исходный код
    ↓
babel-loader
    ↓
Webpack
    ↓
Готовый bundle

Установка

Минимальный набор зависимостей:

npm install -D esbuild esbuild-loader

или:

yarn add -D esbuild esbuild-loader

Базовая настройка

Простейшая конфигурация Webpack:

const path = require('path');

module.exports = {
    mode: 'development',

    entry: './src/index.js',

    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist')
    },

    module: {
        rules: [
            {
                test: /\.js$/,
                loader: 'esbuild-loader',
                options: {
                    target: 'es2017'
                }
            }
        ]
    }
};

Параметр target

Опция target определяет, до какого стандарта ECMAScript необходимо преобразовывать код.

Примеры:

options: {
    target: 'es2015'
}
options: {
    target: 'es2020'
}
options: {
    target: 'chrome80'
}

Поддерживаются:

  • версии ECMAScript;
  • версии браузеров;
  • версии Node.js.

Примеры:

target: 'node18'
target: 'firefox100'

Транспиляция TypeScript

esbuild-loader умеет компилировать TypeScript без использования ts-loader.

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

module.exports = {
    module: {
        rules: [
            {
                test: /\.ts$/,
                loader: 'esbuild-loader',
                options: {
                    loader: 'ts',
                    target: 'es2018'
                }
            }
        ]
    },

    resolve: {
        extensions: ['.ts', '.js']
    }
};

Поддержка TSX и React

Для React-проектов:

{
    test: /\.tsx?$/,
    loader: 'esbuild-loader',
    options: {
        loader: 'tsx',
        target: 'es2017'
    }
}

JSX-трансформация

esbuild-loader поддерживает JSX без Babel.

Пример:

{
    test: /\.jsx$/,
    loader: 'esbuild-loader',
    options: {
        loader: 'jsx',
        target: 'es2017'
    }
}

Использование с React 17+

Для нового JSX Runtime:

options: {
    loader: 'tsx',
    target: 'es2017',
    jsx: 'automatic'
}

Для старого режима:

options: {
    jsx: 'transform'
}

Сравнение скорости с babel-loader

Типичный pipeline Babel:

babel-loader
    ↓
@babel/core
    ↓
plugins
    ↓
presets
    ↓
AST-трансформации

Pipeline esbuild-loader:

esbuild-loader
    ↓
esbuild

Архитектура esbuild значительно проще и агрессивно оптимизирована по производительности.

На практике:

Инструмент Средняя скорость
babel-loader Медленно
ts-loader Средне
esbuild-loader Очень быстро

Особенно заметна разница:

  • в монорепозиториях;
  • в больших React-проектах;
  • при HMR;
  • в CI/CD;
  • при production-сборке.

Замена Babel на esbuild-loader

Типичная Babel-конфигурация:

{
    test: /\.js$/,
    exclude: /node_modules/,
    use: {
        loader: 'babel-loader',
        options: {
            presets: [
                '@babel/preset-env'
            ]
        }
    }
}

Эквивалент через esbuild-loader:

{
    test: /\.js$/,
    exclude: /node_modules/,
    loader: 'esbuild-loader',
    options: {
        target: 'es2017'
    }
}

Конфигурация становится значительно компактнее.


Использование вместе с Babel

Полный отказ от Babel требуется не всегда. Часто используется комбинированный подход:

esbuild-loader
    ↓
babel-loader

Например:

{
    test: /\.js$/,
    use: [
        {
            loader: 'babel-loader',
            options: {
                plugins: [
                    '@babel/plugin-proposal-decorators'
                ]
            }
        },
        {
            loader: 'esbuild-loader',
            options: {
                target: 'es2017'
            }
        }
    ]
}

В таком случае:

  • esbuild-loader выполняет быструю базовую компиляцию;
  • Babel применяется только для специфических трансформаций.

Ограничения esbuild-loader

Несмотря на скорость, esbuild-loader не является полной заменой Babel во всех сценариях.


Отсутствие экосистемы Babel-плагинов

У Babel огромная экосистема:

  • experimental proposals;
  • decorators;
  • macros;
  • кастомные AST-трансформации;
  • сложные polyfill-сценарии.

esbuild поддерживает только ограниченный набор возможностей.


Ограниченная поддержка experimental-функций

Некоторые предложения ECMAScript поддерживаются в Babel раньше.

Например:

  • decorators;
  • pipeline operator;
  • частные proposals.

В таких случаях Babel остаётся необходимым.


Отсутствие полноценного polyfill-механизма

Babel умеет автоматически подключать polyfills:

useBuiltIns: 'usage'

esbuild этого не делает.

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

  • core-js;
  • manual polyfills;
  • отдельные entry-файлы.

TypeScript без type checking

esbuild-loader компилирует TypeScript, но не выполняет проверку типов.

То есть:

const value: string = 100;

Будет успешно скомпилирован.

Проверка типов выполняется отдельно:

tsc --noEmit

Либо через:

  • Fork TS Checker Webpack Plugin;
  • CI-проверки;
  • IDE.

Проверка типов через ForkTsCheckerWebpackPlugin

Пример настройки:

npm install -D fork-ts-checker-webpack-plugin

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

const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

module.exports = {
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                loader: 'esbuild-loader',
                options: {
                    loader: 'tsx',
                    target: 'es2018'
                }
            }
        ]
    },

    plugins: [
        new ForkTsCheckerWebpackPlugin()
    ]
};

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

Помимо loader существует plugin-версия:

const { EsbuildPlugin } = require('esbuild-loader');

Она используется для:

  • minification;
  • optimization;
  • transpilation.

Замена Terser на EsbuildPlugin

Стандартный production-webpack:

optimization: {
    minimize: true
}

Обычно используется:

TerserPlugin

Но esbuild умеет минифицировать код гораздо быстрее.


Настройка minimizer

const { EsbuildPlugin } = require('esbuild-loader');

module.exports = {
    optimization: {
        minimizer: [
            new EsbuildPlugin({
                target: 'es2017'
            })
        ]
    }
};

Минификация CSS

EsbuildPlugin поддерживает и CSS:

new EsbuildPlugin({
    css: true
})

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

Опция define позволяет заменять константы на этапе сборки.

Пример:

new EsbuildPlugin({
    define: {
        __DEV__: 'true'
    }
})

Код:

if (__DEV__) {
    console.log('development');
}

После сборки:

if (true) {
    console.log('development');
}

Работа с sourcemaps

esbuild-loader поддерживает source maps.

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

module.exports = {
    devtool: 'source-map',

    module: {
        rules: [
            {
                test: /\.js$/,
                loader: 'esbuild-loader'
            }
        ]
    }
};

Исключение node_modules

Часто используется:

exclude: /node_modules/

Полный пример:

{
    test: /\.[jt]sx?$/,
    exclude: /node_modules/,
    loader: 'esbuild-loader',
    options: {
        target: 'es2018'
    }
}

Обработка JavaScript и TypeScript одним правилом

Распространённая конфигурация:

{
    test: /\.[jt]sx?$/,
    loader: 'esbuild-loader',
    options: {
        loader: 'tsx',
        target: 'es2018'
    }
}

Поддерживаются:

  • .js
  • .jsx
  • .ts
  • .tsx

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

Production-конфигурация:

const { EsbuildPlugin } = require('esbuild-loader');

module.exports = {
    mode: 'production',

    module: {
        rules: [
            {
                test: /\.[jt]sx?$/,
                loader: 'esbuild-loader',
                options: {
                    target: 'es2018'
                }
            }
        ]
    },

    optimization: {
        minimizer: [
            new EsbuildPlugin({
                target: 'es2018'
            })
        ]
    }
};

Сравнение babel-loader и esbuild-loader

Возможность babel-loader esbuild-loader
Скорость Низкая Очень высокая
Plugins ecosystem Огромная Ограниченная
TypeScript Да Да
Type checking Нет Нет
JSX Да Да
Experimental proposals Отлично Ограниченно
Polyfills Отлично Ограниченно
Минификация Через Terser Встроенная
Конфигурация Более сложная Простая

Когда esbuild-loader особенно полезен

Наиболее эффективен:

  • в больших frontend-проектах;
  • при долгих production-сборках;
  • при медленном HMR;
  • в React/Vue/Svelte-проектах;
  • в TypeScript-монорепозиториях;
  • в CI/CD pipeline;
  • при активной разработке.

Когда Babel остаётся предпочтительным

Babel часто необходим при:

  • использовании experimental proposals;
  • сложной AST-трансформации;
  • кастомных Babel plugins;
  • старых браузерах;
  • сложной polyfill-логике;
  • специфических enterprise-конфигурациях.

Комбинация esbuild-loader и Webpack cache

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

module.exports = {
    cache: {
        type: 'filesystem'
    },

    module: {
        rules: [
            {
                test: /\.js$/,
                loader: 'esbuild-loader'
            }
        ]
    }
};

Использование вместе с thread-loader

Обычно esbuild-loader настолько быстрый, что thread-loader уже не нужен.

Но иногда используется:

{
    test: /\.js$/,
    use: [
        'thread-loader',
        'esbuild-loader'
    ]
}

На практике выигрыш часто минимален.


Интеграция с React Fast Refresh

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

{
    test: /\.[jt]sx?$/,
    loader: 'esbuild-loader',
    options: {
        loader: 'tsx',
        target: 'es2018'
    }
}

Дополнительно:

  • React Refresh Webpack Plugin;
  • React Fast Refresh runtime.

Использование define для NODE_ENV

new EsbuildPlugin({
    define: {
        'process.env.NODE_ENV': '"production"'
    }
})

Это позволяет:

  • удалять development-код;
  • активировать tree shaking;
  • уменьшать bundle.

Tree Shaking

esbuild-loader совместим с механизмами tree shaking Webpack.

Пример:

// utils.js
export function used() {}

export function unused() {}
import { used } from './utils';

Неиспользуемый код может быть удалён при production-сборке.


Использование с современными браузерами

Если поддерживаются только современные браузеры:

target: 'es2022'

или:

target: 'chrome110'

Тогда количество трансформаций уменьшается, а производительность возрастает ещё сильнее.


Поддержка ESM

esbuild-loader корректно работает с:

  • ES Modules;
  • dynamic import;
  • tree shaking;
  • code splitting.

Пример:

const module = await import('./feature.js');

Webpack продолжит выполнять split chunks и lazy loading.


Использование alias и resolve

esbuild-loader не заменяет стандартные механизмы Webpack:

resolve: {
    alias: {
        '@': path.resolve(__dirname, 'src')
    }
}

Он отвечает исключительно за компиляцию файлов.


Типичная современная конфигурация

const path = require('path');
const { EsbuildPlugin } = require('esbuild-loader');

module.exports = {
    mode: 'production',

    entry: './src/index.tsx',

    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, 'dist'),
        clean: true
    },

    resolve: {
        extensions: ['.tsx', '.ts', '.js']
    },

    module: {
        rules: [
            {
                test: /\.[jt]sx?$/,
                exclude: /node_modules/,
                loader: 'esbuild-loader',
                options: {
                    loader: 'tsx',
                    target: 'es2018'
                }
            }
        ]
    },

    optimization: {
        minimizer: [
            new EsbuildPlugin({
                target: 'es2018'
            })
        ]
    }
};