In this article, I'm going to show you how we went from 30 requests for 3.1MB of minified uncompressed JavaScript to 19 requests for 2.2MB of minified uncompressed JS. Why uncompressed? Because Rails doesn't gzip on localhost, and our production servers are running the new setup already.
We weren't doing anything stupid before. Tree shaking, minification, code splitting to avoid JavaScript users don't need… we had all of that. We even split our frontend into 5 or 6 discreet apps, each with its own internal code splitting.
And yet, 30 requests for 3.1MB.
Our problem: Third-party libraries. There's a bunch of stuff that we need, like Backbone and Handlebars and Lodash and jQuery and so on. Most of them we loaded from public CDN. Some of them we bundled locally into a vendor.js
file; this is where the huge size came from.
You see, when all your apps share a vendor.js
file, and some of them are React and some are Backbone, guess what happens? All apps load both React and Backbone.
We achieved this vendor/app split following Webpack's official guide on code splitting libraries. It suggests using the CommonChunksPlugin
to extract common code into a top-level file.
plugins: [
// Avoid publishing files when compilation failed:
new webpack.NoEmitOnErrorsPlugin(),
new ExtractTextPlugin(
envConfig.inDevelopment()
? "[name]_style.css"
: "[name]_style.[chunkhash].css"
),
new webpack.optimize.CommonsChunkPlugin({
names: ["vendor", "manifest"],
}),
];
manifest.js
is meant to contain Webpack's runtime, and vendor.js
contains your more stable third-party dependencies. This is meant to improve caching. shrug
On top of that, we had a bunch of global libraries configured as externals and loaded them in top-level <script>
tags. Not a bad approach per se, but something like the AWS SDK is almost 500kb of compressed JavaScript. When you're using only one function… yeah, no bueno.
We fixed this situation with a 2-pronged approach:
- bundle and tree shake all our dependencies ourselves
- create a different vendor file for each app (entry file)
Here's how that looks:
const Apps = {
// list entries
// will be reused as Webpack's entry config
};
// previously loaded as externals
const GlobalModules = _.map(
{
jquery: ["$", "jQuery", "window.jQuery"],
lodash: ["_"],
backbone: ["Backbone"],
"backbone-validation": ["Backbone.Validation"],
"raven-js": ["Raven"],
moment: ["moment"],
string: ["string", "S"],
async: ["async"],
},
(vars, module) =>
new webpack.ProvidePlugin(_.fromPairs(vars.map((v) => [v, module])))
);
// updated plugins config
plugins: [
new webpack.NoEmitOnErrorsPlugin(),
new ExtractTextPlugin(
envConfig.inDevelopment()
? "[name]_style.css"
: "[name]_style.[chunkhash].css"
),
]
.concat(GlobalModules)
.concat(
Object.keys(Apps).map(
(app) =>
new webpack.optimize.CommonsChunkPlugin({
name: `${app}_vendor`,
chunks: [app],
minChunks: isVendor,
})
)
)
.concat([
new webpack.optimize.CommonsChunkPlugin({
name: "manifest",
chunks: Object.keys(Apps).map((n) => `${n}_vendor`),
minChunks: (module, count) => {
return count >= Object.keys(Apps).length && isVendor(module);
},
}),
]);
All our entry
files, or apps as we call them, go in Apps
. This helps us iterate over them when building the plugins
config.
What used to be externals loaded in global script
tags become GlobalModules
. Each is translated into a ProvidePlugin
configuration, which essentially replaces all occurrences of $
with require('jquery')
, moment
with require('moment')
, and so on.
With this approach, we don't have to change any code that relies on global libs' availability.
Armed with these vars, we dynamically generate a list of Webpack plugins
. Each app gets its own vendor file – _vendor.js
– and at the end, all files together get a common manifest
file with the Webpack runtime. Again, to prevent cache churning.
Oh, and there's the helpful isVendor
function, which I got from Juho's SurviveJS - Webpack book. You should buy it. It's great.
function isVendor(module, count) {
const userRequest = module.userRequest;
return userRequest && userRequest.indexOf("node_modules") >= 0;
}
This function tells us if a specific module is a third-party library or our own code. It's third party if it's in node_modules
, and our own code if it's not.
I hope this helps make your website faster. It got our page speed score from 54 to 95 ?
PS: This article is a spiritual successor to Migrating to Webpack 2: some tips and gotchas.
Continue reading about How We Used Webpack to Reduce Our JS Footprint by 50
Semantically similar articles hand-picked by GPT-4
- A dirty Webpack trick that reduced our gzipped bundle size by 55KB
- Webpack lazy loading on Rails with CDN support
- Migrating to Webpack 2: some tips and gotchas
- Arcane JavaScript knowledge still useful
- My biggest React App performance boost was a backend change
Learned something new?
Read more Software Engineering Lessons from Production
I write articles with real insight into the career and skills of a modern software engineer. "Raw and honest from the heart!" as one reader described them. Fueled by lessons learned over 20 years of building production code for side-projects, small businesses, and hyper growth startups. Both successful and not.
Subscribe below 👇
Software Engineering Lessons from Production
Join Swizec's Newsletter and get insightful emails đź’Ś on mindsets, tactics, and technical skills for your career. Real lessons from building production software. No bullshit.
"Man, love your simple writing! Yours is the only newsletter I open and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. đź‘Ś"
Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.
Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.
Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help 👉 swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.
Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers 👉 ServerlessHandbook.dev
Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization
Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections
Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog
Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com
By the way, just in case no one has told you it yet today: I love and appreciate you for who you are ❤️