DEV Community

Cover image for Understanding JS Build Tools (Part 2: Bundling and Transforming)
Josh Carvel
Josh Carvel

Posted on • Updated on

Understanding JS Build Tools (Part 2: Bundling and Transforming)

Intro 🔨

In Part 1, we talked about the benefits of modular JavaScript and package managers, and how this led to the widespread practice of bundling JS before it hits the browser.

In Part 2, we’re exploring the process of bundling and transforming JavaScript. These days, we tend to use a single tool to do all this work, known as a build tool. We’ll discuss a variety of build tools and the core features they provide, so you can understand how your projects are built and be able to choose and configure a build tool if you need to.

Prerequisites

  • Part 1, or good understanding of JavaScript modules and npm

Choosing your tool 🛒

There are many build tools in the JavaScript community, and they come in many forms. Some are simpler but more ‘opinionated’ in how they are configured, and others are more complicated to set up, but have a lot of flexibility. There’s no perfect tool for your project, unless you make a new one yourself (and now you know why there are so many JavaScript tools!).

You can see usage statistics and ratings of these tools in the annual State of JavaScript survey. As of now, webpack is still by far the most used build tool. It falls into the category of being very flexible and potentially very complicated, which has earned it a divisive reputation. It’s also been around for 11 years now, which is a long time in the JavaScript world.

A few years after webpack debuted, a couple of other build tools also became quite popular. Rollup, from the author of Svelte, was designed as a minimal tool which focuses only on bundling JS and allows you to specify plugins for everything else. Meanwhile, a tool called Parcel sold itself on a zero-config approach, to make it even easier to get started.

But the biggest change in recent years has been the move away from the idea that JS build tools must be written in JavaScript. Though they’re available to install via npm, the new generation of build tools uses low-level languages to perform build tasks more efficiently. This can streamline the deployment process, but perhaps the biggest advantage is speed of build during development, which can make a huge difference to productivity in big projects.

One of the pioneers of this approach is esbuild. It’s still a fairly new and quite minimal project, but it has a major focus on bundling JS very fast, using the Go language and various performance techniques. The benchmark on the esbuild homepage shows it bundling 100x faster than webpack.

Other tools are trying to cater for needs that esbuild can’t meet. The author of Vue.js has written Vite. Its main selling point is around reloading the page during development, which we’ll talk about later. Parcel, meanwhile, was rewritten in Rust for its version 2 - specifically, it makes use of a Rust framework called SWC (Speedy Web Compiler), which we’ll also mention later. And the author of webpack has been busy working with the creators of Next.js to produce Turbopack, which they bill as “The Rust-powered successor to Webpack”. It aims to provide an even better, faster development experience than Vite.

Finally, a couple of very new tools are trying to revolutionise front-end tooling with an all-in-one approach, for a more convenient development experience. Bun is designed as a faster replacement for Node.js and npm, but also comes with a transpiler (I’ll explain that later), a test runner, and a bundler which they have measured as even faster than esbuild. A project called ‘Rome’ had similar ideas - unfortunately the startup behind it closed in August 2023, but it has been forked as Biome. It doesn’t replace Node or npm, but plans to do all of the rest, along with formatting and linting (only the formatter and linter are released at the time of writing). It will be interesting to see if this approach takes off in the JS community once the tools mature.

Ultimately, what build tool you end up using very much depends on the needs of your project, such as its size and complexity, and what front-end library/framework (React, Vue, etc.) and rendering framework (Next.js, Astro etc.) you use, if any. There are initialisers for these frameworks that are built on top of a specific build tool - for example, the create-vue initialiser is built on Vite, since they have the same author.

In this article I’ll demonstrate Parcel, but only because it lends itself well to simple examples, so don’t be unduly influenced by this choice.

Command line JS 👨‍💻

Before we get to discussing their features, it’s worth taking a minute to understand how build tools run with npm.

Executable JS files

