Builds can be made faster using tools like esbuild. However, if you’re invested in webpack but would still like to take advantage of speedier builds, there is a way.
In this tutorial, we’ll show you how to use esbuild alongside webpack with esbuild-loader.
With apologies to those suffering from JavaScript fatigue, the world of web development is evolving yet again. It’s long been common practice to run your JavaScript and TypeScript through some kind of Node.js-based build tool like webpack or rollup.js. These tools are written in the same language they compile to — namely, JavaScript or TypeScript.
The new kids on the blog are tools like esbuild, Vite, and swc. The significant difference between these and their predecessors is that the new-school tools are written in languages like Go and Rust. Go and Rust enjoy far better performance than JavaScript. This translates into significantly faster builds.
These new tools are transformative and likely represent the future of build tooling for the web. In the long term, the likes of esbuild, Vite, and friends may well come to displace the current standard build tools — the webpacks, rollups, and so on.
However, that’s the long term. There are a lot of projects out there that are already heavily invested in their current build tooling — mostly webpack. Migrating to a new build tool is no small task. New projects might start with Vite, but existing ones are less likely to be ported. There’s a reason webpack is so popular; it does a lot of things very well indeed. It’s battle-tested on large projects, it’s mature, and it handles a wide range of use cases.
So if your team wants to have faster builds but doesn’t have the time to go through a big migration, is there anything you can do? Yes, there’s a middle ground to be explored.
There’s a relatively new project named esbuild-loader. Developed by hiroki osame, esbuild-loader is a webpack loader built on top of esbuild. It allows users to swap out ts-loader
or babel-loader
with itself, which massively improves build speeds.
To declare an interest here for full disclosure, I’m the primary maintainer of ts-loader, a popular TypeScript loader that is commonly used with webpack. However, I feel strongly that the important thing here is developer productivity. As Node.js-based projects, ts-loader
and babel-loader
will never be able to compete with esbuild-loader
in the same way. As a language, Go really, uh, goes!
While esbuild may not work for all use cases, it will work for the majority of tasks. As such, esbuild-loader
represents a middle ground — and an early way to get access to the increased build speed that esbuild offers without saying goodbye to webpack. This walkthrough will explore using esbuild-loader
in your webpack setup.
It’s very straightforward to migrate a project that uses either babel-loader
or ts-loader
to esbuild-loader
. First, install the dependency:
npm i -D esbuild-loader
If you’re currently using babel-loader
, make the following change to your webpack.config.js
:
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) + } + }, ... ], }, }
Or, if you’re using ts-loader
, make the folllwing change to your webpack.config.js
:
module.exports = { module: { rules: [ - { - test: /\.tsx?$/, - use: 'ts-loader' - }, + { + test: /\.tsx?$/, + loader: 'esbuild-loader', + options: { + loader: 'tsx', // Or 'ts' if you don't need tsx + target: 'es2015' + } + }, ... ] }, }
Let’s see how esbuild-loader
works in practice. We’re going to create a new React application using Create React App:
npx create-react-app my-app --template typescript
This will scaffold out a new React application using TypeScript in the my-app
directory. It’s worth mentioning that Create React App uses babel-loader
behind the scenes.
CRA also uses the Fork TS Checker Webpack Plugin to provide TypeScript type-checking. This is very useful because esbuild just does transpilation and is not designed to provide type-checking support. So it’s fortunate that we still have that plugin in place. Otherwise, we would lose type checking.
Now that you understand the advantage of moving to esbuild, we first need a baseline to understand what performance looks like with babel-loader
. We’ll run time npm run build
to execute a build of our simple app:
Our complete build, TypeScript type checking, transpilation, minification and so on, all took 22.08 seconds. The question now is, what would happen if we were to drop esbuild into the mix?
esbuild-loader
One way to customize a Create React App build is by running npm run eject
and then customizing the code that CRA pumps out. Doing so is fine, but it means you can’t keep track with CRA’s evolution. An alternative is to use a tool such as Create React App Configuration Override (CRACO), which allows you to tweak the configuration in place. CRACO describes itself as “an easy and comprehensible configuration layer for create-react-app
.”
Let’s add esbuild-loader
and CRACO as dependencies:
npm install @craco/craco esbuild-loader --save-dev
Then we’ll swap over our various scripts
in our package.json
to use CRACO
:
"start": "craco start", "build": "craco build", "test": "craco test",
Our app now uses CRACO, but we haven’t yet configured it. So we’ll add a craco.config.js
file to the root of our project. This is where 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; }, }, };
So what’s happening here? The script looks for babel-loader
usages in the default Create React App config. There will be two: one for TypeScript/JavaScript application code (we want to replace this) and one for nonapplication JavaScript code. It’s not too clear what nonapplication JavaScript code there is or can be, so we’ll leave it in place; it may be important. The code we really care about is the application code.
You cannot remove a single loader using CRACO
, so instead, we’ll remove both and add back the nonapplication JavaScript babel-loader
. We’ll also add esbuild-loader
with the { loader: 'tsx', target: 'es2015' }
option set to ensure we can process JSX/TSX.
Finally, we’ll swap out using Terser for JavaScript minification for esbuild as well.
Our migration is complete. The next time we build, we’ll have Create React App running using esbuild-loader
without having ejected. Once again, we’ll run time npm run build
to execute a build of our simple app and determine how long it takes:
Our complete build, TypeScript type-checking, transpilation, minification, and so on, all took 13.85 seconds. By migrating to esbuild-loader
, we’ve reduced our overall compilation time by approximately one-third. This is a tremendous improvement!
As your codebase scales and your application grows, compilation time can skyrocket. With esbuild-loader
, you should reap ongoing benefits to your build time.
Debugging code is always a tedious task. But the more you understand your errors, the easier it is to fix them.
LogRocket allows you to understand these errors in new and unique ways. Our frontend monitoring solution tracks user engagement with your JavaScript frontends to give you the ability to see exactly what the user did that led to an error.
LogRocket records console logs, page load times, stack traces, slow network requests/responses with headers + bodies, browser metadata, and custom logs. Understanding the impact of your JavaScript code will never be easier!
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowSOLID principles help us keep code flexible. In this article, we’ll examine all of those principles and their implementation using JavaScript.
JavaScript’s Date API has many limitations. Explore alternative libraries like Moment.js, date-fns, and the new Temporal API.
Explore use cases for using npm vs. npx such as long-term dependency management or temporary tasks and running packages on the fly.
Validating and auditing AI-generated code reduces code errors and ensures that code is compliant.
One Reply to "webpack or esbuild: Why not both?"
Nice article, thanks! How in this case can I organise the tree-shaking for lodash and styled-components for example? I can do it using babel (https://www.npmjs.com/package/babel-plugin-transform-imports) or in esbuild (https://github.com/josteph/esbuild-plugin-lodash). How can I do it using esbuild-loader?