Babel to EsBuild migration for Webpack

Development9 feb 2025

Recently I had some time to take a look at the performance of my pet project. One thing bothered me: the project is relatively small, but the bundle size was too big: 780 KiB. Whereas the project had only a few dependencies: react (~10 KiB), react-router (~70 KiB), react-dom (~90 KiB), dayjs (~3 KiB), lodash (~70 KiB). The bundle should not be that big for so few dependencies.

So I ran webpack-dev-analyzer and found a weird thing: the sum of the libs is 3 times smaller than the final bundle size. Then I decided to take a look at @statoscope/webpack-plugin. It was a good tool but it gave me the same result. The bundle has way too much trash.

Ok. Probably it’s just webpack wrappers over 300+ small modules. So I asked Perplexity - what can I do with it? Turned out all the proper options were already activated. So, I made a decision - it’s time to try esbuild. It made a huge impact on my work project, so I knew it could work out.

I knew that esbuild didn’t support a lot of stuff that babel supports. So I expected that it would take a day to migrate the codebase to esbuild. But surprisingly once I replaced babel-loader with esbuild-loader the build command worked out. The build didn’t work but the bundle was assembled without any errors.

Did it help? Nope. Instead of 780 KiB, I got ~1 MiB. Luckily I found out that there’s also EsbuildPlugin, which replaces TerserPlugin and works better. Added this line, it worked like a charm, and… Whoa! 280 KiB. That’s huuuge.

So I figured out - it worked. Now I have to find a way to fix the app (spoiler: it didn’t work at all).

Implicit React imports

React 17+ supports the possibility of avoiding adding import React from ‘react’ to every TSX-file. esbuild supports it too, but somehow it didn’t work out of the box. Solution:

  const esbuildLoader: RuleSetRule = {
    test: /\.(js|ts|tsx)$/,
    loader: 'esbuild-loader',
    exclude: /node_modules/,
    options: {
       target: 'es2020',
       minify: cfg.PROD,
+      // Support implicit React imports:
+      jsx: 'automatic',
+      jsxFactory: 'React.createElement',
+      jsxImportSource: 'react',
    } satisfies LoaderOptions,
  };

TSX-Control-Statements

I just removed the packages and all their <If/>s. I liked them though. There’s a plugin for Vite that adds their support. But I still use Webpack and the plugin works via string replacements, not via AST.

SourceMaps

To enable sourcemaps I had to disable EsbuildPlugin for development. Theoretically, it supports sourcemaps, but no matter what I did it didn’t work. I even debugged the package itself. Didn’t help. So I surrendered and enabled it only for the prod build.

TypeScript

EsBuild supports TypeScript syntax out of the box but it doesn’t check types. So it builds extremely fast but I need to know my errors. Solution:

  webpackConfig.plugins!.push(
    new ForkTsCheckerWebpackPlugin({
      typescript: {
        configFile: cfg.paths.TS_CONFIG,
      },
    })
  );

How does it work? The build and the type-checking processes are run in parallel. The 2nd doesn’t linger the 1st. You still can see the errors in the browser (and CLI) console. And visually too. Nice.

ES-Lint

For some reason, it stopped working. The solution was pretty simple:

  webpackConfig.plugins!.push(
    new ESLintPlugin({
      extensions: ['ts', 'tsx'],

Final results

Afterthought

Taking a deeper look I found: