Would you like to learn how to optimize your webpack bundle so that your site loads really fast? Then you’ve come to the right place.
We’re covering how you can make sure that your users get the best experience possible by giving them a lightning fast site. In the first part of this series, we decreased the bundle from 1.7MB to 640KB via
webpack -p. Part two will dig a bit deeper into the soil of webpack optimization by showing you how to use the
NODE_ENV=production webpack environment variable to get a better bundle size reduction and much more.
In this article, you’ll learn how to:
##What we’ll be using Throughout this article, we’ll use our file-upload-example app. It’s a progressive web app that we will optimize in terms of loading speed and payload size. A key aspect of this app is that it demonstrates our upload feature.
If you're on Windows you need to set up cross-env, and then use
cross-env NODE_ENV=production webpack to prevent your Windows command prompt from choking.
##The optimized webpack—where we are and where we want to go
Last week, we described how running webpack with the
-p switch is one of the fastest ways to reduce the payload. But saving bytes on the wire is always important, and that’s why we’re offering you an alternative solution.
Fortunately, decreasing the bundle size and increasing load speed on your own is not that hard. We can supply our own config to UglifyJS, or use another minification plugin. All of this will take place in your
To get started, I’d recommend you do the following:
###1. Minify the code
webpack.optimize.UglifyJsPlugin. For reference, you can also check out the plugin config and the uglify config options. If you want to deliver ES6 or newer code, you have to use babel-minify (formerly known as babeli) with its webpack plugin. There are also other versions of UglifyJS that support ES6 code, but as far as I can tell, none of them are stable enough yet.
A typical config would be something like:
###2. Add the loaderOptionsPlugin to enable minify Because the plugin configuration changed from webpack 1 to webpack 2, this module tries to bridge the gap for plugins that were not upgraded to the new syntax. The code below ensures all (older) plugins are minimized and no longer include debug code.
###3. Make sure all dependencies are built in production mode Many libraries only include code for specific environments. React is a good example of this. With the EnvironmentPlugin, you can specify values for the environment variables that are typically used by these libraries to opt-in or out of environment-specific code.
EnvironmentPlugin is actually a shortcut for a common pattern with the DefinePlugin.
###4. Enable module concatenation With the ModuleConcatenationPlugin, your dependencies and the related modules are wrapped in a single function closure as opposed to having function closures for each module. This process has minimal impact on the bundle size, but it can speed up the execution time of your code.
Note: Webpack has a nice visualization of this output on their GitHub repository.
###5. Extract the CSS to enable better cacheability, parallel downloading, and parsing
Use the ExtractTextWebpackPlugin to extract text from specific loaders into their own files. This is common practice to get separated CSS files that can be cached by the browser and therefore, reduce your bundle size.
The code snippet below will also make sure that the css-loader is minimizing your CSS for production.
You can find the actual commit for this improvement here.
Sweet, this just gave us a 21% boost in the gzipped size compared to the default
webpack -p approach. The first meaningful paint in Lighthouse, however, was only minimally improved. This is due to the delay of the emulated 3G connection, and we might not see much more improvement there going forward. There’s a chance that server-side rendering could help here, but our webpack diet doesn’t cover this.
So how does this benefit your webpack? The new ES Modules syntax allows you to tree-shake your code, which means it will automatically exclude unused parts of code in your webpack bundle. It’s basically dead code elimination with some other neat tricks. This can significantly reduce your bundle size. If you want to dive deep into this—what it exactly is and how it works—here’s some recommended reading. Rollup, another JS module bundler besides webpack, has a wonderful explanation about the benefits using ES Modules. And this article by Rick Harris, the inventor of Rollup, explains the differences between dead code elimination and tree-shaking.
When using webpack 2 or newer, the only thing you have to do is to replace your
import statements. With our SDK, your code would go from this:
##How to avoid dependency duplication A simple way to avoid dependency duplication is to try and keep your dependencies up to date. This will help ensure that you don’t have the same dependency in different versions in your bundle.
In case a dependency does not have a version with an up to date lodash, try to open an issue on GitHub and ask the maintainer to update the dependencies and then re-release it. Hinting about the need for a dependency update in the issue queue is often enough to spark action.
The new npm 5 can also be an issue. If you update your dependencies one by one, the deduplication might fail due to your lock file—and you might end up with some duplicate dependencies. This process gave my colleague Khaled Garbaya a headache some time ago.
The following command can help to reduce dependency duplication, especially for projects that have been maintained over a longer period of time:
The SDKs come with a lot of other new features and tweaks, while the only breaking change was a rename of the browser bundles. Deciding to upgrade should be a no-brainer.
So, let’s integrate the new version into the example app. As you can see in the screenshot below, the big chunk
contentful-management is gone and the code of the SDK now consumes a way smaller part of our file-upload app.
You can find the actual commit for this improvement here.
Even when the JS CMA bundle size was optimized, the ES Modules version only gives us a minimal advantage. There are several reasons for this behavior:
Since we rely on lodash, the lodash code duplication got even worse. Our HTTP client Axios is pretty big, so we’re looking into a replacement. Any suggestions for a good alternative supporting proxies and caching in node and browsers are very welcome.
By breaking all dependencies of the Contentful SDK into modules, we can apply further optimization to our example app. We will do this in the next part of this blog post series.
##Summary By working our way through each of these steps, we’ve efficiently slimed down the size of our webpack bundle. We went from a gzipped size of 399.17KB, all the way down to 165.48KB. And in doing that, we managed to lower the load time on emulated bad 3G connections from around 3292 ms to about 2200 ms. Quite impressive!
If you’d like to see a complete
webpack.config.js with all of the optimizations mentioned in this article, head over to our file-upload-example repository.
But wait, the best is still to come: In part three of this series, we’re going to tackle more detailed optimizations related to some pretty common modules like Moment.js, Babel, and Lodash. Check back on the Contentful Blog next week or follow us on Twitter to stay updated.