Size Limit: Make the Web lighter

Cover for Size Limit: Make the Web lighter

Keep your JavaScript dependencies and polyfills in check and find out what exactly makes your code bloat with Size Limit. To demonstrate, we will code a simple library together, and then shrink its browser bundle so that it takes 5 000 (yes, five thousand) times less space than initially.

The power of limitation

Let’s admit: node_modules is a blessing and a curse.

npm is the world’s largest software registry, listing more than 350 000 packages. This unrestricted access to the collective mind comes at a price: any third-party module is effectively a pig in a poke. Each new dependency drags more dependencies, increasing your project’s size uncontrollably. Size is especially important when our package is intended to work both in a Node.js environment and in a browser.

When it comes to client-side code, we are fighting for bytes.

Moreover, even built-in Node.js modules can be a threat to your library’s size if you misuse them even slightly. We’ll see that in a moment.

What if we could easily find a source of those extra kilobytes and limit the size of our bundle to some sane value? This way we will impose some discipline on ourselves, one that will serve a greater good: the microcosm of our app will stay micro.

Size Limit embraces exactly this philosophy: a path to happiness through limitation. Let’s give it a try!

The incredible shrinking DIY library

We are going to write an extremely oversimplified version of Nano ID library, which is a tiny, secure, URL-friendly unique string ID generator. Our “clone” will only be able to generate a random number between 0 and 255. It will run in a browser and in Node.js too. Let’s call our project twofivefive because each library, even quite useless one, ought to have a name. You know the drill:

$ mkdir twofivefive
$ cd twofivefive
$ touch index.js
$ npm init

For now, we can just tap ‘Enter’ through all the questions npm init gives us. Make sure we have a package.json file, we will need it later. Now it is time to open index.js in your favorite editor and paste this code:

function randomNumber () {
  if (process.browser) {
    var crypto = global.crypto || global.msCrypto
    return crypto.getRandomValues(new Uint8Array(1))[0]
  } else {
    return require('crypto').randomBytes(1)[0]
  }
}
module.exports = randomNumber

In order to test this, let’s create a file named test.js in the same folder and put this inside:

var randomNumber = require('./')
console.log(randomNumber())
console.log(randomNumber())
console.log(randomNumber())

Now run node test.js in your console to see three random numbers.

Our code uses two different techniques to achieve randomness: one with the browser’s built-in Crypto interface, another with Node’s default crypto module. We use Node’s process.browser to switch between the two, depending on whether our library runs in backend or frontend. Simple enough. This code looks completely benign, but contains three mistakes, or rather slips, that will make our library much larger than necessary once it is included in a bundle.

Okay, we have typed 250 symbols worth of code. Wonder how much Webpack will actually yield? Let’s find out!

Size Limit to the rescue

Time to meet Size Limit. Here is what it does, according to project’s description: “To be really specific, Size Limit creates an empty webpack project in memory. Then, it adds your library as a dependency to the project and calculates the real cost of your libraries, including all dependencies, webpack’s polyfills for process, etc.” I know you can’t wait to see the result.

$ npm install size-limit --save-dev

Or:

$ yarn add --dev size-limit

Now we need to modify our package.json a bit. Add a new "size" key inside "scripts" and another one, named "size-limit", to the top level of our JSON. Here’s how it should look like afterward:

{
  "name": "twofivefive",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "size": "size-limit"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "size-limit": "^0.8.4"
  },
  "size-limit": [
    {
      "path": "./index.js"
    }
  ]
}

Now you can execute npm run size in the console. Better yet, upgrade your npm to the latest version with npm install npm@latest -g and run a brand new npx executable:

$ npx size-limit

Here is the output:

Package size: 94.67 KB
With all dependencies, minified and gzipped

Wait… What?! Why? Almost 100KB of minified and gzipped code just to pick a random number? Well, Size Limit knows how to answer your questions. Back to the console:

$ npx size-limit --why

A new browser page will open where you can marvel at all the dependencies included in the bundle when you added that require('crypto') line.

size-limit --why output

Wonder what secp256k1.js is…

Back to square one

Time to fix our mistakes. A not-so-well-known feature of webpack allows you to specify different files for the browser and Node.js. Meaning, if our library is going to be used in frontend, we won’t have to drag Node.js modules in at all. All we need is to split our index.js into two files: index.js and index.browser.js, like so:

// index.js
var crypto = require('crypto')
function randomNumber () {
  return crypto.randomBytes(1)[0]
}
module.exports = randomNumber

