The growth of the npm ecosystem has been a boon to productivity of developers across the web. Almost any widget, feature, or function is potentially available in one npm install . However, without a careful and continuous skepticism towards those dependencies, your users can pay for this freedom with their time during load. This can lead to lower engagement, lower conversion, and a smaller bottom line. With so much at stake, managing that risk is critical, but how?

Strategy #2: Remove large dependencies

The term “large” is subjective and assumes you have an idea how much a dependency is “worth” to you. This intuitive sense will develop over time. In general, you should try and think about what percentage of functionality a given library helps you implement or how widely used the library is in proportion to what percentage of the bundle size it occupies. If you’re building a react app, and every file starts with import React from ‘react’ , the 33k (minified and gzipped, like all sizes in this article) is likely to be “worth it”. On the other hand, if you’re using something like moment to parse a date from a standard ISO format (something the Date constructor can do natively), moment would not be considered “worth it”. Large dependencies that don’t pull their weight are the where the biggest wins can be found and where you should focus first.

Finding big dependencies

To find out how much space your dependencies are using, the first step is to do a webpack build that outputs a stats.json file. If you are using the CLI, this is webpack ‑‑json > stats.json . Consult the webpack docs or your build system’s docs if using another method, but you’ll need to generate a stats.json file somehow. The stats.json file is the core of the webpack analysis ecosystem. It contains all of the modules used, the reasons for their inclusion, their contents, and other useful data.

With your stats.json file, the next step is to run Webpack Bundle Analyzer in your project’s directory like this:

npx webpack-bundle-analyzer stats.json

A chart called a treemap will show up, similar to the one below:

In this type of chart, the area a box takes up is relative to the size of the file. Folders take up the amount of space of all their children combined. The above screenshot comes from a typical React application, and uses material-ui and react-router as well. Your applications will differ, but visualizing how much space your dependencies are using is an easy way to spot something that is off. The best candidate for an impactful removal is a dependency that is large, but is not very connected into your first-party code.

Nested dependencies

Keep in mind also that a small direct import can import larger dependencies, and you may need to track down the exact path of imports.

In the screenshot above, which zooms in on the overall app, you can see the package lodash.merge . I’m pretty sure I do not use that library in this app, so why is it in the bundle?

jake@shmocking-sherver:~/seriestrackr$ npm ls lodash.merge seriestrackr@1.0.0 /home/jake/seriestrackr └─┬ material-ui@0.20.2 └── lodash.merge@4.6.1

npm ls will show you where a library comes from. In this case, lodash.merge is used by material-ui .

5 kinds of large dependencies

Below are a few examples of unexpectedly large dependencies I’ve seen when doing this on my own bundles. I’ve included images of what these look like in Webpack Bundle Analyzer.

1. moment

(Source)

In the default configuration of webpack, all of moment’s locales are bundled into your app, when most users never even use the locale feature. When bundled this way, over 75% of moment’s 65k size is locales. The root cause of all locales being included by webpack is the dynamic require statement in this function:

function loadLocale(name) { var oldLocale = null; // TODO: Find a better way to register and load all the locales in Node if (!locales[name] && (typeof module !== 'undefined') && module && module.exports) { try { oldLocale = globalLocale._abbr; var aliasedRequire = require; aliasedRequire('./locale/' + name); getSetGlobalLocale(oldLocale); } catch (e) {} } return locales[name]; }

webpack is not fooled by the aliasedRequire rename, so the effective require('./locale/' + name) forces webpack to search the locale directory and include all files it finds in the bundle, in case loadLocale is called with one of their names during runtime.

To fix this, I added the following line in my webpack configuration, which ignores all locale imports:

plugins: [new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)]

(Source)

This brings moment down to 16.4k, realizing 75% savings. This approach is good in a pinch, but moment’s object oriented chaining API makes other types of size optimization impossible as well. If you can help it, I’d try and use a different library, like date-fns .

(Source)

