Web Performance with Webpack
Top Performance Issues
What are the top three causes of web performance issues?
- Amount of CSS for initial download
- Amount of Network Requests on initial download
- <= 100KB (uncompressed) initial CSS [total]
- HTTP: <= 6 initial network calls
- HTTP/2 <= 20 initial network calls
- 90% code coverage (only 10% code unused)
The above performance goals will ensure that you have a fast loading website / application across all user experiences, even in emerging markets.
How can you determine your code coverage? Google’s Chrome browser has a feature in its Dev Tools that lets you look at code coverage. With Dev Tools open, press
Shift+⌘+P and type
coverage, then select the first option and click the reload icon. You will then see all of the files and what their code coverage is. This approach to looking at code coverage is a little time consuming, but is currently the only way to check code coverage. It is important to establish performance as a first class citizen, to make performance a priority amongst your team(s). At the bottom of the code coverage view, you can see the total code coverage for all of your files.
Google Web Toolkit first introduced the concept of code splitting. They allowed asynchronous code to be loaded dynamically, so (for example) if you had a piece of code that was only needed when a button was clicked, that code could then be imported and run. Code splitting is the process of splitting pieces of your code into async chunks [at build time]. Code splitting [according to Webpack] is arguably one of the most important considerations to undertake when enabling performance in web applications.
How does it work? Webpack, under the hood, takes your entry point and passes it to a resolver where you must make sure the file exists [if it doesn’t exist, you cannot parse and read it]. Webpack then reads the source code, looks for dependency statements like imports, requires, Common JS, AMD syntax, etc… And then Webpack puts object models called dependencies on them. This information is then leveraged to separate everything in the graph into a separate bundle, flatten it, and create it.
Types of Code Splitting
The future of web is mobile, the average mobile website takes 14 seconds to get interactive, loading less code => interactive faster.
In the context of Webpack, when referring to ‘dynamic imports’, this
Code Splitting Demonstration
Take a look at the following pseudo code for an example of code splitting:
Walking through the above code, we are first importing
Listener, then assigning a function that returns a dynamic import statement and a path to a module. Finally, when an event occurs, the above code is returning a promise that will, on promise fulfillment, perform the functionality. In the Webpack 4 Fundamentals workshop [see the bottom of the page, “Q&A Section”], this was shown when clicking on a button and then loading the footer code. If you are lazy… here’s the code example:
Webpack Code Splitting Under the Hood
Checkout this branch and set the mode in the webpack.config.js file to none; i.e.
mode: "none", then run
npm run webpack. In the
dist folder you should see a couple of bundle files. Take a look at them to get a sense of what Webpack is doing in terms of code splitting under the hood. For my code, since I started this workshop after the Webpack Fundamentals workshop, there are 2 bundle files in my
bundle.js. Webpack ‘cares’ about code coverage, so the multiple bundle files are an effort to reduce your code to only what is needed at runtime.
The most important piece of the bundle code is the
__webpack_require__e function more details about the function here.
The basic premise of code splitting it that Webpack creates bundles (based on your code) that handle asynchronous loading of files. And you do not have to control any of the on demand loading, it’s part of your code. All you are doing is using that function, using that dynamic import. When anyone talks about code splitting or lazy loading bundles, this is what they are talking about. Now that you have some understanding of code splitting, maybe you can take a look at a website you are working on an check out the code coverage of it and then strategize how to split up the code so that unused code is bundled differently and served only when needed! A lot of the time, the unused code is coming from another library or module.
Load a Heavy Module Asynchronously
The example here is using GSAP. Immediately, the bundle size increased dramatically and the code coverage dropped significantly. Just by importing GSAP, you are instantly not using about 80% of the library (because it is in the global scope). Lazy loading to the rescue! Just by using a dynamic import, you can immediately remove unused code from being initially loaded at runtime.
Code Splitting in Vue, React, & Frameworks
There are some frameworks and libraries that disregarded Webpack as an important tool that people use 😢 and therefore missed the opportunity to make lazy loading a first class citizen for them. Vue (at the time of the workshop) is the only framework that has code splitting built in to it. React has some documentation about code splitting as well.
Code Splitting Named Exports
As of Webpack 4, code splitting named exports was not supported, there are some ideas around how they could approach it, but the example used was to import a single feature of a utility library i.e. lodash-es
Vendor Bundles are an Anti Pattern
Optimizing for caching is not the preferred method for saving load time. Pay attention to code coverage for the greatest savings at page load. Only when you get your bundle down to a very small size should you concern yourself with caching.
Dynamic Code Splitting
Anytime you use an import statement i.e.
import('./thing.js'), it’s always returning a promise and its creating and putting the modules that you dynamically imported into a separate file. “Dynamic” code splitting in Webpack is not truly dynamic. Consider the following code example:
The above import statement should be familiar, assigning a variable to a function that returns a dynamic import, but in this case the path is not fully static. If you were actually using the above code, you can see that you can actually access global conditions at runtime to choose when to dynamically load something. You can use runtime conditions to choose when to load something lazily. In Webpack terms, this is considered to be a
Context Module. The above code would tell Webpack to find all of the modules that are in the partial path
./src/themes/ and create a bundle for each of them. Ultimately, there is no real dynamic code splitting happening because all of the assets will be produced as static assets at build time, but you can dynamically serve them. Potential use cases include; A/B Testing, Theming, and Convenience.
Dynamic Code Splitting Walkthrough
For this example, first make sure your
index.js file looks like this:
button.js file looks like this:
Once your files match the above, make a new folder in your
src directory named
button-styles. In the
button-styles folder add
red.js blue.js green.js yellow.js files. Each of those files should look like this:
If you now run
npm run dev, you should find new bundle files, 1 for each file in the
button-styles folder, output in the
dist folder. Following the button example, try to think of some other examples where you might want to structure your components so that the code for them is only downloaded when needed, perhaps based on an event trigger? If you had additional files in the
button-styles folder, that maybe you did not want to include, you can specify in the
index.js file (or wherever you have defined the import) that you only want that specific file type by adding the extension i.e.
Module Methods & Magic Comments
Introducing Magic Comments
When you are using code splitting as a technique, there is no name that is created when you add the bundle. This is because it is not an entry-point so it cannot be given an assigned name. When you look at the
dist folder after a build, you will find a number of bundles there, but they are not named in a way that is immediately understandable. Enter Magic Comments! One of the things that Magic Comments allow you to do is name your bundles. This could prove valuable when you are debugging or if you are trying to keep track of which files generate their own bundles. In
index.js add a
webpackChunkName to the footer import:
To get the name to appear as part of the bundle name, update the
webpack.config.js file like so:
This may be useful to include as part of a base configuration, depending on your requirements maybe only the dev configuration?
Also a part of Magic Comments is a feature called
webpackMode. With this you can further control how code splitting happens, with four different settings. In the instance of the button styles,
/* webpaackMode: "lazy-once" */ would generate a single bundle, which can save build time. Pushing this further and continuing to define different build configurations, you could add this to the
Webpack, by default, injects a macro replacement to the
process.ev.NODE_ENV variable. When you set the mode to
development, the macro converts to development and by default, Webpack can convert the above code and evaluate the if/else statement. You probably care less about performance optimizations in your dev code, but while in dev would like faster build times, this is useful for that.
Webpack Prefetch & Preload
Insert shameless plug to subscribe to the Webpack Medium account and this article
One of the trade-offs when code splitting is either having a smaller initial download or having additional network requests, both have their performance trade-offs. Two additional Webpack Magic Comments are
/* webpackPrefetch /* and
/* webpackPreload /* both of which use
<link rel"pre[fetch/load]"> respectively. More info about prefetch and preload in the Webpack documentation Similar to the previous Magic Comments, try adding a
/* webpackPrefetch /* or
/* webpackPreload /* to one of the ‘lazy loaded’ bundles. Run
npm run dev again and when you look at the source code on your dev server, you should see a
rel="prefetch" depending on which one you chose.
Wrapping Up Code Splitting
Webpack Config Organization
The Webpack config organization has already been discussed / built in the Webpack 4 Fundamentals workshop, with something like:
The idea with presets is not that you only test out a single piece of functionality, like analyze or compress, but that you can build out entire sets of isolated functionality that allows you to experiment, test, or any other reason that you can just add on with a flag or add on with an extra script. The above configuration is enough to get started with the most complex piece of it being the
loadPresets.js file, which is basically just flattening presets and making sure that if there is not a preset that it doesn’t fail. Best practice is, don’t hide your Webpack config, keep it at the root level of your project.
Building Your Library with Webpack
Q: Should you build your library with Webpack?
A: The only time you should consider it is if you were going to ship something that could be loaded with a script tag, like a UMD (Universal Module Definition) bundle. If you yourself are using Webpack don’t use anything else that has been built with Webpack. You lose out on being able to tree shake, scope hoist, or any of the other optimizations.