Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

How We Used Webpack to Reduce Our JS Footprint by 50

DZone's Guide to

How We Used Webpack to Reduce Our JS Footprint by 50

In this video article combination, you'll learn how to reduce your JavaScript footprint, thus increasing the speed of you site.

· Web Dev Zone ·
Free Resource

Jumpstart your Angular applications with Indigo.Design, a unified platform for visual design, UX prototyping, code generation, and app development.


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.jscontains 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 the global script tags became GlobalModules. Each is translated into a ProvidePluginconfiguration, 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 a third-party library 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.

Take a look at the Indigo.Design sample applications to learn more about how apps are created with design to code software.

Topics:
web dev ,javascript ,load speed

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}