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.
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:
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.
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 inpackage.json
- Using
global
instead ofwindow
- 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:
- Shave 100KB off of Logux-client by changing a dependency.
- Save 70B by changing
global
towindow
in Nano ID. - Make PostCSS 25% smaller by skipping terminal syntax highlighting in a browser.
- Reduce Browserslist size by a quarter by being more specific about what is
require()
‘d
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.