Cool Stuff
July 22, 2021

Converting Create React Apps to esbuild

As a product developer and not a JavaScript tools expert, I rely heavily on frameworks and packaged tools such as Create React App (CRA) and Next.js to handle the “build” side of my applications. When I leverage these tools to more quickly and efficiently compile and bundle my applications, I rely on someone else having already done the heavy lifting of configuring Babel and webpack.

However, sometimes I want a little more control and speed in my bundling process. That is why I decided to test a new and exciting bundler called esbuild. To do this, I convert a project I’d built with CRA to a custom version that uses esbuild. As an introduction to esbuild, CRA users may find this particularly useful.

What is esbuild?

Esbuild advertises itself as “an extremely fast JavaScript bundler”, which succinctly highlights what this simple, yet powerful tool is all about.


JavaScript bundler

We’ve had JavaScript bundlers for years, long before I became a web developer. You use bundlers to break up your project into multiple files for an easier development experience, while still bundling all of your code together for browsers to run.


Fast

Esbuild’s claim to fame is that it’s fast. This is the driving force behind using it, though there are lots of other cool things it can do (see the “major features” on the first page of the docs).


Why is esbuild so fast? It’s fast because it’s built in Go instead of JavaScript, which is a much faster language. Go also handles concurrent processes really well, making for a speedy bundling experience.

Why use esbuild?

So you can build faster! There are roughly two sides to building an app, development and production.


During development, esbuild uses incremental builds, which make it very easy to change your code, save, and see your updated app. This means you don’t have to rebuild your entire application every time you make a change during development.


Second, there is the production side to building a website. Bundling for production is often different from bundling for development, because you’ll usually want to do things like minify your source code, tree shake your app, and create source maps. You don’t really need to do these things during development, as these features add more time to building your bundle. For small projects that aren’t rebuilt often, the time it takes to build the bundle isn’t that big of a deal. 


However, for larger projects you typically require CI/CD pipelines to catch errors before pushing to production, as well as preview deployments created on every commit. Because of features like these, your production build is generated more often than you’d expect. 


To speed up this process, developers need to get feedback on whether their build passes the test suite, so they can share preview deployments with reviewers more quickly. With it’s incredible speed, esbuild tightens the feedback loop for developers, leading to faster insights and thus new features getting shipped more quickly.

Converting a CRA app to esbuild

To learn how esbuild works, we will convert a simple app which implements John Conway’s Game of Life in React. You can check out a live version of the app, or look at the source code. Note, the application runs pretty smoothly for me on every browser I’ve tried except for Chrome.


Currently this application uses Create React App. We are going to replace CRA with esbuild.

Installing esbuild

We begin the process by installing esbuild and uninstalling react-scripts.



Next we need to update our public/index.html file to fix how CRA handled some images. Since esbuild does not bundle css the same way as CRA, we also need to import all our styles generated by Tailwind CSS in this file. If you’re not familiar with Tailwind, don’t worry about it, just know that it’s been set up to automatically generate build/index.css which has all of the styles we use.



Bundling an application with esbuild

Now we’re ready to bundle our application with esbuild. If we run the following command in the terminal, it will create the production build almost instantly:


npx esbuild src/index.tsx --bundle --minify --outdir=public/build


While we could add this as a script in our package.json file, it is better in the long run to create a JavaScript file to handle the build script. To do this, we create the file scripts/build.js.


We use an IIFE (Immediately Invoked Function Expression) to call esbuild directly, passing in many of the same commands as we passed to the CLI. There is much more you can do with esbuild, but this is a nice start.

Let’s go ahead and update the scripts section in package.json.


Now we run npm run build to make Tailwind create the production stylesheet and esbuild generate the production bundle. If we run npx serve -s public, we can start hosting a local version of the application.

Comparing esbuild and CRA build speeds

If you ran the CRA build before, you’ll notice that esbuild is significantly faster.

I compared running the two side by side and the build time was reduced by 70% using esbuild. The majority of the esbuild version’s runtime is running Tailwind CSS/PostCSS, so the actual work esbuildis doing takes almost no time. This is a really small app, so your mileage will vary, but as your application gets larger, esbuild will scale really well.



Implementing live reloading

We’ve gained some speed, but we’ve lost a big feature, the development version of our application. As a modern web developer, I want to see the changes I make in my code reflect nearly instantly in the browser, which is something CRA gave us for free.\

To get live-reloading setup in esbuild, we’re going to need to do a little bit of work. First, let’s start by installing 2 helpful tools, chokidar and live-server:



Next, we’ll add a start.js file in the scripts directory we created earlier:



There’s a little more going on here, but hopefully the code is pretty easy to follow. Basically we’re using chokidar to watch if we make any changes inside of the src directory, and if we do it will rebuild the application. For more details, take a look at the more fleshed out example from which I derived my script. Now we can run npm start and have our application automatically reload when we make changes to our codebase. Take a look at the code changes we made. It really didn’t take a whole lot to replace CRA with esbuild (especially if you ignore the package-lock.json changes 😉).

What we gained

SPEED!! 💨

I can’t stress speed enough, this is esbuild’s biggest draw. It’s freaking fast!

We also gained a lot of control. Don’t get me wrong, abstracting things away and using pre-existing frameworks and tools to get your apps up and running can be incredibly valuable. But sometimes it’s nice, and even necessary, to look under the hood and be able to hook things up for yourself.

What we lost

We did unfortunately lose a few things. Currently esbuild doesn’t support hot module replacement, so it’s up to you to set up some sort of live reload as we did above. Given esbuild’s incremental builds, this is still very fast.  But we lost some nice developer experience benefits.

Additionally, esbuild does not have a 1.0 version yet. People can and certainly have used esbuild in production settings, however it is still under very active development, so approach with caution when deciding if it’s the right tool for your next project.

Other resources

There are a number of community plugins available which handle things like converting markdown, implementing various css flavors, compiling Svelte or Vue components, and much more. You can create your own plugins if you need, but it’s definitely worth checking out the community plugins first.

Besides the esbuild docs, I found the blog “What is esbuild?” to be a very helpful survey.

If this blog has made you excited about the potential that esbuild unlocks for frontend development, definitely check out Snowpack (a frontend build tool) or Remix (a React framework) which are both amazing and use esbuild. For further inspiration, checkout the thousands of open source projects using esbuild.