Most of the modules we install from npm are imported into our code to eventually be executed (run) in the browser. However, authors of Node.js files can also write their code to be executed on the command line. These are often referred to as executables (though strictly speaking, only code the computer can run directly without an interpreter meets this definition). By tradition in Unix-like systems (e.g. Linux, mac), executable files are held in a folder called bin (for binaries, not ‘bin’!).

It is possible to run Node.js executables using the command node in the terminal followed by the file name. This tells whatever shell program you use such as Bash, Zsh (mac) or PowerShell (Windows), to run the code via Node.js rather than trying to interpret it. However, in any Unix-like system the file author can give the file executable permission, then add an interpreter directive to make the shell run it in the program specified (Node.js in this case): #!/usr/bin/env node.

So let’s say I have installed Parcel and I want to run their command line code. In the ‘parcel’ folder in node_modules I can see they’ve added it in lib/bin.js with an interpreter directive. This means I can actually run this code with node_modules/parcel/lib/bin.js in my project’s directory in a Bash terminal. However, JS devs are lazy folks and we don’t like writing things like paths to files.

npm makes this easier. Firstly, it creates a single folder at the top of node_modules called .bin where it stores symbolic links (sort of like a shortcut) to the actual files. The author must list their executable JS files with names in the “bin” property of their package.json, or if it’s a single file the name is taken by default from the package.json “name” property - ‘parcel’ in this case. This means our command can just be node_modules/.bin/parcel.

Of course this is still too much for JS devs! npm actually provides even more of a shortcut with npx (mentioned in the previous article). npx already knows the node_modules/.bin path, so we can just write npx parcel. I’ve taken you the scenic route to get here, but I think it’s nice to understand what’s really going on under the hood.

Executing non-JS files

One more thing. I mentioned that some bundlers contain code written in languages like Rust or Go, yet they’re available via npm to run in Node.js. What’s the deal?

Well, we are not actually running the Rust or Go code directly. That has already been compiled into various different binaries suitable for execution in particular operating systems. Often these are published to npm separately as scoped packages (each npm user or organisation has a scope starting with @ and can publish related packages under it, e.g. @esbuild/linux-x64), and then added as optionalDependencies in the package.json of the main project, to be installed if possible in the user’s OS.

These binaries can then be imported and used directly in Node.js code because Node.js exposes an API for running natively executable files.

Bundling code 🧺

Ok, now let’s look at how we use a build tool for bundling.

Bundling is essentially performing an action on multiple source files in order to output one (or more, if specified) file(s). The source files are left intact for you to continue working on, and the output can be run in the target environment (the browser, in our case).

To bundle your code, you need to specify an input file that is not imported by other files, from which the bundler will start tracing the chain of imported files. You may also need to give the name of the output file to be created, and the path to the folder where you want it to live.

Your input file may be called something like index.js. The output folder is often called dist (for ‘distribution’), and we typically create a separate folder named src to house the source files. How you set up the HTML file(s) depends on the build tool and configuration, but it could for example live in src and contain a script tag pointing to index.js.

We can then ask the bundler to bundle the files. With Parcel, a basic example looks like:

npx parcel src/index.html
Enter fullscreen mode Exit fullscreen mode

We passed the input file as an argument to the parcel command. The output folder was assumed to be dist, which Parcel creates for us and adds the bundled files into, including a copy of my HTML file with a script tag pointing to the outputted JS file. If we want to change the name or location of the output folder, in Parcel we can use the command line flag --dist-dir, or “targets” in package.json.

Now we have an HTML file which is ready to be served to the browser. All the build tools I’ve mentioned have a simple command like this to bundle JS files. But we can use something called npm scripts to make this even simpler.

npm scripts

npm scripts allow us to specify a shorthand for running commands in the terminal, similar to aliases. These shorthands can be anything you like, but some of them are considered standard, which is nice because you can run the same command for basic tasks no matter what project you’re working on.

The scripts are specified in the “scripts” property of the package.json file, like so:

"scripts": {
    "start": "parcel src/index.html"
  }
