99

Concurrent visitors over the last 30 minutes. (~)

Peak concurrent visitors -
Yesterday: 0 / Today: 0

Understanding Bundlers and Build Tools

Understanding Bundlers and Build Tools

Bundlers such as Webpack, Parcel, and Rollup are essential tools in modern web development.
These days, bundlers are more often used indirectly through build tools like Vite.js or Snowpack.js, or through frameworks like Next.js, Nuxt, and SvelteKit, rather than directly. Even so, it is still important to understand the basic concepts of bundlers and how they work.
In this post, we will look at the basic concepts of bundlers along with simple examples that show how they work, and then we will also look at build tools.

# Bundlers

Before the browser module system was standardized, people simply connected multiple JavaScript files with HTML <script> tags. This approach had several problems, such as polluting the global scope and making dependencies hard to manage.
So module systems such as CommonJS, AMD, and UMD appeared, and bundlers appeared to use these module systems efficiently in the browser.

A bundler is a tool that gathers the fragmented (modularized) resources needed to build a web application, such as HTML, CSS, and JS, and combines (bundles) them into one file or a small, optimal number of files.
While interpreting the project to combine it, a bundler can do basic jobs such as removing unnecessary comments and whitespace, obfuscating code, and compressing files. Bundlers have also been extended to convert the latest syntax and other special features that make development convenient into a form that browsers support.
In the end, modern bundlers help a lot in making developers more productive and improving browser compatibility and performance.

bundle - to tie or wrap things together.

Browsers support HTML, CSS, and JavaScript (JS) as standards, and with them we can build web applications.
Developing with these standard languages is basic and intuitive, but as a project grows, managing it becomes much more complex and difficult.

In the following example, each JavaScript file uses the same title variable, and they are all connected to a single HTML file, so the title variable is declared twice and an error occurs.

/index.html
HTML
1
2
3
4
5
6
7
8
9
10
11
<link rel="stylesheet" href="./main.css"> <script defer src="./containerH1.js"></script> <script defer src="./containerH2.js"></script> <div class="container"> <button>Toggle!</button> <h1></h1> </div> <div class="container"> <h2></h2> </div>
/main.css
CSS
1
2
3
4
5
6
7
.container { max-width: 1200px; margin: 0 auto; } .container h1 { font-weight: bold; }
/containerH1.js
JS
1
2
3
4
5
6
7
8
9
10
11
12
let title = 'JavaScript!' const h1El = document.querySelector('.container h1') h1El.textContent = title let isShow = true const buttonEl = document.querySelector('.container button') buttonEl.addEventListener('click', () => { isShow = !isShow isShow ? h1El.style.display = 'block' : h1El.style.display = 'none' })
/containerH2.js
JS
1
2
3
let title = 'Bundler!' // Error: Identifier 'title' has already been declared const h2El = document.querySelector('.container h2') h2El.textContent = title

As a simple fix, you can avoid the duplicate declaration by scoping the code with an immediately invoked function expression (IIFE) as follows. But once the project grows even a little, it is hard to manage every file in the project this way.

