DEV Community

Cover image for Did you webPACK your assets yet? - Getting Started with Webpack
Atul Kumar
Atul Kumar

Posted on

Did you webPACK your assets yet? - Getting Started with Webpack

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

function calcBattleIndex(hero) {
    return (hero.strength * 2 + hero.defence * 3) / 10;
}
Enter fullscreen mode Exit fullscreen mode

In calculatePower.js

function calcPower(hero) {
    return hero.money / 100 + calcBattleIndex(hero);
}
Enter fullscreen mode Exit fullscreen mode

In index.js

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

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

calcPower(batman);
calcPower(superman);
Enter fullscreen mode Exit fullscreen mode

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.

<script src="calculateBattleIndex.js"></script>
<script src="calculatePower.js"></script>
<script src="main.js"></script>
Enter fullscreen mode Exit fullscreen mode

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

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/).

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,

"devDependencies": {
    "webpack": "^4.44.1",
  "webpack-cli": "^3.3.12"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

// webpack.common.js

const path = require('path');

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

Add the Webpack start script in package.json

"scripts": {
    "start": "webpack --config webpack.common.js",
}
Enter fullscreen mode Exit fullscreen mode

Now run, npm start

Alt Text

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.

<script src="dist/myProject.js"></script>
Enter fullscreen mode Exit fullscreen mode

Analysing the bundle

Alt Text

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

mode: "development"
Enter fullscreen mode Exit fullscreen mode

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

devtool: false
Enter fullscreen mode Exit fullscreen mode

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

Alt Text

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

module: {
    rules: [
        {
          test: /\.scss$/,
          use: [
            "style-loader",
            "css-loader",
            "sass-loader"
          ]
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

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

{
  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/'
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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

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

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.

    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)

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

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

Alt Text

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:

output: {
    filename: 'myProject.[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
  },
Enter fullscreen mode Exit fullscreen mode

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

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

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.

const HtmlWebpackPlugin = require("html-webpack-plugin");
Enter fullscreen mode Exit fullscreen mode

Now add this to the module.exports

plugins: [new HtmlWebpackPlugin()]
Enter fullscreen mode Exit fullscreen mode

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.

<script src='myProject.8dcb493e06ef82c4151b.js'></script>
Enter fullscreen mode Exit fullscreen mode

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.

plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'index.html'),
    })
  ],
Enter fullscreen mode Exit fullscreen mode

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:

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()],
});
Enter fullscreen mode Exit fullscreen mode

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

"scripts": {
    "start": "webpack-dev-server --config src/config/webpack.common.js",
    // other scripts.
}
Enter fullscreen mode Exit fullscreen mode

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

Alt Text

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:

// 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,
  },
});
Enter fullscreen mode Exit fullscreen mode
// webpack.prod.js
const common = require('./webpack.common');
const { merge } = require('webpack-merge');

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

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 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.

"scripts": {
    "start": "webpack-dev-server --config src/config/webpack.dev.js --open",
    "build": "webpack --config src/config/webpack.prod.js",
}
Enter fullscreen mode Exit fullscreen mode

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,

const DashboardPlugin = require('webpack-dsahboard/plugin');
Enter fullscreen mode Exit fullscreen mode

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

plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'index.html'),
    }),
        new DashboardPlugin()
  ],
Enter fullscreen mode Exit fullscreen mode

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

"scripts": {
    "start": "webpack-dashboard -- webpack-dev-server --config src/config/webpack.dev.js --open",
}
Enter fullscreen mode Exit fullscreen mode

Run npm start

And Booyah!!

This is your new Webpack log screen πŸ™ŒπŸ»

Alt Text

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

optimization: {
    splitChunks: {
      chunks: 'all'
    }
},
Enter fullscreen mode Exit fullscreen mode

Run npm start and notice the magic!

Alt Text

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

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

(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.

Top comments (2)

Collapse
 
gixxerblade profile image
Steve Clark πŸ€·β€β™€οΈ

Great article!

Collapse
 
atulkr9 profile image
Atul Kumar

Thanks a lot Steve!