Enter fullscreen mode Exit fullscreen mode

Like with npx, we don’t need to specify the path to the .bin folder here. To execute the command, you call npm run (an alias for npm run-script) followed by the name you assigned the script. However, the “start” script is so commonly used that npm have aliased it to just npm start.

Config files

Sometimes it is inconvenient or not possible to specify all options to the build tool on the command line, for example when using plugins. In this case we can use a configuration file, written with a name and format the build tool is expecting. Because these files will be run in Node.js, they are often written using CommonJS syntax. However, since 2020 Node.js has had built-in support for ESM syntax - one way to enable it is adding the .mjs extension to your files rather than .js.

We won’t use a config file in this article because our examples are quite simple, but if you need one the build tool docs should explain everything you need.

Bundling CSS

Ever since webpack came along, build tools (or their plugins) have been able to bundle CSS files too. Since every linked CSS file requires a network request, bundling them in advance helps the page load quicker. It also makes it easier to locate your styles next to the JS module that uses them, if you prefer this approach. Rather than having to add a link tag with a path to the file, you can specify an import from the same directory in the JS file, using ESM import syntax. For example, in index.js:

import "./index.css";
Enter fullscreen mode Exit fullscreen mode

Bundling CSS requires a specific loader, which may be a plugin, or built into the bundler. Typically a bundled CSS file is produced and linked in the outputted HTML via a link tag. In Parcel this works without any extra config. A different approach is to add all the CSS rules to the document’s internal stylesheet (the <style> tag in the HTML file’s <head>, which has the same specificity as an external stylesheet), meaning no network requests are needed to load the CSS. webpack’s style loader uses this approach.

Loading assets

Other assets such as images can also be loaded by build tools from JavaScript. There are a few ways to import these assets, but the simplest syntax looks like this:

import image from "./image.jpeg"
Enter fullscreen mode Exit fullscreen mode

The import which I’ve called ‘image’ here is a URL which can be assigned to an img element’s src attribute. The build tool’s relevant loader will handle getting the asset linked correctly in your output folder. Parcel, esbuild and webpack have loaders built in, though you need a little configuration in webpack and esbuild, and there is a plugin for Rolllup.

Bundling for development

So now we know how to make a basic bundle, but we still need to see our work in the browser before this is useful. We could just open our HTML file in the browser, but that’s not very convenient. npm projects typically use a server, even for local development. Some build tools have this built in, and for others you need a plugin package.

Using a development server

When you serve files, you serve to a particular host (computer), which has a hostname, such as the name of a website. When you tell your browser to get a file from that host, it translates that into the IP address of the computer where that file can be found.

But it is also possible to serve files to the same computer you are serving from. In that case, the hostname is always localhost. The computer reserves at least one specific IP address to do this, which in the IPv4 system is the address 127.0.0.1. (The 127 may seem a bit random, but it’s just half the maximum of an 8-bit block in binary).

When we ask to go to localhost or 127.0.0.1, the computer knows it can skip the actual networking and instead use something called the loopback interface, so we don’t need to specify the HTTP protocol. But we do need to specify a port so the browser knows which program to ask for the file.

There are 16 bits free for port numbers, which is over 65,000 numbers, though some are reserved for particular things. A running instance of a program on the host machine that wants to access the network can use any port which is not reserved or currently being used. Port 3000 is the default port for Node.js apps, so you will often see this used by JS build tools. Other round numbers in the thousands are often used too. Another standard is 8080, based on the fact that the standard HTTP port is 80. The port number is added after a colon, e.g. localhost:3000.

In Parcel, there is a built-in server which by default serves the code on localhost:1234 when you run the parcel command. You can also add the --open flag at the end to get the server to open up that page in your browser. Then you will see the contents of your HTML file on the page.

Rebuilding and reloading

Having the project served is one thing, but during development, we also want to make lots of changes to our source files and see the results reflected in the browser. A typical dev server can watch development files and detect when a file is changed. When this happens, it can trigger a rebuild, so that if you refresh your browser page, the new code is served.

