Did you webPACK your assets yet? - Getting Started with Webpack
Tags: webpack, javascript, webdev, beginners
Published: 2020-09-03 22:13:51

Hola folks!

Here's a documentation of my explorations with setting up Webpack. I checked out Webpack for the first time when I just needed to handle some SVG assets for a react project. But the more I read, the more I realized how beautiful and useful it is. I'm really pumped up to share my take-aways with y'all. Hope this helps other FE devs who want to get started. 

As per Webpack's official doc, 

> Webpack is a static module bundler for modern JavaScript applications

But what does that mean? And how is it even useful?

Here's what I'll cover in this post.

1. The what's and why's of Webpack
2. Advantages of using it over traditional react-scripts of CRA (create-react-app)
3. Setting up Webpack
4. Loaders and Plugins
5. Optimizing

## **01 The what's and why's of Webpack**

Webpack is a bundler which manages the resources and assets of our project (like a CSS/SASS file, an image, or fonts) at compile time. It does so by making a dependency graph to refer to, for every node it visits while processing. That is how it makes sure that code that needs to load first, loads first.

Imagine you have a project where multiple javascript files depend on each other, like this very simple one here. 

In `calculateBattleIndex.js`

```jsx
function calcBattleIndex(hero) {
	return (hero.strength * 2 + hero.defence * 3) / 10;
}
```

In `calculatePower.js`

```jsx
function calcPower(hero) {
	return hero.money / 100 + calcBattleIndex(hero);
}
```

In `index.js`

```jsx
var batman = {
	money: 100,
	strength: 70,
	defence: 92,
}

var superman = {
	money: 50,
	strength: 99,
	defence: 80,
}

calcPower(batman);
calcPower(superman);
```

As you can see, the `caclPower` function is dependent on `calcBattleIndex` function.

So, in order to properly execute the `index.js` file, we would need to include `calculateBattleIndex.js` and `calculatePower.js` in the following order.

```html
<script src="calculateBattleIndex.js"></script>
<script src="calculatePower.js"></script>
<script src="main.js"></script>
```

If we mess up with the order (that is, if we chose to include `calculatePower.js` before  `calculateBattleIndex.js`), then we might get a function undefined error.

But our project might not be as simple and small, so managing dependencies would be a hell of a task. That is one reason why people have started moving to component-based libraries built on javascript, like React.js and Angular.js, because they offer built-in modules to compile code.

Let's see how React does it.

## **02 Advantages of Webpack over react-scripts of CRA**

I'm sure people who've worked on React might already know `create-react-app`, which has some built-in `react-scripts` to run the app, to make a production build, or to even test it. 

But one major problem is that these are built-in script commands, so they are not really customizable. This is where you'll really feel the need to substitute it with Webpack.

Here are some more advantages of Webpack that I've come across:

### **Configurability**

`create-react-app` offers you minimum configuring build settings. They go by 'You Might Not Need a Toolchain' in their official doc. Although there is a way - *by running `npm eject` to get all the configuration files and editing them yourself* - you'll still feel like it takes away the control Webpack provides, where you can really play with different environment configurations as per your needs.

### **SSR (server side rendering)**

> Ultimately server-side rendering is very hard to add in a meaningful way without also taking opinionated decisions. We don’t intend to make such decisions at this time. — Dan Abramov

{% twitter 938505385794818049 %} {% twitter 953296173116276736 %}

SSR on a `create-react-app` is not only complex but it can't be done without the help of third-party support, and CRA's developers are not eager to add this feature either.

But it can be done with Webpack very easily (will not get into that in this post, but you can follow up here: [https://blog.jakoblind.no/ssr-webpack-bundle/](https://blog.jakoblind.no/ssr-webpack-bundle/)).

## **03 Setting up Webpack**

You can install Webpack and its command-line interface by: 

`npm install --save-dev webpack webpack-cli`

That's it.

Check your `package.json` file to see dependencies getting added up there,

```jsx
"devDependencies": {
	"webpack": "^4.44.1",
  "webpack-cli": "^3.3.12"
}
```

Now let's make configuration files - these are required to give sets of rules for how certain types of files will be treated during compilation and resolution (before making AST to parse on). 

For now, I'm making a common configuration file, which will serve both the dev and the prod environments along with the already existing configurations in them (which I'll add later), and name it `webpack.common.js`

The directory structure will look somewhat like this:

```
root
	|_src
	|	|_index.js
	|	|_calculateBattleIndex.js
	|	|_calculatePower.js
	|	|_images
	|_configs
	|	|_webpack.common.js
	|_dist
		|_myProject.js
```

### **Supplying configurations to Webpack**

Since Webpack needs configuration modules to bundle the code, let's make a basic config (inside `webpack.common.js`), where Webpack takes in the `index.js` file, and bundles it in the `dist` directory.

```jsx
// webpack.common.js

const path = require('path');

module.exports = {
  entry: '.src/index.js',
  output: {
    filename: 'myProject.js',
    path: path.resolve(__dirname, 'dist'),
  }
}
```

Add the Webpack start script in `package.json`

```jsx
"scripts": {
	"start": "webpack --config webpack.common.js",
}
```

Now run, `npm start`

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/5f8dzpw0hgjcsx4nyk08.png)