/containerH1.js
JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
;(() => { let title = 'JavaScript!' const h1El = document.querySelector('.container h1') h1El.textContent = title let isShow = true const buttonEl = document.querySelector('.container button') buttonEl.addEventListener('click', () => { isShow = !isShow isShow ? h1El.style.display = 'block' : h1El.style.display = 'none' }) })()
/containerH2.js
JS
1
2
3
4
5
;(() => { let title = 'Bundler!' // OK! const h2El = document.querySelector('.container h2') h2El.textContent = title })()

Now let's go back to the code before the IIFE and change it to use a bundler this time.
Connect only the main script file (main.js) in the HTML, and import each file you need from that file.
Because we now use a bundler, you can import not only JavaScript (*.js) files but also various other files such as *.css, *.scss, *.vue, and *.ts, depending on your configuration.

Script files imported with the import keyword are treated as modules, so the top-level scope title variable in each file is not declared as a global variable. It is declared as a local variable that is valid only inside the module.

/index.html
HTML
1
2
3
4
5
6
7
8
9
<script src="./main.js"></script> <div class="container"> <button>Toggle!</button> <h1></h1> </div> <div class="container"> <h2></h2> </div>
/main.js
JS
1
2
3
4
5
6
7
8
import './main.css' import './containerH1.js' import './containerH2.js' // import { fade } from './animate.ts' import App from './App.vue' import data from './data.json' // ...

In an environment that uses a bundler like this, you can, for example, use special files with the .vue extension (SFC, Single File Component) to quickly apply TypeScript or SCSS as follows. With components, you can easily create, reuse, and efficiently manage each part of the project.

/Main.vue
VUE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<script setup lang="ts"> import { ref } from 'vue' const title = ref('Hello World!') const isShow = ref(true) </script> <template> <div class="container"> <button @click="isShow = !isShow"> Toggle! </button> <h1 v-show="isShow"> {{ title }} </h1> </div> </template> <style scoped lang="scss"> .container { max-width: 1200px; margin: 0 auto; h1 { font-weight: bold; } } </style>
/Sub.vue
VUE
1
2
3
4
5
6
7
8
9
10
<script setup lang="ts"> import { ref } from 'vue' const title = ref('Bundler!') </script> <template> <div class="container"> <h2>{{ title }}</h2> </div> </template>

However, browsers do not directly support .vue files, TypeScript, SCSS, and so on, so .vue files must be converted into the standards (HTML, CSS, JS) that browsers understand!

The Webpack bundler's website illustrates this concept well with the following image.

The basic concept of a bundler as introduced by Webpack

# Webpack

Webpack is a popular JavaScript bundler. With its many features and extensibility, it helps you manage modules efficiently even in more complex projects.
Webpack mainly provides the following features.

  • Module Bundling: Bundles the files connected to the entry point into a single file.
  • Automatic Bundle Optimization: Through bundle optimization, it creates smaller bundles that load that much faster.
  • Code Splitting: Splits modules into chunks so that only the modules you need are loaded dynamically.
  • Tree Shaking: Removes unused code to reduce bundle size and improve performance.
  • Development Server: Runs a development server that reflects changes in the browser (HMR) every time the code changes.

Let's set up the following React project on top of Webpack.

You can see the final result of the Webpack setup here.

1
2
3
4
5
6
7
8
9
10
11
12
├─src/ │ ├─App.jsx │ ├─index.html │ ├─main.js │ └─main.scss ├─static/ │ ├─favicon.ico │ └─favicon.png ├─.swcrc ├─package.json ├─package-lock.json └─webpack.config.js
Basic project structure
/src/index.html
HTML
1
2
3
4
5
6
7
8
9
10
11
12
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>React x Webpack</title> <script defer src="main.js"></script> </head> <body> <div id="app"></div> </body> </html>
/src/main.js
JS
1
2
3
4
5
6
import { createRoot } from 'react-dom/client' import App from './App.jsx' import './main.scss' const root = createRoot(document.querySelector('#app')) root.render(<App />)
/src/App.jsx
JSX
1
2
3
4
5
6
7
8
9
10
11
import { useState } from 'react' export default function App() { const [count, setCount] = useState(0) return ( <> <button onClick={() => setCount(count + 1)}>Increase!</button> <h1>Count: {count}</h1> </> ) }
/src/main.scss
SCSS
1
2
3
4
$color: red; h1 { color: $color; }

First, install the basic packages needed to use the Webpack bundler.

  • webpack: The core package of the module (package) bundler
  • webpack-cli: Lets you use Webpack commands (CLI) in the terminal
  • webpack-dev-server: Runs a live server for development (HMR)
BASH
1
npm i -D webpack webpack-cli webpack-dev-server

Add scripts that run Webpack to the package.json file as follows.

/package.json
JSON
1
2
3
4
5
6
{ "scripts": { "dev": "webpack serve --mode development --open", "build": "webpack --mode production" } }

Webpack sets up the project's configuration through the webpack.config.js file.
There are many configuration options, but the basic ones are as follows.

  • Entry
  • Output
  • Loaders
  • Plugins

The webpack.config.js file must be located in the project root, and because it runs in Node.js rather than in the browser, you write it as a CommonJS module.

/webpack.config.js
JS
1
2
3
4
5
6
7
8
9
module.exports = { entry: '', // Entry point output: {}, // Output module: { // Loaders (modules) rules: [] }, plugins: [], // Plugins // Other options... }

# Entry

The entry point is the location (file) where Webpack starts interpreting the project.
You can set the entry point with the entry property, and it is usually a JavaScript file.

/webpack.config.js
JS
1
2
3
module.exports = { entry: './src/main.js' }

If needed, you can also use multiple entry points.

/webpack.config.js
JS
1
2
3
4
5
6
7
8
module.exports = { entry: { main: './src/main.js', sub: './src/sub.js', hi: './src/hi.js', // ... } }

# Output

Output is the option that sets the name and path of the bundle files that Webpack creates.

__dirname is a global variable provided by the Node.js CommonJS module system, and it returns the absolute path of the current file.

dist is short for distribution, and it means 'the folder that holds the final output to be deployed'.

/webpack.config.js
JS
1
2
3
4
5
6
7
8
module.exports = { output: { // Default values, // path: path.resolve(__dirname, 'dist'), // Creates the output in the dist folder, based on the absolute path of the current file // filename: '[name].js', // [name] uses the entry point's file name as is clean: true // Deletes the previous output before creating new output } }

# Loaders

Loaders are how Webpack processes modules (files) other than *.js files, and you configure them with the module property.
You write them as an array in the module.rules property, and each rule has test and use properties by default.

Write the test property as a regular expression (RegExp) to specify which files to process,
and use the use property to specify the loader that processes those files.

Most loaders are external packages that you install with npm i -D <name>.

The following example is configured to process SCSS, CSS, JSX files, and several image file types.

  • style-loader: Injects loaded styles (CSS) into <head> as <style>
  • css-loader: Loads CSS files
  • sass-loader: Loads SCSS/Sass files
  • sass: Interprets SCSS/Sass syntax (style preprocessor)
  • swc-loader: Loads JS/JSX files
  • @swc/core: Interprets JS/JSX files (transpiles)
BASH
1
npm i -D style-loader css-loader sass-loader sass swc-loader @swc/core
/webpack.config.js
JS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module.exports = { module: { rules: [ { test: /\.s?css$/, use: [ // Order matters! 'style-loader', 'css-loader', 'sass-loader' ] }, { test: /\.jsx?$/, use: 'swc-loader' }, { test: /\.(png|jpe?g|svg|gif|webp)$/i, // i flag - case-insensitive type: 'asset/resource' // Specifies a loader built into Webpack (builtin loader) via type } ] } }

You can provide the options SWC needs when it interprets JS/JSX files.
With "runtime": "automatic", you can use JSX in React 17 and later while omitting some code (import React from 'react').

SWC (Speedy Web Compiler) is a high-performance JavaScript/TypeScript transpiler.
Because it is written in Rust, it is faster than the existing Babel and can use resources efficiently with parallel processing (multithreading).

/.swcrc
JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
{ "jsc": { "parser": { "syntax": "ecmascript", "jsx": true }, "transform": { "react": { "runtime": "automatic" } } } }

# Plugins

You can connect external plugins that process the bundled output.
Loaders process individual files to build the bundle, whereas plugins process the bundled result.

The following example connects the bundled output to an HTML file, copies the files in the static folder into the output, and sets things up so that React can be used as a global variable.

  • html-webpack-plugin: Connects the HTML file (template) that runs first
  • copy-webpack-plugin: Copies static files (favicons, images, etc.) into the production (dist) folder
BASH
1
npm i -D html-webpack-plugin copy-webpack-plugin
/webpack.config.js
JS
1
2
3
4
5
6
7
8
9
10
11
12
13
const HtmlPlugin = require('html-webpack-plugin') const CopyPlugin = require('copy-webpack-plugin') module.exports = { plugins: [ new HtmlWebpackPlugin({ template: './src/index.html' }), new CopyPlugin({ patterns: [{ from: 'static' }] }) ] }

# Parcel

Unlike Webpack, Parcel is a bundler that you can use simply, without a separate configuration file, and it aims for minimal configuration (zero config).
It automatically installs and handles commonly used or needed features, so you can set up a project quickly and easily.

The Parcel bundler is good for quickly starting small projects.
However, in large or complex projects, it is hard to get the detailed configuration you want, so it is not efficient.

Let's set up the following React project on top of Parcel.

You can see the final result of the Parcel setup here.

1
2
3
4
5
6
7
8
9
10
11
├─src/ │ ├─App.jsx │ ├─index.html │ ├─main.scss │ └─main.js ├─static/ │ ├─favicon.ico │ └─favicon.png ├─.parcelrc ├─package.json └─package-lock.json
Basic project structure
BASH
1
npm i -D parcel

# Entry

By default, Parcel uses an HTML file as the entry point, not a JavaScript file.

/package.json
JSON
1
2
3
4
5
6
{ "scripts": { "dev": "parcel ./src/index.html", "build": "parcel build ./src/index.html" } }

To keep configuration minimal, the Parcel bundler has you connect SCSS, JS, JSX, TS files and so on directly in index.html, and it handles all of them automatically.

/src/index.html
HTML
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>React + Parcel</title> <link rel="stylesheet" href="./main.scss"> <script type="module" defer src="./main.js"></script> </head> <body> <div id="app"></div> </body> </html>
/src/main.js
JS
1
2
3
4
5
6
7
import { createRoot } from 'react-dom/client' import App from './App.jsx' // import './main.scss' const app = document.querySelector('#app') const root = createRoot(app) root.render(<App />)

# Output

Output also needs no separate configuration.
By default, Parcel creates the output in the dist folder. If you want to use a different folder, you can use the --out-dir CLI option.

/package.json
JSON
1
2
3
4
5
6
{ "scripts": { "dev": "parcel ./src/index.html", "build": "parcel build ./src/index.html --out-dir hello-world" } }

# Plugins

You can add features that Parcel does not handle automatically with plugins.
A typical example is a plugin that copies the files in the static folder.

Parcel's JavaScript compiler is built on SWC, so it needs no separate configuration.

  • parcel-reporter-static-files-copy: Copies static files (favicons, images, etc.) into the dist folder
BASH
1
npm i -D parcel-reporter-static-files-copy

When you use plugins, you need to create a .parcelrc file in the project root and provide the default configuration as follows.
Do not remove the "..." code!

.parcelrc
JSON
1
2
3
4
{ "extends": ["@parcel/config-default"], "reporters": ["...", "parcel-reporter-static-files-copy"] }

# Build tools

A bundler gathers various resources and combines (bundles) them into one file or a small, optimal number of files.
A build tool is a tool for managing the overall flow of a project's build process. It usually includes a bundler and also has several other tools needed for development, such as a development server, linting, and testing.

# Vite

Vite is a build tool created by Evan You, the creator of Vue.js. It started out for Vue.js projects, but it has been extended so that other frameworks such as React and Svelte can use it too.

  • It provides a development server (HMR) that stays very fast regardless of project size.
  • It supports JSX, TS, TSX, SCSS, and more out of the box, without extra configuration.
  • It is optimized so that there is no difference between the development server and the production build.

ESM is supported in most browsers, but using only ESM in production mode (rather than development mode) is inefficient because of problems such as nested imports.
Features such as code splitting and tree shaking still require bundling in production mode, so Vite uses the Rollup bundler internally.

As you can see in the following image, the bundle-based approach has to rebuild everything whenever something changes, so build times grow as the project grows.
But with the browser's native ESM, only the changed modules are applied right away, so you can develop quickly regardless of project size.

A route is the base path that distinguishes web pages or detailed screens (views).
It is easiest to think of it simply as a 'page'.

Comparing how bundle-based and ESM-based development servers start

Let's quickly set up a React project on top of Vite.
To use SCSS, also install the sass package. No separate configuration is needed.

You can see the final result of the Vite setup here.

BASH
1
2
3
4
5
6
7
npm create vite@latest . # > Select React # > Select JavaScript + SWC # Install dependency packages npm i npm i -D sass

Vite starts with a basic project structure.
So if you modify it to look like the earlier Webpack or Parcel bundler projects, you get a structure like the following.
It is sample code that can run the development server right away, so modify it and use it however you like.

1
2
3
4
5
6
7
8
9
10
11
├─src/ │ ├─App.jsx │ ├─main.jsx │ └─main.scss ├─public/ │ ├─favicon.ico │ └─favicon.png ├─index.html ├─package.json ├─package-lock.json └─vite.config.js
Project structure

For a more detailed Vite project setup, see the Starting a React Project with Vite or Starting a Vue Project with Vite posts.