Cut your Web Application’s Load Time in Half with These Webpack Optimization Techniques

The line between native applications and websites is blurring. Google is championing Progressive Web Apps. User expectations rise while tolerance for latency / slowness falls. In December 2016 mobile internet use passed that of desktop (http://gs.statcounter.com/press/mobile-and-tablet-internet-usage-exceeds-desktop-for-first-time-worldwide). Despite that, average mobile network speed in the US sits at around 12Mbps. Within that average exists huge speed variation from 1 to 30Mbps — Verizon’s average speed on 3G is a paltry 0.83Mbps.

At the same time, web app development feels like it’s moving ever further away from the end user. What gets sent down the wire is a world away from the beautiful code we spend hours crafting. An application in development consists of requiring modules, loading packages and libraries and frameworks. We have little thought for the HTML that ultimately gets rendered to the user’s page, or the JavaScript that’s creating it.

Developers can sometimes have a tendency to think of module bundling tools such as Webpack as a black box — if it works then great, and if there’s hot-reloading — even better. Given most of the time we’re sat rendering our apps from a local server or via fiber optic internet and on powerful desktop machines, the 3MB of JS isn’t creating the kind of problem that will make a future user leave and never come back.

Optimization is about as sexy as testing — but annoyingly it’s just as important. So what do we need to be thinking about?

Checkout Google’s Lighthouse project for the current thinking on what to consider when considering optimisation. It’s becoming more an art than a science, so rather than fixed time and file size targets, think about page-load as a user journey:

It’s happening! — The start of journey just confirms that the user has connected to your server and the app’s coming. If that’s not happening in well under a second then folks are going elsewhere.

It’s useful! — Here the user starts consuming content, makes a decision on utility and whether they’re going to stick around. This is what Lighthouse means by First Meaningful Paint and is a necessarily subjective metric. You need to consider the point at which the load becomes meaningful. The goal here should be 0.5s to 2s.

It’s useable! — The site becomes interactive. Not only does enough code need to have been downloaded and rendered, but sufficient CPU capacity must be available to accommodate smooth user interaction. This is ‘Time To Interactive’, and you should be aiming for under 5 seconds and preferably under 3s.

Keep in mind your actual targets will depend on the application. But whatever you settle on, loading should be incremental and progressive. So a single, monolithic JS generated by default Webpack isn’t going to cut it. Instead you’ll need to employ some advanced configuration.

Before we go through more advanced optimization techniques. Let’s give some context to lay the groundwork to build your app’s for performance. Webpack is a bundler for javascript files. The benefits of using a bundler is to have control over how many get requests your client has to perform in order to load your application. With Webpack it’s possible to bundle multiple assets, files, libraries and even images into a single file so your client can load your entire app in a single get request, relieving your client to having to do multiple ‘get’ round trips to the server.

As I have mentioned before, Webpack tends to be a black box that many developers don’t take the time to understand deeply. Most developers and teams understandably want to focus on the core features of their application. Some teams would consider their bundle configuration after deployment and discover that their bundle contains megabytes of extraneous libraries, files, images etc. Taking the time to understand Webpack and continuously optimizing your bundle throughout development is very powerful in creating an amazing user experience. So with that said let’s dive deep into the realm of understanding Webpack and bundle optimization. We’ll start with…

Minification — Minification is the process of removing all unnecessary characters from the source without changing its functionality. All white spaces, newline characters and comments are removed. Certain minifiers will also shorten variable names and turn your ‘if else’ statements on multiple lines into a single ternary operator on a single line. All these aspects of your code are used for readability, however have no value to the computer. Minifying your code is a great way to make sure your bundle size is as small as possible.

Tree Shaking — Tree shaking means “dead-code elimination” in Javascript. Webpack 2 came with built in support for ES2105 modules as well as unused module export detection. For all modules that aren’t imported/used Webpack will mark it as an “unused harmony export”. The function/module will still be included in the bundle until you use a minifier that supports deadcode removal, the UglifyJSPlugin mentioned earlier. With both minification and tree shaking our bundle should be quite a few bytes smaller!

Identify whether you’re shipping code that’s unexpected to users. Using a webpack bundle analyzer you can visualize your bundle and the size of each part of your bundle. This will allow you to identify parts of your bundle that might have gotten in there that you didn’t expect. When developing larger web applications at scale this becomes particularly important since you want to check what you or your colleagues have installed and required in into your bundles. These visualizations will display every file within your bundle and show a visual representation of their relative sizes. So you can look for some low hanging fruit that you can take out of your bundle from there.

Purify CSS — Purify CSS removes unused selectors from your CSS. Your scripts and views files will be scanned for classes, and those that are unused will be stripped off the CSS.

Code Splitting — Webpack is great in bundling your files into a single js file so the client doesn’t have to do multiple http requests to get all the assets to put together the user experience. The client will only have to do one http request to grab all the files of the user experience. However, this can lead your bundle size to be very big. This would be great to be able to split the bundle up and load some of it to begin with, only what is necessary to produce the initial user experience while loading the rest afterwards, while the user is experiencing the initial view.

Vendor Code Splitting — Vendor code splitting splits your vendor code and your core web app files into separate bundles, typically called bundle.js and vendor.js. For example, the React components that renders your views would be in bundle.js, separate from the React library, Bootstrap, and node modules in vendor.js. Splitting your code this way would make the user have to gather multiple files in the initial render of your application but, with every subsequent render, your user would only have to download the bundle.js file, only the core files that form of your app. The libraries and modules in vendor.js would already be cached on the user’s browser. Splitting your bundle in this way allows for your app to achieve insanely fast load times.

React Router Code Splitting — Another way to code split would be splitting the code between the different routes. If you’re using React and React Router this is another performance optimization you can consider. This method would further split your bundle, one file for each route. The client would perform a ‘get’ request for only the content necessary to render the view where the user navigates. There’s no need to go overboard and split your app at every route if you have a small application. However, if your application is large, React Router code splitting further minimizes what is sent to the client with every get request, allowing for a virtually seamless user experience.

Automatic Image Compression — Use the ‘image-webpack-loader’ plug-in to automatically compress images for you. Also, if your images are small enough, you can use ‘url-loader’ to give you the ability to encode your images as a base 64 string and include the image in bundle.js as raw data. There is definitely some performance gains with including images within your bundle. Likewise, ‘url-loader’ handles large images by including the image in the output directory. While this may add an additional get request, the large image would lag the initial render the the view if it were to be bundled with everything else.

Looking at what is in your bundle — Perhaps the biggest performance optimization you can do for your bundle is to remove unnecessary files from it. You can do this by visualizing your bundle in one of the few webpack bundle analyzers out there, like webpack-bundle-analyzer. Typically with these tools you would need to export your Webpack’s stats.json file. You can do this by running ‘webpack — json > stats.json’ in your terminal. Next, import the stats.json file into the visualization tool and that would render a visualization of your bundle. Looking through this visualization will allow you to make decisions on which files to keep and which to remove. Did you find that you have an extra lodash or d3 installed? Remove that and you will make huge gains in your application’s performance.

Web Performance is a continuous improvement process. Just as your web apps are going to continuously improve you would need to keep an eye on and consider performance on a continual basis. Continually monitor your webpack bundle size and structure and continually making adjustments with every iteration of your app is key to building an excellent user experience.