This is all well and good, but JS devs are impatient as well as lazy. We scoff at having to refresh the browser page when we make a code change!

One solution is to get the build tool to reload the page for you, often called live reloading. In esbuild, if you’ve enabled file watching you can add some JS that listens to server events using the browser’s EventSource API and calls the browser’s reload function when a change event occurs.

new EventSource('/esbuild').addEventListener('change', () => location.reload())
Enter fullscreen mode Exit fullscreen mode

“Still not good enough!”, the JS devs cry.

In fairness though, if your project contains a lot of JS and other assets that need loaded up-front, a page reload can be quite slow, which hurts productivity. So modern JS build tools (or their plugins) often have functionality to reload only part of the page content during development without a page refresh, which is referred to as hot reloading or Hot Module Replacement (HMR). This basically involves the build tool sending updated chunks of content to the browser to replace the outdated parts only.

The new tools on the block focus heavily on the HMR experience. Vite figured out it would be faster in development mode to actually serve native ES Modules to the browser (whereas in production it uses Rollup to bundle the files). When there is a code change it serves new modules, but lets the browser handle caching everything that hasn’t changed. Turbopack, in contrast, does not use ESM but claims to have achieved a faster refresh speed than Vite by leveraging WebSockets (a fast connection between browser and server).

esbuild, however, has gone in the opposite direction and doesn’t support HMR for JavaScript. In their words:

… JavaScript is stateful so you cannot transparently implement hot-reloading for JavaScript like you can for CSS. Some other development servers implement hot-reloading for JavaScript anyway, but it requires additional APIs, sometimes requires framework-specific hacks, and sometimes introduces transient state-related bugs during an editing session.

Bundling for production

When we bundle for production, we will need a slightly different process. In Parcel, we can use the parcel build command. We can add this command to an npm script called build, so my npm scripts now looks like this:

"scripts": {
    "start": "parcel src/index.html --open",
    "build": "parcel build src/index.html"
  },
Enter fullscreen mode Exit fullscreen mode

Then to create my production bundle, I would run npm run build. How and where this is run depends on your deployment process, which is outside the scope of this article. But it is important to have a separate production build so that the development-only features we’ve talked about are turned off.

The packages you use also sometimes want to know if they are running in production mode, so they can remove development-friendly code like log messages. By convention this is done via an environment variable, which is just a variable that is stored outside of the process (running instance of the program) and can be set by the user. In Node.js, environment variables are accessed via the global process object, on an object called env. The variable NODE_ENV is used for toggling production mode.

Handily, most build tools set this variable for you based on your configuration, so you don’t need to worry about it. For example, Parcel sets process.env.NODE_ENV to ‘production’ when the parcel build command is used, and ‘development’ otherwise. You can always override the variable by setting it via the command line, or alternatively, any Node.js environment variables can be listed in a file called .env.

As well as turning off features, in production mode build tools can also apply optimisations to the bundling process for better performance.

Tree shaking

Tree shaking is a term popularised by Rollup which means removing unused imports from the outputted bundle. As described in the webpack docs:

You can imagine your application as a tree. The source code and libraries you actually use represent the green, living leaves of the tree. Dead code represents the brown, dead leaves of the tree that are consumed by autumn. In order to get rid of the dead leaves, you have to shake the tree, causing them to fall.

Tree shaking works best with ESM import statements, which as we mentioned last article are static in nature, meaning you can know for sure what code will be imported without running it. So build tools usually tree shake unused imports so long as they are not CommonJS imports or being converted to that format (though Parcel can tree shake CommonJS).

Note that we are referring only to unused imports, not unused code in general. For example, if you import a package that exports a single class and you only use some of the methods, the whole package will still end up in your bundle. But many popular libraries split up their exports, so you can just import the parts you need.

Code splitting

Code splitting refers to any means of splitting up your code into smaller parts, known as chunks, in the interests of better performance.

