DEV Community

Cover image for Friendship ended with Webpack, now ESBuild is my best friend
Rahul Soni
Rahul Soni

Posted on • Originally published at Medium

Friendship ended with Webpack, now ESBuild is my best friend

In this post, I will be discussing about the current configuration of our JavaScript/React setup.
Last year we were struggling with very long webpack build times. Due to our project's constraints, every page in our app is a separate React App, which when compiled and bundled is injected into it's own HTML page, which gets served from a Perl Server.
Our previous webpack configuration used to look like this, where after running npm run build the files would be generated into the reactbuild folder.

require("babel-polyfill");

module.exports = {
  entry: {
    page_name: ['path_to_page'],
    ...,
    ...
  },
  output: {
    filename: "[name].main.js",
    path: path.resolve(__dirname, "./reactbuild/"),
  },
}

Enter fullscreen mode Exit fullscreen mode

At this point our build times were around a minute for npm run build, even while running in watch mode it would take around 5-8 seconds on average for webpack to compile all the changes, which was also affecting our developer experience. Moreover, the longer feedback loop between making changes and seeing the results made it challenging to maintain a high level of productivity and responsiveness in addressing issues and implementing new features.

After some digging around we came to know about esbuild-loader for webpack, which was supposed to be faster than babel-loader that we were using currently. After replacing babel-loader with esbuild-loader, our build times were reduced to 1/10th, along with our bundle sizes. At that point our webpack.config.js looked like this

const path = require("path");
const { ESBuildMinifyPlugin } = require("esbuild-loader");
const { ProvidePlugin } = require("webpack");

module.exports = {
 entry: {
  page_name: ['path_to_page'],
   ...,
   ...
 },
 output: {
   filename: "[name].main.js",
   path: path.resolve(__dirname, "./reactbuild/"),
 },
 plugins: [
    new ProvidePlugin({
      React: "react",
    }),
  ],
  optimization: {
    minimizer: [new ESBuildMinifyPlugin()],
  },
 module: {
    rules: [
      {
        test: /\\.css$/,
        use: [
          "style-loader",
          "css-loader",
          {
            loader: "esbuild-loader",
            options: {
              loader: "css",
              minify: true,
            },
          },
        ],
      },
      {
        test: /\\.(js|jsx)$/,
        exclude: /node_modules/,
        loader: "esbuild-loader",
        options: {
          loader: "jsx",
          target: "es2015",
        },
      },
    ],
  },
}
Enter fullscreen mode Exit fullscreen mode

Everything was working great until we encountered a performance bottleneck with the esbuild-loader when we began adding new pages to our entry points array. As a result, our build times started to increase. This became a significant issue for us, as our project progressed. We noticed that our build process started to slow down considerably, affecting our development efficiency. We had to find a solution to address this challenge and optimize our build process for better performance and faster build times. Hence, we had to take immediate action to resolve this bottleneck and improve the overall build performance. At one point, our build process looked like this, with slow build times and a noticeable impact on our development speed.

npm run build stats

npm run watch stats

In addition to the excessively long build times mentioned earlier, we encountered another issue in our CI pipeline. Specifically, when running the npm run build command, it would sometimes fail to complete execution, thereby causing the subsequent steps in the pipeline to fail as well. To address this problem, we brainstormed the idea of dividing the build step into multiple stages within our CI process, hoping that it would solve the issue. Unfortunately, this approach also proved to be unsuccessful.

Enter ESBuild

While looking for a solution to our build time problem, we stumbled upon ESBuild. We were a little skeptical about it's shown performance, as it was shown 100x faster than webpack, but we decided to give it a try and we were amazed by the results. ESBuild lived up to its promise and significantly improved our build times. With ESBuild, our build times were reduced even further, allowing us to compile and bundle our React apps in a matter of seconds. The performance boost was a game-changer for our development workflow, and we quickly became big fans of ESBuild.

The below image showcases our current build times with ESBuild. From 90 seconds in webpack to less than 2 seconds with ESBuild.

esbuild stats

Our current ESBuild Config files looks like this

const sassPlugin = require("esbuild-sass-plugin");
const autoprefixer = require("autoprefixer");
const postcss = require("postcss");

const path = require("path");
const esbuild = require("esbuild");
const entries = {
 page: "path_to_page",
 ...,
 ...,
 ...
}
const config = {
  entryPoints: Object.values(entries),
  bundle: true,
  logLevel: "info",
  outExtension: {
    ".js": ".main.js",
  },
  outbase: path.resolve(__dirname, "./react/"),
  outdir: path.resolve(__dirname, "./reactbuild/"),
  format: "esm",
  splitting: true,
  mainFields: ["browser", "module", "main"],
  incremental: process.argv.includes("--watch"),
  inject: ["./mui-shim.js"],
  loader: {
    ".js": "jsx",
    ".locale.json": "file",
    ".json": "json",
    ".png": "file",
    ".jpeg": "file",
    ".jpg": "file",
    ".svg": "file",
  },
  watch: process.argv.includes("--watch") && {
    onRebuild(error) {
      if (error) console.error("watch build failed:", error);
      else console.log("watch build succeeded:");
    },
  },
  minify:
    !process.argv.includes("--watch") &&
    !process.argv.includes("--development"),
  metafile: true,
  plugins: [
    sassPlugin.sassPlugin({
      async transform(source) {
        const { css } = await postcss([autoprefixer]).process(source);
        return css;
      },
    }),
  ],
  target: ["safari12", "ios12", "chrome92", "firefox88"],
};
esbuild.build(config).catch(() => process.exit(1));
Enter fullscreen mode Exit fullscreen mode

With splitting code shared between multiple entry points is separated into a dedicated shared file that can be imported by both entry points.

The outdir options allows us to specify the path where the generated files will be stored.

The incremental option allows us to speed up rebuilding files in the development environment.

We highly recommend giving ESBuild a try if you're facing similar performance issues with your webpack builds.

In conclusion, the journey from struggling with prolonged webpack build times to embracing ESBuild has been nothing short of transformative for our development team. ESBuild emerged as the game-changing solution we needed to overcome the performance bottlenecks that were hindering our progress.

Top comments (0)