It's quite evident, `myProject.js` in the final bundle which is generated by Webpack for the JS file. We can now remove all the other script(s) from our `index.html` file and just use this generated bundle as the only source script. 

```jsx
<script src="dist/myProject.js"></script>
```

### **Analysing the bundle**

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/y0p9j6deqo0tb9r3nxmn.png)

This section of the file is quite interesting, as we can see the functions we made have been minified and have become an argument to the `eval` function.

The minification is happening because Webpack will run our code in production mode by default. If we don't set the mode manually, the output will be minified.

To set the mode manually, add this to `module.exports`

```jsx
mode: "development"
```

But even in `development` mode, the argument inside the eval function is still minified, so by adding

```jsx
devtool: false
```

in `module.exports` we can make the content in the bundle readable just like the following screenshot.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/yb2fgwcok8kg9l4zud0b.png)

On running through the code, you might have these questions in mind.

**a) Why are we using `path.resolve()` ?**

This is basically used to resolve the relative path for a particular system. For example, in my machine, the `__dirname` (a node script to get the complete address of the current directory) is `atulkumar/documents/src/dist` whereas in some other machine the root directory could be different.

**b) What are entry and output points?**

In this case, the root javascript file (`index.js`) becomes the entry point,  and the output file is the file generated by the Webpack (`myProject.js`)

## **04 Loaders and Plugins**

### **Loaders**

Loaders are used by Webpack to pre-process files. This enables us to bundle static resources apart from javascript files as well. There is a well documented official Webpack doc where you can find a lot of different loaders and their use-cases.

I'll call out a few helpful loaders which, according to me, every project must have.

### **04.01 Loader for CSS `css-loader` , `style-loader` & `sass-loader`**

These loaders will handle our CSS and SASS/SCSS files.

To install the loaders,

`npm install --save-dev style-loader css-loader sass-loader` 

and add the following piece of code to the `module.exports`

```jsx
module: {
	rules: [
		{
		  test: /\.scss$/,
		  use: [
		    "style-loader",
		    "css-loader",
		    "sass-loader"
		  ]
		}
	]
}
```

**Note**: Here, the order of the `use` array matters, and the loaders are applied on our CSS/SCSS file in the reverse order, i.e:

a) `sass-loader` will be applied first which will pre-process the SCSS into CSS

b) and then `css-loader` will turn CSS into Common JS

c) lastly, `style-loader` will inject style straight into DOM

### **04.02 Loader for images and fonts, `file-loader`**

Again, we will need to install it first,

`npm install --save-dev file-loader`

and add the following piece of code in the `rules` array of `module.exports`

```jsx
{
  test: /\.(svg|png|jpg|gif)$/,
  use: {
    loader: 'file-loader',
    options: {
      name: '[name].[hash].[ext]',
      outputPath: 'images/'
    }
  }
},
{
  test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
  use: [
    {
      loader: 'file-loader',
      options: {
        name: '[name].[ext]',
        outputPath: 'fonts/'
      }
    }
  ]
}
```

**Running through from the code...**

1. The test will receive a RegEx to match for the type of file (format).
2. We can also pass an `options` object along with our loader to customize it further - here, I've set up `name` and `outputPath`.
3. `[name]` extracts the name of the particular asset being processed. `[hash]` is a unique hash appended after the dot. This has its own use, I'll talk about it a little later. `[ext]` as by the name, extracts, and appends the extension of the asset.
4. We can also give a custom path for the generated asset type by defining the  `outputPath`
5. `file-loader` resolves `import` and `require()` on a file and converts it into a URL.

### **04.03 Loader for JS or JSX, `babel-loader`**

Install it with:

 `npm install -—save-dev babel-loader`

Also install the presets and plugins it requires, with: 

