Swizec Teller - a geek with a hatswizec.com

Senior Mindset Book

Get promoted, earn a bigger salary, work for top companies

Senior Engineer Mindset cover
Learn more

    How We Used Webpack to Reduce Our JS Footprint by 50

    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.

    Published on February 10th, 2017 in Front End, Technical, Webpack

    Did you enjoy this article?

    Continue reading about How We Used Webpack to Reduce Our JS Footprint by 50

    Semantically similar articles hand-picked by GPT-4

    Senior Mindset Book

    Get promoted, earn a bigger salary, work for top companies

    Learn more

    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 ❤️

    Created by Swizec with ❤️