The cost of JavaScript

Count what a megabyte of JavaScript actually costs, and cut the part that runs before anything is on screen.

08of 16
≈ 13Min read
5Parts

Bytes are the cheap part

A 200 KB image and 200 KB of JavaScript cost the same to download and nothing alike after that. The image is decoded off the main thread and drawn. The JavaScript has to be parsed, compiled, executed, and then it usually allocates objects, builds a DOM and attaches listeners — all on the one thread that also has to answer the user.

This is why the same site feels instant on a laptop and broken on a phone. The download differs by a factor of two; the CPU differs by a factor of eight.

A rough exchange rate

These are order-of-magnitude figures from published measurements, not a law — but they are close enough to reason with, and they make the point immediately: 500 KB of boot JavaScript is about half a second of frozen main thread on a mid-range phone, before your code has done anything useful.

The honest version of the lesson: measure on a real device, or in DevTools with CPU throttling at 4× or 6×. Never judge JavaScript cost on the machine you wrote it on.

DeviceParse + compile + execute
Desktop / recent laptop≈ 0.25 ms per KB
Mid-range phone≈ 1 ms per KB
Low-end phone≈ 2 ms per KB

The question to ask of every module

Not "is this library big?" but "does this have to run before the first screen?". Most of a bundle is code for things the user has not done yet: a checkout flow, a chart, a date picker, an analytics SDK.

three ways to move code out of boot
/* on demand — the import happens when the route is reached */
const Charts = lazy(() => import("./charts.js"));

/* when the browser is idle */
requestIdleCallback(() => import("./analytics.js"));

/* on the first interaction that needs it */
button.addEventListener("click", async () => {
  const { openEditor } = await import("./editor.js");
  openEditor();
}, { once: true });

The cheapest dependency is none

Tree shaking helps only if the library is written for it — a CommonJS build, a side-effecting import, or a barrel file that re-exports everything will hand you the whole package. Check the built bundle, not the import statement.

Instead ofUse
A date library for one format callIntl.DateTimeFormat
A utility library for groupBy / uniqObject.groupBy, new Set(), Array.prototype.at
A carousel frameworkscroll-snap-type in CSS
A validation library on the clientConstraint validation attributes, plus the server check you needed anyway

Third parties are someone else’s main thread

A tag manager, a chat widget, a consent script and two analytics SDKs will out-weigh your application, and you control none of them. The rules: load them async or defer, never in the critical path, never synchronously in the head; give them a facade (a static image of the chat button that loads the real thing on click); and put a budget on them that fails the build.

And measure what they cost with them removed — the honest number, not the vendor’s.

Practice