One natural way your code might be split is in a multi-page application with separate HTML and associated JS files. Build tools allow you to specify these files as separate entry points, which results in separate bundles for each page, so the browser only loads the content it needs for each page. However, these pages may use some of the same code, e.g. third-party packages. There is no need for the browser to re-download that code if it already has it cached, which is why build tools enable splitting up a bundle further into chunks (separately loaded files) so parts of it can be shared between pages.

Chunks can also be used in single-page applications, via ESM dynamic imports in the form import(moduleName). Dynamic imports return a Promise that resolves when the module and its dependencies have loaded. These can be used inside conditions or callbacks to implement lazy loading for modules, so they are only loaded if and when they are needed (e.g. see React.lazy in React 18). The build tool then handles creating separate chunks for dynamically imported modules and any shared dependencies. As the name suggests, dynamic imports are dynamic expressions, so they are not as easily compatible with tree-shaking (though again, Parcel supports it).

Note that code splitting support differs across build tools. Parcel and Rollup do a lot automatically, but esbuild support is still in progress. webpack requires you to specify dependencies when splitting chunks for multiple entry points, but has automatic support for dynamic imports via its SplitChunksPlugin. SplitChunksPlugin also by default splits out any node modules code into a separate chunk (known as the ‘vendor chunk’). This is handy for example if you ship new application code but your dependencies haven’t changed, because the vendor chunk does not need re-downloaded if the user’s browser already has it cached.

Content hashing

We just mentioned how the browser can cache our JS files so it doesn’t need to re-download them. This works great unless we do actually need them re-downloaded because the content has changed. For this reason, build tools support content hashing, either via configuration or by default. Hashing is the process of applying a function to some content that outputs a unique string, and if the content has not changed, the string will always be the same. In this case the outputted ‘hash’ string is added to the filename so the browser knows when the file has changed.

Transforming code 🤖

Now that we know how to create a basic bundle for development and production, let’s move on to understanding transformations. The high-level source code that we write can be transformed in a number of ways before it is run by the browser. This process is known as transpilation, i.e. converting code to a different high-level form, as opposed to compilation, which is converting high-level to very low-level code.

We already know that bundling usually involves some conversion of ESM or CommonJS syntax. For example when we bundle for the browser, module syntax is stripped away and often replaced with plain functions that wrap each module.

But there are additional transformations we can do for various reasons. Previously, task runners such as Gulp or Grunt were used for many of these operations, but using the bundler to do them keeps things simpler.

Transforming code for development

Certain tools are used by developers to write code more productively, but need transpiled for the browser to run them.

CSS transforms

Tools known as CSS preprocessors allow you to write custom CSS syntax, then transpile it to regular CSS. The most popular one (as per the 2022 State of CSS survey) is called Sass. Its syntax provides for CSS rule nesting and some programming concepts like variables and functions. How you transform the syntax into CSS depends on what bundler you use. Parcel automatically installs its own transformer for you - other bundlers require adding plugins and in some cases also installing the sass package from npm.

Another tool you should know about is PostCSS. PostCSS is not a preprocessor or even a tool you use directly. It provides some core logic that parses non-standard CSS and can run a chain of plugins to do the transformations. There are hundreds of these plugins such as autoprefixer, which automatically adds vendor prefixes to CSS selectors where necessary, and preset-env, which can transpile modern CSS for older browsers. There are also plugins for pretty much all Sass functionality, so it’s a viable and more flexible alternative. You can use PostCSS plugins by configuring them for your bundler as required.

Finally, it’s worth mentioning Lightning CSS, a fast CSS transform tool written in Rust by the author of Parcel. It’s built into Parcel and also available as a standalone tool. It implements the functionality of some of the most popular PostCSS plugins and can also support the draft CSS specification on nesting if you add the config for this.

Templating languages

Most devs writing for the browser use some kind of library or framework to make the job of writing JS for a system devised in the 90’s a bit more manageable. These provide some form of templating to make it easier to display different bits of UI conditionally with JS. However, this special syntax needs transformed to plain JavaScript for the browser.