`npm install —-save-dev @babel/preset-env @babel/plugin-transform-runtime`

```jsx
{
  test: /\.jsx?$/,
  exclude: /(node_modules)/,
  use: {
    loader: 'babel-loader',
    options: {
      presets: ['@babel/preset-env'],
      plugins: ['@babel/plugin-transform-runtime'],
    }
  }
},
```

**Running through from the code...**

1. `babel-loader` is basically used for transpilation. I'm sure you know why we need transpilation.
2. **Why did we exclude the `node_module` directory?**

    While transpiling a `js` file or preprocessing and transpiling the `jsx` we excluded the `node_module` directory. And we did this for a very good reason. 

    When we serve javascript to Webpack or any other asset for that matter, to increase the performance we need to cut down on the amount of code (size of compilation) we give Webpack for transpiling, especially because it's a costly process. So we skip on anything that comes from `node_module` because these should already be runnable, without transpilation. 

    But this doesn't necessarily hold true all the time - you may come across a certain third party library, which may require transpilation on your off days. Don't worry, this can be taken care of as well.

    Imagine there are two modules amongst all the other modules - `module-1` and `module-2` which need to be transpiled. We can simply modify our `regEx` to exclude these modules from being excluded for transpilation, or simply, to include them while transpiling.

    ```jsx
    exclude: /node_modules\/(?![module-1|module-2])/
    ```

    Here, it will skip all the files in node_module except `module-1` and `module-2` 

3. `@babel/preset-env` 

    Thanks to this preset, JS developers can write the latest JS code without worrying about browser support.

4. `@babel/plugin-transform-runtime` enforces babel helper functions that help save on the code-size. (I would recommend you to read the official doc to know more since it's quite interesting: [https://babeljs.io/docs/en/babel-plugin-transform-runtime](https://babeljs.io/docs/en/babel-plugin-transform-runtime))

### **Cache Busting**

There are a lot of things a browser does in the background that we sometimes don't observe. But, caching is something most of us are familiar with. A browser caches certain assets like bundled JS, CSS bundles, or even images to reduce load-time for future visits. If you refresh a page and look at the network tab in the developer tools, you'll see all the calls the website makes to get the content.

Here is my final bundle file `myProject.js` on a hard refresh

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/o8z85qylwqx3qfrqeb97.png)

To compare here is the same file on a soft refresh (cache disable off)

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/h9lji6mrv1pegvgv8vub.png)

Look at the difference in the size, astonishing right? 

But there is a pitfall.

While caching helps improve the load-time of a website, it hampers the user experience. Whenever the content is loaded from the cache, the user won't see the latest content of our website if we've made an update, and we can't expect them to perform a hard refresh, or to clear the cache regularly.

So busting cache becomes important.

After digging a little deeper, I came to know that the browser depends on the file name when it caches it. So essentially, changing the file-name on every refresh should solve our problem. 

But how do we do it?

**`[contenthash]`** does it for us. It is basically a hash generated for extracted content. 

Lets add it to the output file:

```jsx
output: {
    filename: 'myProject.[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
  },
```

**Note**: We can replace the dot with a dash or any other special character, or we can just skip it altogether and simply write `myProject[contenthash].js`. This will also work.

Let's start the Webpack again and check,

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/1zxb3rlm2gbs7v19g8jz.png)

I've added a few fonts and images, but let's focus on the main bundle.

`8dcb493e06ef82c4151b` has been appended to the name we provided in the output. This is the `contenthash`, which like any other hash function gives us a unique hash value, which changes only when the content of any dependency in the bundle changes.

To put in simply, this works like a normal hash function - for a particular value as input the function will always return the same unique output.

**Tip**: You can also slice the hash to limit it to a certain number of characters only, using:`[contenthash:6]`.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/c0lz0g2le70g3ssjdn49.png)

Now we have a unique name on every file change, so the browser will know when to request for this file and when to load it from the disk cache.

A good example to use cache busting would be in `vendor.js`, where we bundle the code from all the third-party libraries, as it doesn't change frequently. 

But how can we link a JS file with a random name in the `<script>`, when it changes on every update?

Plugins! We can do it with the help of plugins!

### **Plugins**

Plugins are used to customize Webpack's build process and they make Webpack much more powerful.

### **04.04  Linking bundles with names having random hash values - `html-webpack-plugin`**

Let me start with a very important plugin `html-webpack-plugin`, which will solve the problem of using `[contenthash]` and linking the output bundle with the main HTML template file.

