A gentle introduction to Webpack

Today's javascript ecosystem is full of new concepts, tools and libraries. Some of them are still with us, some of them died along the way. One of those tools that managed to stay on top is Webpack. If you are not too familiarized with the concepts behind webpack, this post is for you ;). My approach will be with more focus on the practice rather than giving you long, hard to grasp, definitions. So, let's start by the basics.

Module bundles

If you look into the oficial documentation of Webpack, they define this piece of software as: “webpack is static module bundler for modern Javascript applications”. To be honest with you, that definition sounds weird to me: kind of not web-oriented, more propaganda than a real thing. It turns out, webpack it is really a static module bundler for javascript apps. I was being too negative about it (in my defense, every day we know about a new super “solve-all-my-problems” new tool for javascript, so I’m a little bit skeptical regarding this topic).

So, what webpack does? consider this code (ES6):

and:

We have two files: exciting_times.js and math.js, and the first depends on the second. Webpack allow us to bundle those two dependent files into just one no-dependency bundle. Probably, you must be thinking, why?

The first most obvious answer is: to make above code work on most web browsers. ES6 is still not fully supported yet, webpack (plus another tool called babel) let you use ES6 in development mode, and later in the final bundle (built by webpack) your code is going to be fully compatible with older browsers, in a process is called transpiling. That is pretty exciting, bla bla bla. But, there is another good reason to use webpack.

Enters tree shaking and dead code elimination

In the near future, ES6 will become the standard in web browsers, so this transpiling feature will not be so useful anymore. So, what is webpack giving to us that is so useful? Consider the next code:

and:

So, we have our math module with two functions but in our actual app (exciting_times2.js) we only use the sum function. If you run the above code in your browser within a webpage:

In your networks panel on your browser, you are going to see something like (I’m using chrome):

3 requests! one for the index file, another for the exciting_times script and another for the math library. And if you check the code for the math request:

The entire math.js library is loaded. Nothing wrong or weird here, is just how javascript works, if you want to use the sum function in your code, you need to request the complete math library file. Thats kind of inefficient, even more in the web world where performance is a must. In an ideal world, if we want to use only the sum function, well only that code should be loaded. Webpack enters to save us!

Webpack implements (by means of plugins) a feature called tree shaking. This technique, erase the code that is actually not being used in our application (in the above example, the times function) and in the final bundle produced, this code will not be there! Think about all code you don’t use in third party libraries… This is why webpack is so amazing.

To enable tree shaking, you need to tell webpack to minify your code. For this, we use UglifyJSPlugin:

and in our webpack.config.js file:

with only that amount of configuration we are benefiting our application from tree shaking and bundling.

Code Splitting

Code splitting is a common technique in modern bundling systems which allows the developer to split the code of an application in smaller parts, to avoid loading big chunks of code at once.

So, webpack in its simpler form, will create just one big bundle with all our code and resources, code splitting is about avoiding this. Why? because we are in the web development business, where the number of requests and the size of requests matters and matters a lot!

Webpack offer us different ways to code split our app. Let check the basics with the next example:

and:

and suppose we have this two html pages (the scripts in the dist folder doesn’t exists yet, later I’m going to show how to tell webpack to build those files):

and:

So, we have two different pages, and the second page doesn’t need to know about the code in index.js. Also, the first page doesn’t need to know about the code in module2.js. But, with the default configuration of webpack, we only get one big bundle making the loading of every page inefficient. We can fix that very easily:

Execute webpack:

and you will see two different bundles in your dist folder, one for index and one for module2. Nice!

Using the technique above, we can split our codebase into different chunks and avoid to load unnecessary code in some pages of our app. The problem with this version of code splitting is, if you check the code of every bundle, you are going to see in both index and module2 the code for lodash, so we are effectively loading lodash twice! (one for the index.js and one for the module2.js). It would be better if we load lodash only once for the two scripts.

Actually, webpack allow us to split our codebase but preventing duplications of code in every bundle. For this, we use the CommonsChunkPlugin:

Note what is the output of webpack:

Webpack created for us a new bundle called common bundle, if you look the content of the file you will see lodash library on it. So, we effectively split our code in a way more efficient fashion. Now, we load lodash only once and our two bundles consume the same common bundle with lodash on it.

Webpack offers us another useful technique to split out our code. This technique is called /dynamic imports/ and probably it’s the technique where you can get the most of code splitting in webpack. So, to understand what is dynamic import, consider the next scenario. In your application, you need to use a lodash function only when the user clicks on a button. With the technique from above, we are actually creating a bundle just for lodash which improves our performance a little bit. But, it seems unproductive to always load lodash because we need it only when the user clicks a very specific element ¿what if the user never clicks that element? with the technique from above we still are loading lodash. With dynamic imports, we can solve that limitation. Let’s see how this works:

In the html code from above, we added a button with an id of myButton. Also, we loaded the main bundle of our app. The code for our index.js is:

So, here we added a new function called /addEvent/, which add an event listener to our button. The interesting thing is import statement in the onClick event:

That import statement is actually telling webpack: “when the user clicks on the button, load the lodash chunk an after is loaded execute the code passed to the “.then” function. This is also called *lazy loading* of dependencies. Your wepack.config.js should be something like:

