A diet for dependencies
Weigh npm packages before you add them, use what the browser already has, and stop shipping polyfills your visitors don't need. Measured on a phone.
- 01Weigh a package firstRead Bundlephobia, weigh your own import line with esbuild, and measure what a package really adds to a page.
- 02Reach for the browser's ownSwap a carousel, a date picker, axios and uuid for what the browser already has, and know what you give up.
- 03Ship only what your visitors needRead a browser list, see which polyfills and rewrites it asks for, and spot legacy JavaScript in a build.
Meera's wishes come with packages
Meera has two new wishes for Leaf and Kettle. The first is for the tea list. At the top, she wants this month's teas in a strip. Asha can swipe through them, one tea at a time.
The second is for the checkout. Asha should pick the day her tea arrives. Then she taps Place order. The order goes to the shop's server, with a number of its own.
Ravi looks on npm, the big online shelf of JavaScript packages. Each wish has a popular package:
- react-slick for the strip of teas. A strip you swipe like this is called a carousel.
- react-datepicker for the delivery day.
- axios to send the order to the server.
- uuid to give each order a number no other order has.
A package your app needs is called a dependency: the app depends on it. Each of these is one npm install away. But Ravi remembers lesson 22.3. He asked lodash for one function, and the whole library came. So before he installs anything, he asks one question. What will each package cost Asha?
He tries them all. He builds the tea list with react-slick, and the checkout with the other three. He also builds both pages a second way, with what the browser already has. Then he loads each page on Asha's phone. Here is what each page sends, next to the 250 KB budget he set in lesson 22.1:
Each bar is one page's JavaScript, sent with Brotli: the bytes that travel, after the server packs the file. A KB is 1,024 bytes. Beside each bar is when the page could answer a tap. As in lesson 22.1, that's the later of two moments: the biggest thing on the page appearing, and the last long task ending. A long task keeps the phone busy for more than 50 ms.
Both pages stay under the budget. But the packages made the checkout almost three quarters heavier. Now look at the third bar on each page: the browser's own way. Those pages weigh about the same as today.
"Asha's phone" is the stand-in from lesson 22.1: Chrome on a phone's screen, DevTools' Slow 4G network, and a CPU 4 times slower. Here are the numbers behind the packages' bars:
// Asha's phone: each page's JavaScript (KB, sent with Brotli), and when it could answer a tap (ms).const today = { "Tea list": [111.1, 2267], Checkout: [84.0, 2083] };const withPackages = { "Tea list": [125.3, 2335], Checkout: [144.8, 2294] }; for (const page of Object.keys(today)) { const [kb, ms] = withPackages[page]; console.log(`${page}: ${(kb - today[page][0]).toFixed(1)} KB more, ready ${ms - today[page][1]} ms later`);}- Line 1 is a comment.
- Line 2 is each page today, as lesson 22.4 left it. Line 3 is each page with the packages.
- Lines 5 to 8 print
Tea list: 14.2 KB more, ready 68 ms laterandCheckout: 60.8 KB more, ready 211 ms later
Why it matters: a budget is a limit, not an allowance to spend. Every package costs Asha time. So weigh each one before you add it.
In a shop, you pick up a packet of biscuits. Before it goes in the basket, you turn it over. The label says how heavy it is, and what's inside. Sometimes the list of ingredients is long. And sometimes you already have the same thing at home.
- In real life: A packet on the shop's shelf
- In web performance: A package on npm
- In real life: The weight on the label
- In web performance: The bytes it adds, sent with Brotli
- In real life: The list of ingredients
- In web performance: The other packages it brings
- In real life: Food you already have at home
- In web performance: What the browser already has
Every package you add costs your visitors bytes and time. Weigh it before you add it. Use what the browser already has. And build only for the browsers your visitors really use. That's a diet for dependencies: less JavaScript in, the same page out.
The lesson follows Ravi, one question at a time:
- What does a package really weigh? Measuring it before you install it.
- Does the browser already do it? Lighter ways to make the same wish.
- What is a polyfill, and who asks for one? Polyfills and browser lists.
- What do old browsers cost new ones? Legacy JavaScript.
Weigh a package before you install it
Ravi starts with react-slick. He doesn't install it yet. First, he looks it up on Bundlephobia. It's a website that builds a package the way a bundler would. Then it tells you how big the result is. Here is what it says about react-slick:
// What Bundlephobia reports for react-slick 0.31.0, in bytes.const slick = { minified: 61816, gzipped: 15416, dependencies: 4, esModule: false }; console.log(`${(slick.gzipped / 1024).toFixed(1)} KB with gzip, ${slick.dependencies} dependencies`);console.log(slick.esModule ? "ES modules: it can be tree-shaken" : "CommonJS only: the bundler keeps all of it");- Line 1 is a comment. Line 2 holds four of Bundlephobia's facts.
minifiedis the size with spaces, comments and long names taken out.gzippedis that size packed with gzip, an older way to pack files than Brotli.dependenciescounts the packages react-slick needs.esModulesays whether it's written as ES modules.- Lines 4 and 5 print
15.1 KB with gzip, 4 dependenciesandCommonJS only: the bundler keeps all of it
The last line matters most. Lesson 22.3 showed why. A bundler can only leave out the unused parts of ES modules. react-slick is written as CommonJS, the older style, so it comes whole, as lodash did.
Bundlephobia gives a good first look. But it weighs the whole package. Ravi wants to weigh the line he would really write. So he writes a small script, measure.mjs:
import { build } from "esbuild";import { brotliCompressSync } from "node:zlib"; const result = await build({ stdin: { contents: process.argv[2], resolveDir: "." }, bundle: true, minify: true, format: "esm", write: false, external: ["react", "react-dom"],});const code = result.outputFiles[0].contents;console.log(`${code.length} bytes minified, ${brotliCompressSync(code).length} with Brotli`);- Lines 1 and 2 bring in esbuild, Ravi's bundler since lesson 22.2, and Node's Brotli packer.
- Lines 4 to 8 build one import line. Line 5 takes the line from the command, as if it were a file. esbuild calls a file given as text
stdin. - Line 6 bundles everything the line imports, and minifies it. It keeps the result in memory, instead of writing a file.
- Line 7 leaves out React and React DOM, because Ravi's pages load them already. react-slick lists them as its peer dependencies: packages it expects the app to have.
- Lines 9 and 10 print the size, minified and with Brotli.
Ravi runs it for two packages:
$ node measure.mjs 'import Slider from "react-slick"; console.log(Slider);'
65204 bytes minified, 14897 with Brotli
$ node measure.mjs 'import { v4 } from "uuid"; console.log(v4);'
789 bytes minified, 400 with BrotliLine 1 weighs react-slick. Line 2 says 14.5 KB with Brotli, close to Bundlephobia's 15.1 KB with gzip. Line 3 weighs the line Ravi would write for uuid. Line 4 says only 400 bytes.
But Bundlephobia says uuid weighs 3.8 KB with gzip. Why so different? uuid is written as ES modules. And its package.json says "sideEffects": false, the promise from lesson 22.3. So the bundler kept only v4, the one function Ravi imports. Bundlephobia also lists the size of each export. Its list says v4 alone is 520 bytes with gzip.
Why it matters: weigh the import line you will write, not the whole package.
There's a third way, and it's the truest. Build the feature into the app, and compare the page before and after. The course did it for every package. react-slick added 14.2 KB to the tea list, close to what measure.mjs said. Read each row across: the way, what it weighs, and what it can't see.
| The way | What it weighs | What it can't see |
|---|---|---|
| Bundlephobia | The whole package and what it brings, minified, then packed with gzip. | Which parts your import line uses, and what your app already has. |
| measure.mjs | Your own import line, bundled and minified by esbuild, then packed with Brotli. | What the rest of your app already has, beyond React. |
| Built into the app | What the page really sends, against the same page without it. | Nothing about the page. But you have to write the feature first. |
Bundlephobia counted 4 dependencies. But npm installed 5 packages with react-slick, because one dependency has one of its own. Here they are, as built into the tea list:
react-slick's own code is the big card. Each small card is a package Ravi never chose. One of them is a polyfill. You'll meet it again soon.
A printer is on sale, and the price looks small. At home, you find it needs its own ink, its own cable and its own paper tray. The real cost is the printer and everything it needs.
- In real life: The printer
- In web performance: react-slick's own code
- In real life: Its ink, its cable and its paper tray
- In web performance: The packages npm installs with it
- In real life: The price on the printer's box
- In web performance: The size of the package alone
- In real life: Everything you paid in the end
- In web performance: What the tea list really sends: 14.2 KB more
One more trap. What npm puts on Ravi's computer isn't what Asha downloads. npm puts packages in a folder called node_modules. npm install axios added 27 packages there. But Asha's checkout got code from just one of them: axios itself. A tool called pkg-size measures that other size: what a package adds to node_modules. It's about Ravi's disk, not Asha's phone.
Weigh all five packages yourself. One of them is Embla, a lighter carousel library Ravi also tried:
- Bundlephobia: the whole package60.4 KB minified, 15.1 KB with gzip
- Packages npm install brings5: classnames, json2mq, string-convert, lodash.debounce, resize-observer-polyfill
- Written asCommonJS only: the bundler keeps all of it
- measure.mjs: your import line63.7 KB minified, 14.5 KB with Brotli
- Built into the shop14.2 KB more for the tea list, with Brotli
Pick a package. The rows show what Bundlephobia reported, what npm installs with it, what measure.mjs printed for Ravi's import line, and what it really added to its page.
Use what the browser already has
Ravi looks at the four wishes again. Can the browser already do any of them? A group of developers called e18e keeps a list of packages that have lighter replacements. Its list says axios can be replaced by fetch(), and uuid by crypto.randomUUID(). Both are built into the browser.
You're going for a picnic. You could carry a folding table from home. It's heavy, and your arms ache by the time you arrive. But the park already has picnic tables. They're not the table you would pick. Still, they hold the sandwiches just as well.
- In real life: Your folding table from home
- In web performance: A package, like react-datepicker
- In real life: The park's own picnic table
- In web performance: What the browser has, like a date field
- In real life: Carrying the table there
- In web performance: Asha's phone downloading and running the package
- In real life: A table you didn't choose
- In web performance: A calendar the browser draws its own way
A strip of teas, with no JavaScript
The browser can make a strip you swipe without any script. It's called scroll snap. The strip scrolls sideways, and it always stops with one tea lined up. Here is the carousel as a plain list:
import { TEAS } from "./teas.js"; export function TeasOfTheMonth() { return ( <ul className="month"> {TEAS.slice(0, 4).map((tea) => <li key={tea.name}>{tea.name}</li>)} </ul> );}Line 1 brings in the shop's teas. Lines 5 to 7 draw the first four as a list. Two lines of CSS do the rest:
.month { display: flex; gap: 8px; overflow-x: auto; scroll-snap-type: x mandatory; }
.month li { flex: 0 0 85%; scroll-snap-align: start; }- Line 1 puts the teas in a row with
display: flex.overflow-x: autolets the row scroll sideways.scroll-snap-type: x mandatorymakes it stop at a tea every time. - Line 2 makes each tea 85% as wide as the strip.
scroll-snap-align: startlines up each tea's left edge.
A delivery day from the browser
Here is the delivery day with react-datepicker, and then with the browser's own date field:
import { useState } from "react";import DatePicker from "react-datepicker"; export function DeliveryDay() { const [day, setDay] = useState(null); return <DatePicker selected={day} onChange={setDay} placeholderText="Delivery day" />;}export function DeliveryDay() { return <input type="date" name="day" aria-label="Delivery day" />;}In the first, line 2 brings in react-datepicker. Line 5 keeps the day Asha picks, and line 6 draws the picker. In the second, line 2 is all it takes: <input type="date">. The browser draws the field and its calendar. MDN says the calendar's look depends on the browser and the system. And the value always comes out the same way: year, month, day.
Sending the order, and its number
Here are the two small files the checkout needs, first with packages:
// send-order.jsimport axios from "axios"; export function sendOrder(order) { return axios.post("/api/orders", order);} // order-id.jsimport { v4 as uuidv4 } from "uuid"; export const newOrderId = () => uuidv4();Line 5 sends the order with axios. Line 11 makes its number with uuid. Here are the same two files with what the browser has:
// send-order.jsexport function sendOrder(order) { return fetch("/api/orders", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(order), });} // order-id.jsexport const newOrderId = () => crypto.randomUUID();- Lines 3 to 7 send the order with
fetch(), the browser's own way to make a request. Line 4 asks for a POST, which sends data. - Line 5 tells the server the body is JSON. Line 6 turns the order into JSON text.
- Line 11 makes the number with
crypto.randomUUID(). It gives back a random ID in a standard form, called a UUID.
uuid itself knows about this. In its own source, v4() with no options just calls crypto.randomUUID(), if the browser has it.
What Asha's phone did
The course loaded each page three ways on Asha's phone. With scroll snap, the tea list sent 111.1 KB, the same as today. It could answer at 2,273 ms. With the browser's own ways, the checkout sent 84.1 KB, and answered at 2,129 ms. With the three packages, it answered at 2,294 ms.
So the packages cost the checkout 165 ms. Try each page and build:
- JavaScript sent with Brotli111.1 KB in 4 files
- JavaScript work on the main thread230 ms
- Could answer a tapat 2,267 ms
Pick a page and a build. The rows show what the page sent at once, how long its JavaScript kept the main thread busy, and when it could answer a tap.
The browser's own ways have limits, though. Read each row across: the wish, the browser's way, and what you give up.
| The wish | The browser's way | What you give up |
|---|---|---|
| A strip of teas to swipe | A list with CSS scroll snap | No dots or arrows, unless you add them. |
| A delivery day | <input type="date"> | Its calendar looks different in each browser and system. |
| Sending the order | fetch() | A server error doesn't reject the promise. You check response.ok yourself. |
| An order number | crypto.randomUUID() | It only works on pages served over HTTPS. |
When the browser's way isn't enough, pick a lighter package. Embla added 7.1 KB to the tea list: half of react-slick's 14.2 KB.
Why it matters: before you add a package, check whether the browser already does the job.
Polyfills, and the browser list that picks them
Look again at the packages react-slick brought. One is called resize-observer-polyfill. What is a polyfill?
A polyfill is code that fills a gap in an older browser. It checks whether the browser has a feature. If not, it adds the feature, written in JavaScript. This one is for ResizeObserver, a way to hear when an element changes size. Here are its last lines:
// resize-observer-polyfill 1.5.1, src/index.js, lines 4 to 11export default (() => { // Export existing implementation if available. if (typeof global.ResizeObserver !== 'undefined') { return global.ResizeObserver; } return ResizeObserverPolyfill;})();- Line 1 names the file, in the package's own source.
- Line 4 checks whether the browser already has ResizeObserver. If it does, line 5 hands back the browser's own.
- Line 8 hands back the polyfill only when the browser has none.
Chrome has had ResizeObserver since version 64, Firefox since 69, and Safari since 13.1. On Asha's phone, line 5 runs, and the polyfill is never used. But her phone still downloads it: 7.7 KB as built. It's like a booster seat that stays in the car after the children have grown up.
Polyfills from the build
Polyfills also come from the build itself. Two tools work together:
- core-js is a library of polyfills. Its README calls it "a modular standard library for JavaScript". Each feature has a small module of its own.
- Babel is a tool that rewrites JavaScript so that older browsers can run it. With a plugin, it also adds the core-js polyfills the code needs.
Here is a Babel setup many apps use:
{ "targets": "defaults", "presets": ["@babel/preset-env"], "plugins": [["polyfill-corejs3", { "method": "usage-global", "version": "3.51" }]]}- Line 2 names the browsers to build for. This is a browser list. A tool called Browserslist reads it.
- Line 3 is preset-env. It rewrites any syntax the browsers on the list can't run.
- Line 4 adds polyfills.
usage-globalmeans: for each feature the code uses that some browser on the list lacks, add its polyfill to the page. Babel's docs say it works like the olderuseBuiltIns: "usage"setting. Since Babel 8, this plugin is the way to add them.
A browser list is written as a query, in almost plain English. Three examples:
ie 11is one browser: Internet Explorer 11.defaultsis Browserslist's own choice. It takes browsers used by more than 0.5% of people, the last two versions of each browser, and Firefox's long-support version. Then it drops the browsers it calls dead: those with no updates for 24 months, like IE 11.baseline widely availablefollows Baseline. A feature is Baseline widely available once Chrome, Edge, Firefox and Safari have all had it for 30 months. This query picks the browser versions that have all those features.
You're cooking for a party, so you check the guest list. One guest can't eat nuts, so you make a dish without nuts too. But the list is old. That guest moved away years ago, and you still cook the extra dish every time.
- In real life: The guest list
- In web performance: The browser list, like defaults
- In real life: What each guest can't eat
- In web performance: What each browser lacks
- In real life: The extra dish
- In web performance: Polyfills and rewritten code
- In real life: The guest who moved away
- In web performance: A browser nobody uses any more, like IE 11
Ravi tries Babel on three lines of his own code:
const names = teas.map((tea) => tea.name);
const cheap = teas.filter((tea) => tea.price < 5);
const first = teas.at(0);Line 1 makes a list of the teas' names. Line 2 picks the teas under £5. Line 3 takes the first tea. Here is what Babel wrote for defaults:
import "core-js/modules/es.iterator.constructor.js";import "core-js/modules/es.iterator.filter.js";import "core-js/modules/es.iterator.map.js";const names = teas.map(tea => tea.name);const cheap = teas.filter(tea => tea.price < 5);const first = teas.at(0);- Lines 4 to 6 are Ravi's code, almost unchanged. The browsers Babel builds for can run it as it is.
- Lines 1 to 3 are new. They import polyfills for iterator helpers: methods like
mapandfilteron an iterator, an object that hands out values one at a time. Chrome 109, the oldest Chrome on the list, doesn't have them.
But teas is an array, not an iterator. Arrays have had map for many years. Babel can't tell which one teas is. It sees the word map, and adds the polyfill to be safe.
In the whole tea list, these guesses add up. React's scheduler is the part of React that decides when to run its work. It checks whether setImmediate exists, a function from Node.js and old Internet Explorer. Babel saw the name, and added a polyfill for it too.
Why it matters: Babel adds polyfills by the names in your code, not by what the code really needs. Try the three lists:
import "core-js/modules/es.iterator.constructor.js"; import "core-js/modules/es.iterator.filter.js"; import "core-js/modules/es.iterator.map.js"; const names = teas.map(tea => tea.name); const cheap = teas.filter(tea => tea.price < 5); const first = teas.at(0);
- Oldest browsers Babel builds forChrome 124, Firefox 125, Safari 17.4, Safari on iOS 17.4, Edge 124
- Polyfills Babel imports3
Pick a browser list. The box shows what Babel wrote for Ravi's three lines, with preset-env and the core-js polyfill plugin.
What old browsers cost new ones: legacy JavaScript
Then Meera asks a fair question. "Will the shop work on my mum's old laptop?" Ravi doesn't know what browser it has. To be safe, he could build for IE 11, the last Internet Explorer. What would that cost Asha?
For IE 11, Babel has more to do. In the playground above, pick ie 11. Lines 1 to 9 import nine polyfills. And Ravi's three lines changed. The arrow function (tea) => tea.name became function (tea) { return tea.name; }. And const became var.
Rewriting new syntax into older syntax is called transpiling. Shipping old syntax and polyfills to browsers that don't need them is called legacy JavaScript.
You text your friends in short forms, like "thx" and "c u l8r". Your grandma doesn't know them, so for her you spell every word out. Her message is much longer. Now imagine you sent the long one to everyone, just in case.
- In real life: Short forms your friends know
- In web performance: New syntax, like arrow functions
- In real life: Spelling it out for Grandma
- In web performance: Transpiling for IE 11
- In real life: A longer message
- In web performance: More bytes to send and to read
- In real life: Sending the long one to everyone
- In web performance: Shipping the IE 11 build to Asha's Chrome
The course built the whole tea list four ways. The first is Ravi's build today: esbuild alone. esbuild rewrites only syntax its target can't run. Its docs say it "does not automatically add polyfills". The other three went through Babel and core-js first, each for one list. Every build split the app in the same places, as lesson 22.4 did.
// The tea list's first load on Asha's phone, built four ways:// KB sent with Brotli, core-js polyfills inside, and when it could answer a tap (ms).const builds = [ ["esbuild alone", 111.1, 0, 2267], ["baseline widely available", 125.6, 22, 2421], ["defaults", 126.9, 27, 2404], ["ie 11", 174.6, 116, 2604],]; for (const [list, kb, polyfills, ms] of builds) console.log(`${list}: ${kb} KB, ${polyfills} polyfills, ready at ${ms} ms`);- Lines 1 and 2 are comments. Line 4 is esbuild alone. Lines 5 to 7 went through Babel for each list.
- Line 10 prints one line for each build, from
esbuild alone: 111.1 KB, 0 polyfills, ready at 2267 mstoie 11: 174.6 KB, 116 polyfills, ready at 2604 ms
Built for IE 11, the tea list sent 63.5 KB more than esbuild alone. Most of it was polyfills. Asha's phone could answer a tap 337 ms later. Even the modern list, baseline widely available, cost 14.5 KB and 154 ms. Babel can't see what the code needs, only the names it uses.
Why it matters: every visitor pays for the oldest browser on your list.
A check that finds it
Chrome's DevTools and Lighthouse have a check for this, called Legacy JavaScript. Its description says: "Polyfills and transforms enable older browsers to use new JavaScript features. However, many aren't necessary for modern browsers." It flags a script with more than about 5 KB of them, for features that Baseline calls widely available.
The course ran the check's own code on each build. In the IE 11 build, it named 31 polyfills and 3 of Babel's rewrites. In the defaults build, it found nothing, though that build held 27 polyfills. Its list only covers features every browser has had for years. So a clean result doesn't mean no polyfills.
IE 11 can't run this page anyway
There's a twist. Since lesson 22.4, Ravi's page loads its code like this: <script type="module" src="app.js">. That's a module script. MDN's data says IE 11 never ran module scripts. To IE 11, the tea list has no script at all.
So the IE 11 build helps nobody. Every byte of it lands on browsers that don't need it. Try the builds yourself. The fifth build did the IE 11 rewrites with no polyfills:
- Oldest browserswhatever runs ES2022
- Sent with Brotli111.1 KB
- core-js polyfills inside0
- The Legacy JavaScript check findsnothing
- JavaScript work on the main thread230 ms
- Could answer a tapat 2,267 ms
Pick a build. The rows show its oldest browsers, what the tea list sent at once, and its polyfills. Then what the Legacy JavaScript check found, and what Asha's phone did with it.
Which list, then?
The honest answer is in the shop's own visitor data, as in Field data from every browser. Browserslist can read it too. A query like > 0.5% in my stats picks the browsers above 0.5% of the shop's own visits, from a file made from its analytics.
Beware of having no list at all. Babel's docs say that Babel 7 then built for all browsers. Babel 8 builds for defaults.
Ravi keeps esbuild alone for now. If he ever uses a feature some visitors lack, he'll add that one polyfill by hand. And he tells Meera the truth: he'll check which browsers the shop's visitors really use.
Why it matters: build for the browsers your visitors have, not the ones they had years ago.
The browser's own, a package, or nothing?
Here are jobs a shop's app might have. Sort each one by what it should use:
- A field to pick the delivery day
- Sending the order to the shop's server as JSON
- A random number for each new order
- Writing "3 days ago" under a review
- A review editor with bold, lists and links
- Turning reviews written in Markdown into HTML
- core-js polyfills for IE 11, in an app loaded with type="module"
- A ResizeObserver polyfill, for a list that starts at Chrome 109
Sort each job by what it should use.
Common mix-ups
- "Bundlephobia's size is what my page will send." No. It weighs the whole package. uuid was 3.8 KB with gzip there, and added 0.4 KB to the checkout.
- "What npm installs is what visitors download." No. axios added 27 packages to node_modules. Asha's checkout got code from one of them.
- "A polyfill only costs old browsers." No. Every visitor downloads it. The IE 11 build sent Asha's Chrome 63.5 KB more, for a browser that never runs the page.
- "defaults means only the newest browsers." No. Babel read it as reaching back to Chrome 109, and added 27 polyfills to the tea list.
- "If the Legacy JavaScript check passes, there are no polyfills." No. The defaults build passed with 27 polyfills inside.
- "esbuild adds polyfills for its target." No. Its docs say the target is "only concerned with syntax features, not APIs".
Practice exercises
Answer each one first, then open the hints if you need them.
What does this print? The string is random, but these two parts never change.
const id = crypto.randomUUID();
console.log(id.length, id[14]);36 4. randomUUID gives back a 36-character string, like 36b8f84d-df4e-4d49-b662-bcde71a8764f. Index 14 is the first character of the third group: the version, 4.
Ravi swapped axios for fetch. Here the server is down. What does the code print?
// A stand-in for fetch(): the shop's server answers 503, "Service Unavailable".
const fetch = async () => ({ ok: false, status: 503 });
fetch("/api/orders", { method: "POST" }).then(
(response) => console.log(`Placed: ${response.status}`),
() => console.log("Failed"),
);const response = await fetch("/api/orders", { method: "POST" });
if (!response.ok) throw new Error(`The server said ${response.status}`);Placed: 503. A fetch() promise doesn't reject for an error status, so the first function runs. Check response.ok yourself. axios rejects for any status outside 200 to 299, so this is one thing to add when you swap.
How many packages does npm install with react-datepicker, not counting react-datepicker itself?
8. Bundlephobia counts its 3 direct dependencies. They bring 5 more, like tabbable and the @floating-ui packages.
Built for IE 11 with no polyfills, how many KB did the tea list send at once, with Brotli?
123.4 KB. The rewrites alone added 12.3 KB to the tea list. The polyfills added the other 51.2 KB of the IE 11 build's extra 63.5 KB.
Open the copy of the tea list built for IE 11 in Chrome, with DevTools' Network panel open. How many JavaScript files does it load before you tap anything?
8. Built with esbuild alone, the tea list loads 4. Babel's polyfills from core-js land in shared files of their own, so the IE 11 build loads 4 more.
Which command prints the browsers that your project's browser list picks?
npx browserslistnpx browserslist. Run it in the project's folder, and it prints every browser version your list picks. Run it whenever the list changes.
Check your understanding
For each question, ask: what does Asha's phone really receive, and does it need it?
Question 1 of 7Bundlephobia's size for uuid was far bigger than what
import { v4 } from "uuid"added to the checkout. Why?Choose an answer to see the explanation.
Question 2 of 7npm install axios put many packages into node_modules. Which of them sent code to Asha's checkout?
Choose an answer to see the explanation.
Question 3 of 7What does esbuild's
--target=chrome109do to Ravi's code?Choose an answer to see the explanation.
Question 4 of 7What does this check print, in a browser from this year?
Read the code, then predictconst fill = () => console.log("The polyfill ran"); if (typeof Array.prototype.at !== "function") fill(); console.log([3, 4, 5].at(-1));Choose an answer to see the explanation.
Question 5 of 7The tea list built for
defaultsheld many core-js polyfills, yet the Legacy JavaScript check found nothing. Why?Choose an answer to see the explanation.
Question 6 of 7Ravi's page loads app.js with
<script type="module">. Why is anie 11browser list pure waste for it?Choose an answer to see the explanation.
Question 7 of 7Which change gives the checkout a delivery day with no JavaScript package at all?
Choose an answer to see the explanation.
Key takeaways
- Every package costs your visitors bytes and time. A budget is a limit, not an allowance.
- Weigh a package before you install it: Bundlephobia first, then your own import line, then the page itself.
- CommonJS packages come whole. ES modules with no side effects give you only what you import.
- The browser already has a lot: scroll snap,
<input type="date">,fetch()andcrypto.randomUUID(). - A polyfill fills a gap in older browsers, but every visitor downloads it. Babel adds them by name, not by need.
- Build for the browsers your visitors really use. A stale list costs everyone.
Remember the one-liner.
Weigh it, swap it for the browser's own if you can, and build only for the browsers your visitors have.
Sources
The facts on this page come from these docs, tools and source code:
- Bundlephobia, Shubham Kanodia.
- package-build-stats: README, Shubham Kanodia.
- esbuild API: Build, Evan Wallace.
- react-slick: README, Kiran Abburi.
- pkg-size: README, Hiroki Osame.
- module-replacements, e18e.
- Replacements: Fetch-based HTTP clients, e18e.
- Replacements for uuidv4, e18e.
- scroll-snap-type, MDN.
- <input type="date">, MDN.
- Window: fetch() method, MDN.
- axios 1.21.1: lib/defaults/index.js, validateStatus, axios.
- Crypto: randomUUID() method, MDN.
- uuid 14.0.3: src/v4.ts, uuidjs.
- How to think about Baseline and polyfills, web.dev.
- resize-observer-polyfill 1.5.1: src/index.js, Denis Rul.
- core-js: README, Denis Pushkarev.
- @babel/preset-env, Babel.
- Upgrade to Babel 8, Babel.
- babel-polyfills: Usage, Babel.
- Browserslist: README, Andrey Sitnik.
- Use Baseline with Browserslist, web.dev.
- React 19.3.1: Scheduler.js, setImmediate, Meta.
- esbuild API: Target, Evan Wallace.
- esbuild content types: ES5 is not supported well, Evan Wallace.
- Legacy JavaScript insight, Chrome for Developers.
- DevTools: LegacyJavaScript.ts, Chromium.
- Lighthouse 13.6.0: polyfill-module-data.json, Google.
- <script>: the nomodule attribute, MDN.
Coming next: Minification and build output. Ravi's pages send less now. Next, look inside the files the build writes.