Let's first install this plugin by running:

`npm install —-save-dev html-webpack-plugin`

Include it in the `webpack.common.js` file.

```jsx
const HtmlWebpackPlugin = require("html-webpack-plugin");
```

Now add this to the `module.exports`

```jsx
plugins: [new HtmlWebpackPlugin()]
```

This will make a new HTML file with a default `<title>` tag and a `<script>` tag linking to the output JS bundle. You'll see your final JS bundle already linked in this newly generated HTML file. 

```html
<script src='myProject.8dcb493e06ef82c4151b.js'></script>
```

But what if we already have an HTML file with some content in it? How do we link all our bundled assets to that particular file?

The answer is fairly simple, 

`html-webpack-plugin` lets us supply our own template using `lodash templates` so that all the bundles can be sourced to this template. 

```jsx
plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'index.html'),
    })
  ],
```

### **04.05 Cleaning up unwanted build resources `clean-webpack-plugin`**

Another really important plugin you can use in your production build is the `clean-webpack-plugin`. Whenever you make a production build by running `npm run build`, you would see new files piling up and increasing the collective size of the build directory. Only the files generated from running the latest `npm run build`, will be important for us so why should we keep all the other extra files?

Well, we won't be keeping them with  `clean-webpack-plugin`.

Let's start by installing it,

`npm install -—save-dev clean-webpack-plugin`

Remember, this would be useful for the production environment as there is no build made in the development mode, well there is but not in our project directory, Webpack makes it in the system memory and loads it from there. 

So now the `webpack.prod.js` will look like this:

```jsx
const common = require('./webpack.common');
const { merge } = require('webpack-merge');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = merge(common, {
  mode: 'production',
  plugins: [new CleanWebpackPlugin()],
});
```

What `clean-webpack-plugin` does is, it empties the build directory before making the new build resources. With this, you don't need to worry about the extra unused files now.

### **Live reloading - the webpack-dev-server**

Suppose you change something in the code and on saving it, the website reloads automatically! Wouldn't that be cool?

`webpack-dev-server` can do it for us and it's quite simple to add it up.

We just need to install it by running this command

 `npm install —-save-dev webpack-dev-server` 

and adding to the `npm start` script in `package.json`

```jsx
"scripts": {
	"start": "webpack-dev-server --config src/config/webpack.common.js",
	// other scripts.
}
```

 Yass that's it, that will do the magic.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/rgp0be7cx61jb9m4veyd.png)

`webpack-dev-server` uses `webpack-dev-middleware` under the hood, which provides fast in-memory access to Webpack assets.

**Note**: `webpack-dev-server` should be used in the development mode only.

**Tip:** You can add `—-open` to the script to start the Webpack with opening a new window with `localhost:[port]` every time you run `npm start`.

### **Configuring according to the environment (dev/prod)**

Like I discussed earlier in this post, we'll be making 3 separate files for webpack config:

One was already made - `webpack.common.js` - let's make configs for both the production and the development environments too, and name them `webpack.prod.js` and `webpack.dev.js`.

Throwing some code in them:

```jsx
// webpack.dev.js
const common = require('./webpack.common');
const { merge } = require('webpack-merge');
const path = require('path');

module.exports = merge(common, {
  mode: 'development',
  devServer: {
		contentBase: path.join(__dirname, 'build'),
    compress: true,
    port: 3006,
  },
});
```

```jsx
// webpack.prod.js
const common = require('./webpack.common');
const { merge } = require('webpack-merge');

module.exports = merge(common, {
  mode: 'production',
});
```

**Running through from the code...**

1. To merge the common module we made earlier with the new ones, we need to install `webpack-merge`  (`npm install -—save-dev webpack-merge`) and include it in both the files.
2.  `mode` will govern the built environment for the Webpack
3.  `devServer` is a set of options picked by `webpack-dev-server`. 
    - `contentBase` holds boolean | string | array value stating the static file location.
    - `compress: true` will enable gzip compression
    - `port` is the [`localhost`](http://localhost) port to serve the website content on

Now, in `package.json` add a build script that would generate the build resources for the production environment.

```jsx
"scripts": {
    "start": "webpack-dev-server --config src/config/webpack.dev.js --open",
    "build": "webpack --config src/config/webpack.prod.js",
}
```

## **05 Optimisations**

Before running straight into optimizing Webpack, let us configure a super-cool plugin which will make the Webpack logs look prettier!

The `webpack-dashboard` plugin.

Let's start by installing it,

`npm install --save-dev webpack-dashboard`

We'll require the plugin,

```jsx
const DashboardPlugin = require('webpack-dsahboard/plugin');
```

Now adding the plugin in the config file and instantiating DashboardPlugin. 

```jsx
plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'index.html'),
    }),
		new DashboardPlugin()
  ],
```

We need to edit the start script as well to make sure Webpack starts up with the `webpack-dashboard` plugin.

```jsx
"scripts": {
	"start": "webpack-dashboard -- webpack-dev-server --config src/config/webpack.dev.js --open",
}
```

Run `npm start`

And Booyah!!

This is your new Webpack log screen 🙌🏻

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/ubrg6k820o4u1zrh3tlk.png)