The most popular of these languages is probably JSX, the lovechild of JavaScript and HTML which was created for React and can be optionally used in Vue. This is traditionally transpiled by a tool called Babel, which can translate the JSX markup into JavaScript functions to create UI elements.

However, there are now alternatives to Babel. SWC is a tool written in Rust to achieve tens of times faster performance than Babel, and is built into Parcel and Turbopack. esbuild comes with its own JSX transpilation, which is also used by Vite. And the Typescript compiler, which we’ll cover in a second, can also transform JSX to React code. With webpack and Rollup, you can specify your transpiling plugin. All these build tools also support many other templating languages.

Some frameworks come with their own compiler, which just needs configured with the build tool. For example, Svelte uses its own templating language which it compiles to JavaScript, and Solid.js also uses this approach, but with JSX syntax.

TypeScript

TypeScript is an extension to JavaScript with types. VSCode comes with Typescript, so if you write Typescript in a .ts file in VSCode, it understands the syntax, shows type information and errors on hover in the file, and lists errors and warnings from all files in the ‘Problems’ tab. This is a fairly convenient way to use Typescript in your project. However, because the browser doesn’t understand Typescript, it needs to be transpiled to JavaScript.

Many bundlers now come with Typescript transpilation built in, either using SWC (Parcel, Turbopack) or esbuild (esbuild, Vite), so you should be able to get started straight away. There is also an official Typescript compiler called tsc. This is handy when you want the type errors to stop your build from completing, rather than causing an error in the browser console. This is most important for production builds, but can be handy in development too. You can run it from the command line any time you like, or add it to a npm script, e.g. run tsc and then (&& operator) run parcel. We use the --noEmit flag so that tsc only does type checking, not emitting the transpiled JS files.

"start": "tsc --noEmit && parcel index.html",
Enter fullscreen mode Exit fullscreen mode

Getting type errors to block the build on an ongoing basis as you edit files is more complicated because tsc has to run in the background, though Parcel has an experimental plugin for this which you can try. It can also be achieved by bundlers that run tsc via plugins such as webpack.

Bear in mind that tsc uses configuration defined in a tsconfig.json file, which is documented on the Typescript website. Explaining this config is outside the scope of this article, but you will need to configure at least a few properties here if you want to use tsc directly.

Transforming code for production

Serving code in a production environment brings its own challenges which further code transformations can help with.

Browser compatibility

During development you probably use the latest version of your preferred browser, but your users may be using an older browser in which newer JavaScript syntax doesn’t work. To tackle this problem, build tools can transform the newer code into the equivalent older syntax, via any of the transpilers mentioned above - Babel, SWC, esbuild or tsc.

To do this, you need to specify what level of compatibility you need. Many transpilers allow you to specify a range of browsers, from which they figure out which syntax needs transformed. This is done via a popular npm package called browserslist, which accepts a plain English syntax for specifying a range of browser versions and returns the full list. This configuration can be added in a “browserslist” property in package.json, or a .browserslistrc file (a filename ending in ‘rc’ is standard for a config file in any Unix-like system, and can contain whatever format the parser expects). You can see the configuration details on their website.

Note that tsc uses a different approach - you specify the lowest JS version you need to support in the “target” property of tsconfig.json. esbuild’s transpiler (also used by Vite) also uses a “target” property which takes JS versions, but it does accept a list of browser versions as well, which can be generated by running browserslist separately if necessary.

All of this is great for transforming newer syntax, such as the nullish coalescing operator, for example, which can be fairly easily written in older JS. However, there is a separate problem of newer APIs, e.g. Promises, which have more complicated implementations inside the browser (typically in C++). Writing another implementation of an API to fill a gap in browser support is known as a polyfill (a term coined back in 2009). Transpilers don’t take on the task of writing polyfills, but luckily there is an incredibly popular npm library called core-js that does, though it’s mostly maintained, with much frustration, by just one guy in Russia (yup, welcome to the JS ecosystem!).