Both lodash and date-fns are broadly useful, feature-rich libraries. In both of them, it is common to use only a function or two at a time. Importing only those functions can be a bit tricky. A statement like import _ from ‘lodash’ will import the entire 24k library even if all that gets used is _.pick . Writing the import as import pick from ‘lodash/pick’ will only import the code you actually intend to use.

(Source)

In this case, there seem to be more files, but these only are the internal lodash helpers necessary for lodash/pick and the bundle size is down to 3.2k. Take a look at lodash docs for a babel plugin to do this automatically. While I don’t usually like package-specific tooling, this babel plugin only automates something you could write yourself and is optional to get optimal bundles.

date-fns also provides subpath imports. In their next major release, date-fns will provide an es modules build, which allows for unused code elimination aka “tree shaking”. I’ll talk more about tree shaking in a future article.

3. slug

(Source)

slug, a library to take any string and make it a safe part of a url, is 3.4k. The Unicode character replacement table that it imports is 98.5k. The small sliver on the right of the above image is the code in the actual slug module. Since I didn’t need the full feature set of this library, I replaced its usage with a set of regexes to do the character replacements I needed directly.

4. video.js / three.js

(Source)

video.js are three.js are large libraries that support complex features. These libraries are also quite large: 132k and 133k respectively. If your app includes 3D visualization or video, it’s not usually practical to remove them. What can be done is delaying the import of these dependencies through the use of dynamic import(). You could skip loading video.js until the user clicks ‘play’ on your video or delay the load of three.js until the user clicks ‘start’ on your game.

(Source)

I’ll go into the mechanics of how to import code asynchronously in a later article, but if you load chunks on demand, the user won’t download any of vendors~three.js until they need it, keeping the page load light with that small blue rectangle.

5. zxcvbn

(Source)

zxcvbn is a library used to determine password strength. It has a javascript module that runs in the browser, but to do this, it has to bundle a huge list of common passwords (388k) to check against. This kind of query makes more sense as a server api call. To reduce bundle size, I added a password strength checking endpoint to my backend app and called it instead of downloading the module to the browser. zxcvbn has clients in 15 different languages, so the endpoint should not take long. If you can’t or don’t want to do that, make sure to delay the load of zxcvbn at least until an account creation screen or password box is visible on the page.

Working with authors

Assume best intent

Keep in mind, the large sizes of many of these modules are not deliberate malice by the package authors, but a side-effect of the multiple targets that npm packages can have: server-side and browser-side. Working with maintainers to help support the browser use case better is likely to be well-received and help you out as well. Frontend js is fairly unique in that the size of downloaded code matters a lot.

Differing priorites

A package like slug is primarily focused on supporting the server-side js ecosystem, where a large Unicode table is not an issue but actually a feature. The author is aware of the size of the table and suggests a mitigation for browserify users. The “ignore the symbol table” approach can be applied in webpack with a webpack.IgnorePlugin , but requiring special configuration for a dependency is a red flag for using the dependency in the first place. It also makes users of create-react-app and other zero-configuration projects consider ejecting to save their bundle size.

Make it work by default

I’d instead suggest a separate build of the library with the symbols table pre-excluded. This file can be pointed to by the browser field of package.json, and webpack will resolve require('slug') to that file instead of the file pointed to by main . If full Unicode support in the browser is something the author wants to support, another import path ( slug/unicode ) can be provided that does import the symbols table. This shouldn’t be the default, so users not yet focused on bundle size can have small bundles anyway.

Looking forward

With bundle size of your dependencies now in mind, make sure to check out something like bundlephobia.com to evaluate new dependencies before npm install ing them. A skeptical eye now can prevent an overlarge dependency from bloating your whole app.

This is part 2 of a series on reducing your bundle size in order to keep your app lean and delight your users. If you want to learn more stategies to keep bundle size down, check out part 1, which talked about removing duplicate modules from the bundle.

If you’re interested in a single, integrated tool to track down large dependencies and quickly see why they are being bundled, you’ll be interested in Watchdog. Watchdog is a tool I’m working on to encode best practices around dependency management, responsible importing, and preventing regression of your hard-won performance gains.

Sign up here for updates

Back to posts