**Note**: Just so you don't get lost, these logs are from a different project where I'd already installed a lot more dependencies so that we can go forward with optimizations. A lot has to do with third-party libraries. With `npm` you'll get all of your 3rd party dependencies nicely clubbed in the `node_modules` directory. 

### **Splitting chunks with splitChunks**

As you can see in the above screenshot, the only bundle that was generated by the Webpack is `squaremate.8dcb493e06ef82c4151b.js`, having a size of **4.42Mb.**

Now consider this - if we have to change something in the code, Webpack will re-bundle the whole file again (not load it from the cache... because we did bust some cache, right?), and serve it to the browser.

On every change, the browser will be requesting a 4.42Mb of data. That is quite a significant, if not a huge, breach in performance. 

But what is in this file that is making it so huge? Of course, the vendor (third party) libraries.

`splitChunks` enables us to split this file into chunks according to our needs.

Let's configure the basic optimization for Webpack by splitting all types of chunks

```jsx
optimization: {
	splitChunks: {
	  chunks: 'all'
	}
},
```

Run `npm start` and notice the magic!

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/yasbw7sdaio09xfeouv6.png)

As you can see now we have got 2 bundles `squaremate.b9351008d8c24dca3f91.js` [119.64Kb] and `vendors~main.squaremate.dafe32322b2b203e53da.js` [4.31Mb]

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/y4irbj6xejn1wdbha586.png)

Oh hell! This vendor bundle was hiding behind the main bundle and eating up resources of the browser. If you take a closer look at the module section of the logs, you can also infer which module is actually killing up the browser and you can provide special attention to that particular module. 

While this little piece of code can do the magic for us, let's try to understand what is actually happening behind the scenes.

**Inferences**

1. As per the official docs, there are certain rules according to which Webpack automatically splits chunks
    - A new chunk can be shared OR modules are from the `node_modules` folder
    - New chunk would be bigger than 20kb (before min+gz)
    - Maximum number of parallel requests when loading chunks on demand would be lower than or equal to 30
    - Maximum number of parallel requests at the initial page load would be lower than or equal to 30
2. As the vendor code tends to change less often, browsers can cache it and load it from the disk cache itself, rather than making calls for it every time we hit refresh. 
3. If you'll do the math you'll notice the gigantic reduction in the main bundle size here, with just the 3 lines of code we added. Isn't that commendable?

Well, this is just basic optimization. You can flex much more with the power that `splitChunk`  provides. I won't get into more details, but I'll link an insane blog post by David Gilbertson from New South Wales, on optimization by splitting chunks on a whole new level [spoiler alert: more bundles incoming...]. 

[https://medium.com/hackernoon/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758](https://medium.com/hackernoon/the-100-correct-way-to-split-your-chunks-with-webpack-f8a9df5b7758)

(Highly recommended read)

## **Conclusion**

Webpack takes away the worry of managing resources for a front-end developer. You'll know how smart it is in managing them efficiently only if you choose to go into the depths of it. 

The underlying aspects are quite interesting to move forward with, and it's only fair for us to know what has been going behind the scenes because of the sheer power it harbors and gives away to a developer. Do I sound like Alfred from Batman, Master Wayne?

In this particular blog, I tried to give justice to a few of Webpack's important features and tried to explain concepts from my point of view. 

- We started with why is it even important to have Webpack in our project. The smallest example I could think of, still yielding a huge takeaway. There will be more such examples you'd come across, which would help answer your why's.
- We covered the reasons for why would you want to switch to Webpack from a more native react-scripts, when you stumble upon a react project.
- We set up our configuration files for the production and development environments and threw in some loaders and plugins in the process.
- We talked about how could cache busting solve our caching problems.
- We also talked briefly about the optimizations that Webpack provides and how can we save up on the load-time of our website.