core-js polyfills can be configured with Babel and SWC. They are not included as part of esbuild or Typescript, but you can import the polyfills you need directly from core-js as described in the core-js docs.

Finally, it’s worth noting that adding extra syntax and polyfills makes your bundles larger, especially when supporting ES5, which is a bit of a waste in up-to-date browsers. One approach to tackle this is known as differential serving/bundling. Two scripts are output - one with ES6 code, linked with type=module in the script tag, the other with ES5 code, with a nomodule attribute in the script tag, which tells the browser to ignore it if it supports modules (an ES6 feature). This is built into Parcel and can be configured in some other bundlers such as webpack.

Minification

Production is all about getting the code to the user as fast as possible, so the less code there is, the better. This also means as few characters as possible, why is why developers use minification to remove any characters that are not necessary for the code to run. Whitespace and comments are removed, variable names are shortened to one character, and syntax may be rewritten in a more concise way (you can usually configure these operations). This can be used on CSS, HTML, SVG and other files as well as JS.

The standard tool for minification is called terser. This became the most popular tool over the older projects uglify-js and babel-minify which are no longer maintained. It’s used by webpack when you specify ‘production mode’, and there is a plugin for Rollup. However, by now you will probably not be surprised to hear that SWC and esbuild use their own, speedier minification process.

Scope hoisting

There's one more little optimisation you might hear about. As mentioned earlier, bundlers need to maintain each module's scope to avoid naming conflicts once the code is all bundled together. In development mode, they create plain wrapping functions to achieve this, but for production, there is a newer approach called scope hoisting. Any top-level variables are given unique names, which avoids the need for wrapper functions, so the code runs quicker. This is built in with Parcel and Rollup, and available for webpack via a plugin.

Debugging code 🔧

Bundling and transforming code has lots of benefits, but it does lead to an issue with debugging. You are probably familiar with debugging a JavaScript error in devtools by clicking on the link to the source file in the top-right of the error. However, when we bundle JS, the source is now one giant file which has probably been transformed in a number of ways from the original code, making debugging much more difficult.

Luckily, there is a solution to this, called source maps. These are files ending in .map that are generated by the build tool, and they enable devtools to point you back to your original source file instead of the bundle. Unless you are writing your own library, you don’t need to worry about these maps too much aside from making sure they are generated, which is sometimes done by default (e.g. by Parcel), and sometimes requires a small amount of configuration.

Conclusion

Hopefully these explanations have given you a much better understanding of various build tools and what they do. Let’s recap some key points:

  • Modern build tools can handle both bundling and running various transformations on our code, and the needs of the development environment are very different from production.
  • There are many tools to choose from, with different pros and cons, including speed of development experience, ease of use and flexibility. Always keep in mind the needs of your particular project rather than getting carried away with the latest trend.
  • Build tools are complicated, but we are in an exciting era where they are also faster, more all-encompassing and perhaps easier to get started with than ever before.

I definitely haven’t covered all the ins and outs of build tools here, so if you want to know more about any of the tools I’ve mentioned, their documentation (listed below) is a good place to start. There are also plenty of discussions in blogs and social media about all these tools and users’ experiences and frustrations when it comes to the fine details.

Read these if you wish, though your main focus should always just be achieving reasonable productivity for your work. After all, tools are just tools - it’s what we build with them that matters!


Footnote: yet more build tools?!

If you looked at the State of JS 2022 survey, you’ll notice a couple of build tools I didn’t mention: Snowpack and WMR. As explained on Vite’s comparisons page, Snowpack is no longer maintained, and WMR is geared towards Preact, so it’s a more niche solution. You may also be aware of Deno, which is the follow-up runtime from the author of Node.js. It used to have a bundle option built in, but this was deprecated in favour of letting other tools handle bundling instead.

Sources

I cross-reference my sources as much as possible. If you think some information in this article is incorrect, please leave a polite comment or message me with supporting evidence 🙂.

* = particularly recommended for further study

Top comments (0)