Now run npx webpack:

Webpack generates two different bundles, one for the index.js app and the other for lodash (automatically knows it needs to have two different bundles, because lodash is being lazy loaded in our code). Let’s see how this works in our browser:

We can see from the gif above, on the page load we only make one request for the index.js file, later when the user clicks on the button another request is made to load lodash. If the user doesn’t click the button, the lodash is never loaded and the overall performance of our app is improved.

Source Maps

When you bundle your javascript files you often loose track on valuable information when dealing with errors in your code. So, for example, if your file file1.js has an error, in your webpage you are going to see that error pointing to your index.bundle.js file. Tracking the error to the original file can be hard when the codebase grows or when the apps are too complex. This can be solved by the means of source maps (let’s take the same configuration file from the lazy loading section):

We only need to add the ‘devtool’ property in our configuration file to enable source maps in our code. Bundle your files with nix webpack:

Everything looks pretty much the same right? actually, take a look at the filesize of each bundle. They have a size twice as their original value! *you should use this feature only in development mode!*. So, why is this useful? the browser will help us to answer that question:

From the animation above, we can see the console.log’s tracking down to the original javascript code (index.js) and not to the index.bundle.js. The source maps included in our bundles tells to google chrome where to find the original source code. This is really useful when solving coding problems and to easily track the code that triggered an error in our application.

How you can actually run this samples? webpack-dev-server

Until now, I haven’t told you how you can actually run those samples. Of course, you can just write the index.html file, bundle your javascript and open the final html file from your browser. That technique has a lot of limitations (browsers impose a lot of limitations to directly-from-the-filesystem html files) and to overcome those, one usually lift up a development server to serve our assets and html files. Luckily for us, webpack has a nice server to help us, let’s start by adding it as a dependency to our package.json file:

Now, modify your webpack.config.js file like this:

We only added the devServer attribute to our configuration. With this config file, you can run from your terminal:

And you should see an output like this one:

If you go to http://localhost:8080 you will see your webpage served together with your assets, and the best of all: fully functional and without file:// restrictions!

Another great feature of this web server is the ability to monitor the changes on our source code. To test this, make a change in your index.js file, you will see webpack-dev-server recompile everything and even automatically reload your browser. Nice!

Hot Module Replacement

Besides webpack-dev-server, another nice feature of webpack is its ability to refresh our modules without the need of a full refresh of our page. Again, obviously this feature is only useful in development mode. Lets see how this works. First, we need to enable HMR (Hot Module Replacement) in our configuration file:

Note the “hot: true” in the devServer property and the addition of the HotModuleReplacementPlugin in the plugins property. Also, we our going to change a little bit our index.js file, remember our old math.js module? let’s use it again:

So, we updated our onclick handler to use our math module and calculate a simple sum. Note also the last piece of code in the file:

To actually make HMR works, we need to implement the module.hot interface in our client javascript file. Is pretty simple, for every module we want to reload, we need to implement the accept function with the module as a parameter. In this case, we are implementing the accept for the math module. Note that if you don’t implement the accept call, HMR will not work! Now, as usual, let’s see how this works on the browser:

Note that after the HMR, the console of the browser is not reloaded, an indication of no new request to inject the changes on the math module.

Preparing for production

To finalize this introduction to webpack, let’s check how we can prepare our application to go into production. Luckily for us, webpack has some useful features to handle development and production environments with ease.

The basics are to separate the configuration of webpack into development and production mode configurations, so separate configurations per environment. As a lot of the basic configuration will be the same for the development and the production environment, we are going to keep a common configuration for the two envs and then separate only specific parts of the configuration files per environment. For this to work, we can use the tool webpack-merge:

This tool will allow us to reutilize the common configuration in our different environments. Let’s see the definition of the common configuration:

It’s very similar to our previous examples. Now, the development configuration:

with the merge tool, we “imported” the configuration of the common file. Also, we enabled the source maps in our bundle. For the production configuration:

So, no source maps for production, and also we used UglifyJS. The DefinePlugin instruction it’s used to notify Node that we are actually in production environment (when using the webpack.prod.js). To finalize, let’s add some tasks to our package.json file:

So, we have a start task that runs the webpack-dev-server (development mode) and a build task that use our webpack.prod.js file to produce a production bundle. To test if this worked correctly, run:

and point your browser to localhost:8080. Everything should be working as usual, in development mode (like the previous examples). Stop the webpack dev server and execute:

You should end up with two files in your dist folder. One for the index.html and the other with the content of your bundle, if you look your bundle for the production build, you will see no source maps at the bottom!

Final thoughts

I hope that with this short introduction you can have a notion of what is webpack and what are the benefits of using it. As stated at the beginning of the article, sometimes with the documentation and all the hype terms you read on the internet it can be daunting when starting with a new technology, I tried to do a gentle introduction to webpack based strongly on their own docs. The best way to get a grasp of webpack is to use it, so start a new project, choose your preferred library, maybe with ES6 and use webpack as your bundle generator. You are not going to regret it.

Share:
Diego Acuña

Diego Acuña

I'm a Software Engineering and Web Developer from Chile. My main interests are web technologies, software development methodologies and data mining (mainly on financial area). If you want to get in touch with me, use the contact page from the top menu.