// index.browser.js
var crypto = global.crypto || global.msCrypto
function randomNumber () {
  return crypto.getRandomValues(new Uint8Array(1))[0]
}

Then, in our package.json we need to add a top-level "browser" key that will be recognized by Webpack once the time comes to build a bundle, so the server version will not be used at all.

{
  // ...
  "size-limit": [
    {
      "path": "./index.js"
    }
  ],
  "browser": {
    "./index.js": "./index.browser.js"
  }
}

Shall we try with Size Limit again? Sure!

$ npx size-limit

Result:

Package size: 92 B
With all dependencies, minified and gzipped

Whoa! 92 bytes versus 94.67 kilobytes. That is 1 000 times lighter.

Our --why output is now much easier on the eyes too:

size-limit --why output

What’s that global.js?

Turns out, after refactoring we keep referring to Webpack’s global object instead of browser’s window. Sure, it is just 136 extra bytes, but those are bytes we do not need. Let’s just use window directly, as we are sure that Webpack will serve only index.browser.js to the browser, and all browsers sure have windows. This way our bundler won’t have to generate a polyfill.

Last refactoring, I promise:

// Just replace 'global' with `window` in index.browser.js
var crypto = window.crypto || window.msCrypto
function randomNumber () {
  return crypto.getRandomValues(new Uint8Array(1))[0]
}

This code is also testable directly in the browser console. Let’s run Size Limit one last time. Here is the result:

Package size: 17 B
With all dependencies, minified and gzipped

Finally! Now any frontend code using randomNumber() calls will increase the bundle size just by seventeen bytes compared to ninety-four kilobytes in the first implementation. Sure, our Node.js version will still be heavier, but we don’t really care about it, do we? It is in the backend, so it will not be sent over to the client.

size-limit --why output

5000x gain! Actually, it’s /5000 gain!

Restricting ourselves

Now we know that we don’t really need more than 17B to make our library work in the browser. How do we keep it this way? Size Limit has a feature for that. We just need to add one extra line to our package.json: "limit" nested key inside "size-limit". Remember to give yourself some elbow room: your limit should be a bit larger than the reported size. Here is what our final package.json looks like:

{
  "name": "twofivefive",
  "version": "1.0.0",
  "description": "Demonstrating Size Limit",
  "main": "index.js",
  "scripts": {
    "test": "node test.js && size-limit",
    "size": "size-limit"
  },
  "author": "Your's truly",
  "license": "ISC",
  "devDependencies": {
    "size-limit": "^0.8.4"
  },
  "size-limit": [
    {
      "path": "./index.js",
      "limit": "20 B"
    }
  ],
  "browser": {
    "./index.js": "./index.browser.js"
  }
}

Now, if we try to change window back to global in our index.browser.js and run npm run test—it will fail with the following message:

Package has exceeded the size limit
Package size: 92 B
Size limit:   20 B
With all dependencies, minified and gzipped

Of course, we are not releasing our twofivefive as an OSS project. But if you already have one, or just mustering up the courage to start one, you can easily add Size Limit to your TravisCI config (or another CI tool of your choice) so it will run on each new pull request, making sure no one added extra 100 kilobytes of dependencies to your project by mistake.

But I don’t want to write a library! Will it work for my app?

Yes, it will! Say, you’re working on a frontend project that already uses Webpack. You can add another key to your package.json

"size-limit": [
  {
    "path": "public/app-*.js",
    "webpack": false,
    "limit": "300 KB"
  }
]

See that webpack: false part? That means Size Limit will not create a virtual Webpack bundle in memory for you but will work with the one you already have in your project. Then you can always be sure that the bundle you send to the client will never bloat out of proportions.

Size Limit in the wild

Some of the common programming slips we described:

  • Not leveraging "browser" key in package.json
  • Using global instead of window
  • Accidentally using a large dependency or unneeded polyfill

… are quite common in the wild. Here are some examples of pull requests to production libraries that fix some of these issues with the help of Size Limit:

You can do your part!

Adding Size Limit to your favorite project is just one pull request away. A pull request that changes less than 10 lines of code. Remember to be polite in the description! Here’s a good example.

Well-known projects that have already adopted Size Limit:

Size Limit was created by an OSS enthusiast for OSS enthusiasts. Thank you for supporting this tool and appreciating that in frontend Web size is what matters the most.

Join our email newsletter

Get all the new posts delivered directly to your inbox. Unsubscribe anytime.