Webpack or Esbuild: Why Not Both? By John Reilly at LogRocket You can build faster using tools like EsBuild. However, if you are investing in WebPack and still want to take advantage of a faster build, there is a way.

In this tutorial, we will show you how to use esbuild and Webpack with esBuild-Loader.

The world of Web development is evolving

With apologies to those suffering from JavaScript fatigue, the world of Web development is moving on again. It has long been common practice to run JavaScript and TypeScript through some node.js-based build tool such as Webpack or rollup.js. These tools are written in the same language they are compiled into — namely JavaScript or TypeScript.

New to the blog are tools like Esbuild, Vite, and SWC. The notable difference between them and their predecessors is that the new tools are written in languages like Go and Rust. Go and Rust perform much better than JavaScript. This translates into significantly faster builds.

These new tools are transformative and may represent the future of Web building tools. In the long run, tools like EsBuild, Vite, and Friends will likely replace the current standard build tools — Webpacks, Rollups, etc.

However, this is for the long term. There are many projects that have invested heavily in their current build tools — mainly WebPack. Migrating to a new build tool is not easy. New projects may start with Vite, but existing projects are unlikely to be ported. Webpack is so popular for a reason; It does a lot of things really well. It is battle-tested on large projects, very mature, and can handle a wide range of use cases.

So if your team wants to build faster but doesn’t have time for mass migration, is there anything you can do? Yes, there is a middle ground to explore.

There is a relatively new project called esbuild-Loader. Esbuild-loader, developed by Hiroki Osame, is a WebPack loader built on esBuild. It allows users to swap ts-loaders or Babel-loaders through it, which greatly improves build speed.

To declare an interest here for full disclosure, I am the main maintainer of TS-Loader, a popular TypeScript loader commonly used with WebPack. However, I strongly believe that what is important here is developer productivity. As Node.js-based projects, TS-Loader and Babel-Loader will never be able to compete in the same way as Esbuild-Loader. As a language, Go really, er, works!

While esbuild may not work for all use cases, it works for most tasks. Esbuild-loader therefore represents a middle ground — and an early way to get the higher build speeds esBuild provides without saying goodbye to WebPack. This walkthrough will explore using esbuild-Loader in your Webpack setup.

Migrate existing projects to esbuild

Using either babel-loader or TS-Loader allows you to migrate a project to esbuild-Loader very directly. First, install dependencies:

npm i -D esbuild-loader
Copy the code

If you are using babel-loader, modify your webpack.config.js as follows:

module.exports = {
    module: {
    rules: [
-       {
-         test: /\.js$/,
-         use: 'babel-loader',
-       },
+       {
+         test: /\.js$/,
+         loader: 'esbuild-loader',
+         options: {
+           loader: 'jsx',  // Remove this if you're not using JSX
+           target: 'es2015'  // Syntax to compile to (see options below for possible values)
+         }
+       },
Copy the code

Create the baseline application

Let’s see how the Esbuild-Loader works in an exercise. We are creating a new React App with the Create React App:

npx create-react-app my-app --template typescript
Copy the code

This will build a new React application using TypeScript in the my-app directory. It is worth mentioning that the Create React App uses the Babel-Loader behind the scenes.

CRA also uses the Fork TS Checker Webpack plug-in to provide TypeScript type checking. This is useful because esBuild only does translation and is not designed to provide type checking support. So luckily we still have that plugin. Otherwise, we lose type checking.

Now that you understand the advantages of migrating esbuild, we first need a baseline to understand how babel-Loader will behave. We run time NPM run build to perform a build of our simple app.

Our full build, TypeScript type checking, translation, minification, and so on all took 22.08 seconds. Now the question is, what happens if we put esbuild into the composite?

Introduce esbuild – loader

One way to customize the Create React App build is to run NPM Run eject and then customize the CRA output. That’s fine, but it means you can’t keep up with CRA. Another approach is to use a tool such as Create React App Configuration Override (CRACO), which allows you to adjust the Configuration in place. CRACO describes itself as “a straightforward configuration layer for create-React-app.”

Let’s add esbuild-Loader and CRACO dependencies

npm install @craco/craco esbuild-loader --save-dev
Copy the code

Then we’ll swap our various scripts in our package.json to use CRACO

"start": "craco start",
"build": "craco build",
"test": "craco test",
Copy the code

Our application uses CRACO, but we haven’t configured it yet. So we are adding a craco.config.js file to the root directory of our project. Here we swap out babel-loader for esbuild-loader:

`const { addAfterLoader, removeLoaders, loaderByName, getLoaders, throwUnexpectedConfigError } = require('@craco/craco'); const { ESBuildMinifyPlugin } = require('esbuild-loader'); const throwError = (message) => throwUnexpectedConfigError({ packageName: 'craco', githubRepo: 'gsoft-inc/craco', message, githubIssueQuery: 'webpack', }); module.exports = { webpack: { configure: (webpackConfig, { paths }) => { const { hasFoundAny, matches } = getLoaders(webpackConfig, loaderByName('babel-loader')); if (! hasFoundAny) throwError('failed to find babel-loader'); console.log('removing babel-loader'); const { hasRemovedAny, removedCount } = removeLoaders(webpackConfig, loaderByName('babel-loader')); if (! hasRemovedAny) throwError('no babel-loader to remove'); if (removedCount ! == 2) throwError('had expected to remove 2 babel loader instances'); console.log('adding esbuild-loader'); const tsLoader = { test: /\.(js|mjs|jsx|ts|tsx)$/, include: paths.appSrc, loader: require.resolve('esbuild-loader'), options: { loader: 'tsx', target: 'es2015' }, }; const { isAdded: tsLoaderIsAdded } = addAfterLoader(webpackConfig, loaderByName('url-loader'), tsLoader); if (! tsLoaderIsAdded) throwError('failed to add esbuild-loader'); console.log('added esbuild-loader'); console.log('adding non-application JS babel-loader back'); const { isAdded: babelLoaderIsAdded } = addAfterLoader( webpackConfig, loaderByName('esbuild-loader'), matches[1].loader // babel-loader ); if (! babelLoaderIsAdded) throwError('failed to add back babel-loader for non-application JS'); console.log('added non-application JS babel-loader back'); console.log('replacing TerserPlugin with ESBuildMinifyPlugin'); webpackConfig.optimization.minimizer = [ new ESBuildMinifyPlugin({ target: 'es2015' }) ]; return webpackConfig; ,}}};Copy the code

So what’s going on here? The script looks for the use of babel-loader in the default Create React App configuration. There will be two types: one for TypeScript/JavaScript application code (which we want to replace) and one for non-application JavaScript code. It’s not clear what non-application JavaScript code exists or might exist, so we’ll leave it in; That could be important. The code we really care about is the application code.

You cannot remove individual loaders using CRACO, so we will remove both and re-add non-application JavaScript babel-Loader. We will also add the esbuild-loader setting with {loader: ‘TSX ‘, target: ‘es2015’} option to ensure we can handle JSX/TSX.

Finally, we will also miniaturize JavaScript using Terser instead of esbuild.

Huge performance boost

Our migration is complete. Next time we build, we’ll run the Create React App with esbuild-Loader without popping up. Once again, we’ll run time NPM run Build to perform our simple application build and determine how long it will take:

Our full build, TypeScript type checking, translation, minification, and so on all took 13.85 seconds. By migrating to esbuild-Loader, we reduced our overall compile time by about a third. This is a huge step forward!

As the code base expands and the application grows, compilation times can explode. With esbuild-Loader, you should get an ongoing benefit from build time.