completefrontend
Open lab My progress
THE WEB PERFORMANCE FIELD GUIDE

The cost of JavaScript

A kilobyte of JavaScript costs far more than a kilobyte of image: download, parse, compile, and run. Measured in Chrome: a script against a picture of the same size, three bundle sizes at three CPU speeds, and code caching across three visits.

By the end, you can
  • 01
    Follow a script's bytesSay what the browser does with a script after it arrives, and which of that work lands on the main thread.
  • 02
    Keep compiled code cachedExplain V8's cold, warm and hot runs, and what makes a visitor start again from cold.
  • 03
    Estimate the cost on a phoneWork out main-thread time for each kilobyte, see how a slower CPU makes it grow, and load less at start-up.

A kilobyte isn't a kilobyte

Asha opens the Leaf and Kettle page on her phone, on the bus. Say the shop adds two things to its page. One is a photo of the counter. The other is a small app for ordering tea. The photo is about 300 KB. So is the app's script.

Same size, so same cost? On the network, yes. After that, not at all. To see why, the course built two test pages and loaded each one in Chrome:

  • One page loads a made-up app's 300 KB script. It's full of small widget classes, like a component library. When it starts, it draws every widget once, the way a framework draws a page.
  • The other page shows one picture of almost exactly the same size: a PNG of random colors.

Chrome loaded both as if it were a phone. Every request waited one 150 ms round trip: the time for a message to reach the server and an answer to come back. Then the bytes arrived at 1.6 Mbps. That's the phone network Lighthouse uses. Chrome also ran the CPU 4 times slower than normal. First, the two downloads:

Two downloads of the same sizePop out in the code editor (opens in a new tab)JavaScript
// Two files of almost the same size, over a phone-like network.const script = { bytes: 307718, downloadMs: 1536.8 };const picture = { bytes: 307678, downloadMs: 1537.5 }; console.log(`The script took ${script.downloadMs} ms to download.`);console.log(`The picture took ${picture.downloadMs} ms to download.`);
  • Line 1 is a comment.
  • Lines 2 and 3 hold each file's size in bytes, and how long it took to download in milliseconds (ms). A millisecond is a thousandth of a second.
  • Lines 5 and 6 print The script took 1536.8 ms to download. and The picture took 1537.5 ms to download.

Same size, same wait: about a second and a half each. On the network, a byte is a byte. Now look at what happened once the files had arrived. This is how long each page kept the main thread busy:

What each file cost after it arrivedPop out in the code editor (opens in a new tab)JavaScript
// How long each page kept the main thread busy, in ms, with the CPU 4 times slower.const scriptPage = { mainThreadMs: 171.1, javascriptMs: 121.1 };const picturePage = { mainThreadMs: 77.5, javascriptMs: 0 }; console.log(`The script's page: ${scriptPage.mainThreadMs} ms, ${scriptPage.javascriptMs} ms of it JavaScript.`);console.log(`The picture's page: ${picturePage.mainThreadMs} ms, ${picturePage.javascriptMs} ms of it JavaScript.`);const extra = scriptPage.mainThreadMs - picturePage.mainThreadMs;console.log(`The script's page needed ${extra.toFixed(1)} ms more.`);
  • Line 1 is a comment. The main thread is the head cook from lesson 6.1. It runs your JavaScript and works out styles, layout and paint, one job at a time.
  • Line 2 holds the script page's recorded times. The main thread was busy for 171.1 ms, and 121.1 ms of that was JavaScript. Line 3 holds the picture page's: 77.5 ms, with no JavaScript at all.
  • Lines 5 and 6 print The script's page: 171.1 ms, 121.1 ms of it JavaScript. and The picture's page: 77.5 ms, 0 ms of it JavaScript.
  • Line 7 subtracts one from the other. Line 8 prints The script's page needed 93.6 ms more.

And the picture? Before the browser can show it, it has to decode it: unpack the file into the color of each pixel. Decoding the picture took 0.8 ms, all of it on worker threads. Worker threads are the helpers in the back of the kitchen from lesson 6.1. They work beside the main thread, so the main thread stayed free.

So the downloads cost the same. Then the script cost the main thread about 94 ms more. Why? A picture is ready once it's decoded. A script still has to be read, turned into steps the browser can follow, and then run:

What happens to the bytes. A script is downloaded, parsed, compiled and executed. A picture is downloaded and decoded.app.jsdownloadparsecompileexecutephoto.pngdownloaddecode
Both files start with the same download. Then the script has three more steps before it does anything. The picture has one short step.
Real-life analogyA flat-pack wardrobe and a framed picture

Two parcels arrive at your door, the same size and the same weight. One is a framed picture: you unwrap it and hang it in a minute. The other is a flat-pack wardrobe. You read the instructions, work out the steps, and build it. That takes the whole evening.

In real life: The delivery van, the same for both
In the browser: The download
In real life: Unwrapping the picture
In the browser: Decoding an image
In real life: Reading the wardrobe's instructions
In the browser: Parsing the script
In real life: Working out the steps
In the browser: Compiling it
In real life: Building the wardrobe
In the browser: Executing it
The main idea

You pay for a script twice. First its bytes are downloaded, like any file. Then the browser has to parse, compile and run them, and much of that work lands on the main thread. A picture of the same size pays for the download and a short decode.

Addy Osmani put it this way in 2018: "Byte-for-byte, JavaScript is still the most expensive resource we send to mobile phones, because it can delay interactivity in large ways." Here's why. Lesson 6.2 showed that the main thread does one task at a time. While it runs a script, a tap has to wait, and so does the next frame.

The rest of this lesson takes the script's cost apart:

  • what parse, compile and execute mean, and which thread does each;
  • how Chrome keeps compiled code between visits;
  • why a slower phone pays more;
  • how the cost grows with the size of the script, and how to load less at the start.

The JavaScript lessons Lazy parsing & preparsing and Startup: snapshots & code caching explain how V8 does this work. This lesson measures what it costs in Chrome.

Download, parse, compile, execute

Back to the wardrobe. As a script's bytes arrive, Chrome hands them to V8, its JavaScript engine. An engine is the part of a browser that runs JavaScript. V8 has three jobs to do:

  • Parse: read the code's text and build a tree of its parts: functions, variables, calls.
  • Compile: turn that tree into bytecode. Bytecode is a list of short, simple instructions that V8 can run one after another.
  • Execute: run the bytecode. For the test app, that means building every widget.

Along the way, V8 also does garbage collection. It clears out memory the code no longer uses.

Worker threads read the script while it downloads

Which thread does all this? Think of the worker threads again, the helpers in the back of the kitchen. They can't touch the page, but they can read a script. V8 uses them for streaming: it parses and compiles a script file on a worker thread while the file is still downloading.

For most connection speeds, V8 reads faster than the bytes arrive. Its 2019 article says it is "done parsing+compiling a few milliseconds after the last script bytes are downloaded."

The test pages show this. Over the phone network, V8's streaming task stayed open on a worker for the whole 1.5 second download. Most of that time it was waiting for bytes. It parsed for 19 ms in all.

Functions left for later are compiled on the main thread

So the main thread is free? Not quite. The script's top level, the code that isn't inside any function, has to be compiled before anything runs. But a big script holds many functions, and a page may never call some of them. So for each function, V8 chooses. It can compile it now. Or it can give it a quick read, to check the code is valid, and leave it for later. This quick read is called preparsing.

When a function that was left for later is called for the first time, V8 parses it fully and compiles it, right then. That happens on the main thread, in the middle of running the script. V8's article on compile hints says why that hurts: "the main thread cannot proceed until the function is compiled."

Real-life analogySkimming a new recipe book

A new recipe book arrives at the café. While the head cook works, a helper skims it page by page and checks nothing is missing. Later, the first order comes in for a dish from the book. Now the cook has to stop and read that recipe properly before she can start. The guest waits while she reads.

In real life: The helper skimming the book as it arrives
In the browser: A worker thread streaming the script
In real life: A quick look to check no page is missing
In the browser: Preparsing a function
In real life: The first order for a dish from the book
In the browser: The first call to that function
In real life: The cook stopping to read the recipe properly
In the browser: Compiling it on the main thread
Who works on a script file, over time. While the bytes download, a worker thread parses and compiles the top level of the script. After the last byte arrives, the main thread runs the script. Each time it calls a function for the first time, it stops to compile that function, then runs on.networkworker threadmain threaddownloadparse + compileruncompileruncompilerunthe last byte arrives
A worker thread reads the script while it downloads. Afterwards, the main thread runs it, and stops to compile each function the first time it's called.

How much landed on the main thread for the test app? The course served the 300 KB script from the same computer, so the download took no time. The CPU was 4 times slower again. Here is the main thread's share:

The main thread's work on the scriptPop out in the code editor (opens in a new tab)JavaScript
// The 300 KB script's work on the main thread, in ms, with the CPU 4 times slower.const mainThread = { parse: 18.1, compile: 46.8, execute: 52.7, gc: 3.4 }; const total = mainThread.parse + mainThread.compile + mainThread.execute + mainThread.gc;const compiling = mainThread.parse + mainThread.compile;console.log(`The main thread spent ${total.toFixed(1)} ms on the script.`);console.log(`Parsing and compiling took ${Math.round((compiling / total) * 100)}% of it.`);
  • Line 1 is a comment. Line 2 holds the recorded time for each kind of work. gc is garbage collection.
  • Line 4 adds all four together. Line 5 adds only parsing and compiling.
  • Line 6 prints The main thread spent 121.0 ms on the script.
  • Line 7 prints Parsing and compiling took 54% of it.

And here is what the worker threads did in the same load:

The worker threads' work on the scriptPop out in the code editor (opens in a new tab)JavaScript
// The same load: the script's work on the worker threads, in ms.const workers = { parse: 2, compile: 2.1 }; console.log(`Worker threads parsed and compiled for ${(workers.parse + workers.compile).toFixed(1)} ms.`);
  • Line 2 holds the recorded parsing and compiling on the worker threads.
  • Line 4 adds them up and prints Worker threads parsed and compiled for 4.1 ms.

So more than half of the main thread's time went on parsing and compiling, and the workers did very little. Why? The app draws every widget as it starts. That calls each widget's constructor and its render method, so most of the script's functions run at once. Each one was compiled on the main thread the first time it was called.

Why it matters: streaming only helps with the code V8 compiles up front. If your start-up calls most of your functions, their compiling still lands on the main thread while the person waits.

Read this table across. Each row is one step: what happens, and where it ran for the test app's script.

Four steps, and where they ran
The stepWhat happensWhere it ran
DownloadThe file's bytes travel from the server to the browser.On the network. No JavaScript work happens yet.
ParseV8 reads the code and builds a tree of its parts.The top level on a worker thread, while the file streamed in. Each function left for later on the main thread, when it was first called.
CompileV8 turns that tree into bytecode, the instructions it runs.The same threads as parsing, at the same moments.
ExecuteV8 runs the bytecode. Here, it builds every widget.On the main thread.

Seeing compile time from the page

A page can measure some of this for real visitors. Lesson 6.2 met long tasks. A long animation frame is a frame that was held up for more than 50 ms. The Long Animation Frames API reports each one, with a list of the scripts that ran in it.

Compile time in the fieldJavaScript
new PerformanceObserver((list) => {  for (const frame of list.getEntries()) {    for (const script of frame.scripts) {      const compiling = script.executionStart - script.startTime;      console.log(script.sourceURL, `compiling ${compiling.toFixed(1)} ms`, `in all ${script.duration.toFixed(1)} ms`);    }  }}).observe({ type: "long-animation-frame", buffered: true });
  • Line 1 makes a PerformanceObserver: an object the browser calls with new performance entries.
  • Line 8 asks it for long animation frames, including any from before it started (buffered: true).
  • Lines 2 and 3 go through each long frame, and each script in that frame.
  • Line 4 subtracts the script's startTime from its executionStart. MDN describes executionStart as "the time when the script compilation finished and execution started". So the gap is the compiling done before the script began to run.
  • Line 5 prints the script's address, that gap, and how long the script took in all.

For the 300 KB script, the page saw compiling end 0.4 ms after the script's entry started. The script took 120.1 ms in all. Only 0.4 ms of compiling? Streaming had already compiled the top level. The functions compiled later, while the script ran, count as running.

Code caching

Asha likes the shop. She comes back the next day, and the day after. Does V8 compile the same script from scratch every time?

No. Chrome can keep the compiled code and use it again. This is called code caching. It builds on the HTTP cache, where the browser keeps copies of files it downloaded before.

Real-life analogyA recipe the cook has made before

The first time a cook makes a new dish, she works out every step from the recipe card. The second time, she works it out again, and writes the steps down in her own short notes. From then on, she reads her notes and starts cooking at once. If the café swaps in a new card, she starts again from the beginning.

In real life: The recipe card, kept in a drawer
In the browser: The script, kept in the HTTP cache
In real life: Working out the steps
In the browser: Compiling
In real life: Writing her own notes, the second time
In the browser: The warm run: saving the compiled code with the file
In real life: Cooking straight from her notes
In the browser: The hot run: reading the saved code instead of compiling
In real life: A brand-new recipe card
In the browser: A new script URL, which starts again from cold

V8's Leszek Swirski describes three runs of the same script:

  1. The cold run, the first visit: "Chrome downloads it and gives it to V8 to compile." Chrome also keeps the file in the HTTP cache.
  2. The warm run, the second visit. Chrome takes the file from the cache, and V8 compiles it again. This time, "the compiled code is serialized, and is attached to the cached script file as metadata." Serialized means turned into bytes that can be saved.
  3. The hot run, the third visit: "V8 deserializes the metadata and can skip compilation." It reads the saved code back instead of compiling.
V8's three runs of a script. Cold: from the network, compiled, run. Warm: from the HTTP cache, compiled, run, and its compiled code saved with the file. Hot: from the HTTP cache with its compiled code, which is read instead of compiling, then run.coldnetworkcompilerunwarmHTTP cachecompilerunsave codehotHTTP cacheread codecompilerun
The second visit compiles again and saves the compiled code. The third reads that code instead of compiling: the crossed-out step is skipped.

The course loaded the 300 KB script three times in a row. Each time, it started Chrome afresh with the same profile, the way a person comes back another day. The CPU was 4 times slower:

Three visits in a rowPop out in the code editor (opens in a new tab)JavaScript
// The 300 KB script on three visits in a row, with the CPU 4 times slower.// Each visit's work on the main thread, in ms. cache: saving or reading compiled code.const visits = {  cold: { parse: 20.4, compile: 48.6, cache: 0, execute: 49.9, gc: 5.4 },  warm: { parse: 0, compile: 5.9, cache: 24.9, execute: 37.7, gc: 3.7 },  hot: { parse: 0, compile: 0.8, cache: 1.1, execute: 27.2, gc: 4.8 },}; for (const [visit, work] of Object.entries(visits)) {  const total = Object.values(work).reduce((sum, ms) => sum + ms, 0);  const compiling = work.parse + work.compile;  console.log(`The ${visit} visit: ${total.toFixed(1)} ms, ${compiling.toFixed(1)} ms of it parsing and compiling.`);}
  • Lines 1 and 2 are comments. cache is time spent saving or reading compiled code.
  • Lines 4 to 6 hold each visit's recorded work on the main thread.
  • Line 10 adds up each visit's work. Line 11 adds its parsing and compiling.
  • Line 12 prints The cold visit: 124.3 ms, 69.0 ms of it parsing and compiling. Then The warm visit: 72.2 ms, 5.9 ms of it parsing and compiling. and The hot visit: 33.9 ms, 0.8 ms of it parsing and compiling.

The hot visit is the reward. Parsing and compiling fell from 69.0 ms to 0.8 ms. Instead, V8 read 957,520 bytes of saved code. That's about three times the size of the script itself. The saved code also holds the functions compiled while the script ran, because Chrome saves it only after the script has run.

The warm visit pays to save

The warm visit is a surprise. It compiled again, yet its main thread compiled much less than the cold visit's did. Here is where its time went:

Inside the warm visitPop out in the code editor (opens in a new tab)JavaScript
// The warm visit, in ms: where it compiled, and what saving the code cost.const warm = { compileOnMainThread: 5.9, compileOnWorkers: 10.6, savingTheCode: 24.9 }; console.log(`Compiling on the main thread: ${warm.compileOnMainThread} ms`);console.log(`Compiling on worker threads: ${warm.compileOnWorkers} ms`);console.log(`Saving the compiled code: ${warm.savingTheCode} ms`);
  • Line 2 holds three recorded times from the warm visit.
  • Lines 4 to 6 print Compiling on the main thread: 5.9 ms, Compiling on worker threads: 10.6 ms and Saving the compiled code: 24.9 ms.

Most of the compiling moved to worker threads. Chromium's source points to a likely reason. During a visit, Chrome notes which functions were compiled, and saves that list with the cached file. These notes are called local compile hints. On the next visit, V8 can compile those functions up front, on a worker, while the script streams in.

Then saving the code cache cost the main thread 24.9 ms. That's the warm visit's price. The hot visit, and every visit after it, gets the work back.

What sends a visitor back to cold

Code caching works without any help from you. But a few things throw the saved code away, or stop it being made:

  • A new URL. V8 says changing a script's URL, "including any query parameters!", creates "a new cold cache entry." So app.js?v=2 starts cold.
  • New code. Code you just deployed isn't cached yet. When the server sends a changed file, Chrome clears the old code cache.
  • A long gap. V8 says code caching starts only if the first two visits happen within 72 hours.
  • A tiny file. Chrome doesn't cache code for scripts with less than 1 KiB (1,024 bytes) of source.
  • An inline script. A script written inside the HTML has no file of its own. Chrome attaches its cache to the HTML page instead, so any change to the page throws it away.

Why it matters: each deploy sends every visitor back to the cold run. V8 suggests putting code that rarely changes, like libraries, in a file of its own. Then it stays cached when your own code changes.

A fast phone and a slow one

Asha's phone is fairly new. Her friend Ravi's is older and cheaper. They open the same page and download the same script. Does it cost them the same?

No. Running code is work for the CPU, the phone's processor. V8's 2019 article says it plainly: "execution is CPU-bound". A slower CPU takes longer over every step. V8 timed Reddit's JavaScript on three phones:

  • A median phone, a Moto G4, took 3 to 4 times as long as a high-end one, a Pixel 3.
  • A low-end phone, an Alcatel 1X, took over 6 times as long.
Real-life analogyThe same recipe in a smaller kitchen

Two cafés cook from the same recipe card. One has a big kitchen, a hot stove and sharp knives. The other has one cook with no helper, a small stove and a blunt knife. The same dish takes four times as long there, and the card hasn't changed at all.

In real life: The same recipe card
In the browser: The same script, byte for byte
In real life: A big kitchen, a hot stove, sharp knives
In the browser: A fast laptop or a high-end phone
In real life: One cook, a small stove, a blunt knife
In the browser: A cheaper phone with a slower CPU
In real life: Every step taking longer
In the browser: Parsing, compiling and executing all taking longer

You can't test on every phone, but you can slow your own CPU down. Lighthouse runs the CPU 4 times slower by default, to stand in for a mid-tier phone. The course did the same on its Apple M1 Pro computer, at three speeds:

One script, three CPU speedsPop out in the code editor (opens in a new tab)JavaScript
// The 300 KB script's JavaScript on the main thread, in ms.const fullSpeed = 27.7;const slower = { 4: 121, 6: 187.2 }; for (const [slowdown, ms] of Object.entries(slower)) {  console.log(`With the CPU ${slowdown} times slower: ${ms} ms, ${(ms / fullSpeed).toFixed(1)} times as long.`);}
  • Line 1 is a comment.
  • Line 2 holds the 300 KB script's recorded JavaScript on the main thread at full speed. Line 3 holds it with the CPU 4 and 6 times slower.
  • Line 6 divides each time by the full-speed time. It prints With the CPU 4 times slower: 121 ms, 4.4 times as long. and With the CPU 6 times slower: 187.2 ms, 6.8 times as long.

The main thread's time grew with the slowdown. The worker threads' parsing and compiling didn't: it stayed between 4.1 and 6.7 ms at every speed. The slowdown landed on the main thread.

One warning. DevTools says "Throttling is relative to your computer's capabilities." Four times slower than a fast laptop isn't the same as four times slower than an old one. And DevTools "can't truly simulate the CPUs of mobile devices", because phones are built very differently from laptops.

Why it matters: a slowdown gives you a feel for a phone, not the real thing. Ravi's phone may be slower still. Test on a real, cheaper phone when you can.

Main-thread time per kilobyte

So far, one script of 300 KB. What happens as an app grows? The course built the same kind of app in three sizes: 100 KB, 300 KB and 1000 KB. A bigger one simply has more widgets. Each one stands in for a bundle: a single script file that a build tool makes by joining many smaller files of code. Here they are with the CPU 4 times slower:

Three sizes of the same appPop out in the code editor (opens in a new tab)JavaScript
// Three sizes of the same app, with the CPU 4 times slower.const bundles = {  "100 KB": { bytes: 102920, ms: 44.4 },  "300 KB": { bytes: 307718, ms: 121 },  "1000 KB": { bytes: 1025948, ms: 374.6 },}; for (const [name, { bytes, ms }] of Object.entries(bundles)) {  const kilobytes = bytes / 1000;  console.log(`The ${name} bundle: ${ms} ms, or ${(ms / kilobytes).toFixed(2)} ms for each KB.`);}
  • Lines 3 to 5 hold each bundle's size in bytes, and its recorded JavaScript on the main thread.
  • Line 9 turns bytes into kilobytes. Here a kilobyte (KB) is 1,000 bytes.
  • Line 10 divides the time by the kilobytes. It prints The 100 KB bundle: 44.4 ms, or 0.43 ms for each KB., The 300 KB bundle: 121 ms, or 0.39 ms for each KB. and The 1000 KB bundle: 374.6 ms, or 0.37 ms for each KB.

Each kilobyte cost about the same: roughly 0.4 ms of main thread. So the cost grew about in step with the size. That figure belongs to this app. A script that does more as it starts costs more for each kilobyte, and one that does less costs less. Measure your own.

Why it matters: the biggest bundle ran 374.6 ms of JavaScript on the main thread as the page started. A tap in that time has to wait. Compare every size and speed the course recorded:

Playground: bundle size and CPU speed
Main thread0 to 250 ms
300 KB, 4x slowerrecorded
Main-thread JavaScript121.0 ms
Each KB0.39 ms
Main thread busy153.4 ms
Parse and compile on workers4.1 ms
Try it yourself
The cost

The 300 KB bundle is 307,718 bytes, with 668 widget classes. With the CPU 4 times slower, it ran 121.0 ms of JavaScript on the main thread. That's 0.39 ms for each KB, and 4.4 times as long as at full speed.

Recorded in Chrome, from the same computer, so the download takes no time. Each bar is one kind of JavaScript work from the trace. Each recording is the middle one of five loads.

Or run a bundle in your own browser. Each button builds a bundle as a string of code. new Function turns that string into a function, and the browser has to parse the code to do it. Then the button runs the function. Line 4 of the panel's code is the first timed step, and line 6 is the second.

Playground: run a bundle in your browser
What each button does, shortenedJavaScript
// Build the bundle; return its size instead of writing to the page.const source = bundle(300).replace("document.body.dataset.size = String(html.length);", "return html.length;");const started = performance.now();const app = new Function(`// run ${run}\n${source}`);const created = performance.now();app();const finished = performance.now();console.log(created - started, finished - created);
Read, then runlive
100 KBnot yet
300 KBnot yet
1000 KBnot yet
Try it yourself
Your browser

Each button builds a bundle, has your browser read it with new Function, and runs it once. Your times depend on your device.

This runs in your browser, on this page's main thread. Code made with new Function isn't streamed like a script file. Each run adds a new comment, so nothing comes from a cache.

Load less at the start

If every kilobyte costs main-thread time, the cheapest kilobyte is one the page doesn't need yet. Addy Osmani's advice is to "only load JavaScript needed for the current page" and to "lazy-load the rest with code-splitting."

Real-life analogyUnpacking only the boxes you need tonight

You move into a new flat. Twenty boxes of your things wait in storage. Tonight you only need the kettle, a mug and your bedding, so you bring those three boxes home and unpack them. The rest stay in storage until the day you need them.

In real life: All twenty boxes in storage
In the browser: All of the app's code
In real life: The three boxes for tonight
In the browser: The code the page needs to start
In real life: A box fetched on the day you need it
In the browser: A file loaded later with import()
In real life: Unpacking a box
In the browser: Parsing, compiling and running a file

Two names go with this idea:

  • Code splitting cuts one big bundle into several smaller files.
  • Lazy loading fetches a file only when the page needs it.

In JavaScript, import() does the loading. It loads a module, a JavaScript file that shares its functions with export. MDN says import() lets you load a module "conditionally or on demand." Here the shop loads its checkout only when someone opens the cart:

Load the checkout when it's neededJavaScript
// Load the checkout's code only when someone opens the cart.const cartButton = document.querySelector("#open-cart"); cartButton.addEventListener("click", async () => {  const { showCheckout } = await import("./checkout.js");  showCheckout();});
  • Line 1 is a comment. Line 2 finds the cart button.
  • Line 4 tells the button to run a function each time someone clicks it.
  • Line 5 calls import(). The first time it runs, the browser fetches checkout.js, then parses, compiles and runs it. await waits until it's ready.
  • Line 6 calls showCheckout, a function from that file.

At the start, the page skips checkout.js completely: no download, no parsing, no compiling. People who never open the cart never pay for it. People who do pay later, when they ask for it. Smaller files help streaming too: V8 can stream and compile each one on its own.

The JavaScript lessons Dynamic import & top-level await and Loading performance show how to set this up.

Where the JavaScript lessons go deeper

This lesson measured what JavaScript costs. These lessons in the JavaScript course explain the engine work behind it:

Which cost does it cut?

Each fix below cuts one kind of cost. For each one, ask what it changes. Does it shrink the bytes on the network? Does it save the main thread from compiling? Or does it make the code do less once it runs?

Which cost does it cut?
  • Compressing the bundle with Brotli
  • Serving it from a server closer to the user
  • Keeping the script's URL the same between visits
  • Deploying changes less often
  • Moving a big inline script into its own file
  • Doing less work in each widget's render method
  • Building fewer rows in each widget's constructor
Try it yourself
0 of 7 correct

Sort each fix by the cost it cuts.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "A kilobyte is a kilobyte." No. The script and the picture downloaded in the same time. Then the script ran 121.1 ms of JavaScript on the main thread. The picture took 0.8 ms to decode, on worker threads.
  • "Compression makes JavaScript cheaper to run." No. It only shrinks the download. Once the bytes are unpacked, the browser still parses all of them. Addy Osmani's example: 350 KB of compressed script "bloats up to over 1MB of script a browser needs to process."
  • "Code caching helps from the second visit." No. The second visit compiled again and spent 24.9 ms saving the code. Only the third visit read it.
  • "Parsing is the slow part." No. For the 300 KB script, parsing took 18.1 ms of the main thread's 121.0. Compiling and executing each took longer.
  • "Four times slower in DevTools is a phone." No. It slows your own CPU, whatever that is. DevTools says it can't truly simulate a phone's CPU.
  • "Streaming means compiling never blocks." No. A function left for later is compiled on the main thread when it's first called. In the 300 KB script's load, the main thread spent 46.8 ms compiling.

Practice exercises

Answer first, then use the hints.

Exercise 1 · Warm-upSix times slower

Here are the same three bundles, recorded with the CPU 6 times slower. What does the last line print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
// Three sizes of the same app, with the CPU 6 times slower.
const bundles = {
  "100 KB": { bytes: 102920, ms: 68.4 },
  "300 KB": { bytes: 307718, ms: 187.2 },
  "1000 KB": { bytes: 1025948, ms: 582.6 },
};

for (const [name, { bytes, ms }] of Object.entries(bundles)) {
  const kilobytes = bytes / 1000;
  console.log(`The ${name} bundle: ${ms} ms, or ${(ms / kilobytes).toFixed(2)} ms for each KB.`);
}

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upA share of the cold visit

    Now each visit is shown as a share of the cold visit. What does the last line print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    // The 300 KB script on three visits in a row, with the CPU 4 times slower.
    // Each visit's work on the main thread, in ms. cache: saving or reading compiled code.
    const visits = {
      cold: { parse: 20.4, compile: 48.6, cache: 0, execute: 49.9, gc: 5.4 },
      warm: { parse: 0, compile: 5.9, cache: 24.9, execute: 37.7, gc: 3.7 },
      hot: { parse: 0, compile: 0.8, cache: 1.1, execute: 27.2, gc: 4.8 },
    };
    
    for (const [visit, work] of Object.entries(visits)) {
      const total = Object.values(work).reduce((sum, ms) => sum + ms, 0);
      console.log(`The ${visit} visit took ${Math.round((total / 124.3) * 100)}% of the cold visit's time.`);
    }

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeFull speed

      In the playground, how many milliseconds of JavaScript did the 1000 KB bundle run on the main thread at full speed?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeA deploy on Wednesday

        Asha visits the shop on Monday and Tuesday. On Wednesday morning, the shop deploys a changed app.js at the same URL. Asha visits again that afternoon. Which of V8's three runs is her Wednesday visit?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeCompile it all up front

          Which comment, at the top of a script, asks Chrome to compile every function in the file up front?

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeWhen to split a bundle

            V8 suggests splitting a bundle into smaller ones once it passes a certain size. What size is that?

            Answer, then press Check. Spacing and letter case don’t matter.

              Check your understanding

              For each question, ask where the cost lands: on the network, on the main thread, or on a worker. Then ask whether a cache can skip it.

              The cost of JavaScript quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does the last line of the picture example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // How long each page kept the main thread busy, in ms, with the CPU 4 times slower.
                const scriptPage = { mainThreadMs: 171.1, javascriptMs: 121.1 };
                const picturePage = { mainThreadMs: 77.5, javascriptMs: 0 };
                
                console.log(`The script's page: ${scriptPage.mainThreadMs} ms, ${scriptPage.javascriptMs} ms of it JavaScript.`);
                console.log(`The picture's page: ${picturePage.mainThreadMs} ms, ${picturePage.javascriptMs} ms of it JavaScript.`);
                const extra = scriptPage.mainThreadMs - picturePage.mainThreadMs;
                console.log(`The script's page needed ${extra.toFixed(1)} ms more.`);

                Choose an answer to see the explanation.

              2. Question 2 of 7In 2019, what did V8 call the dominant costs of processing scripts?

                Choose an answer to see the explanation.

              3. Question 3 of 7V8 left a function for later, and the page now calls it for the first time. Where is it compiled?

                Choose an answer to see the explanation.

              4. Question 4 of 7What does the second line of the code caching example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                // The 300 KB script on three visits in a row, with the CPU 4 times slower.
                // Each visit's work on the main thread, in ms. cache: saving or reading compiled code.
                const visits = {
                  cold: { parse: 20.4, compile: 48.6, cache: 0, execute: 49.9, gc: 5.4 },
                  warm: { parse: 0, compile: 5.9, cache: 24.9, execute: 37.7, gc: 3.7 },
                  hot: { parse: 0, compile: 0.8, cache: 1.1, execute: 27.2, gc: 4.8 },
                };
                
                for (const [visit, work] of Object.entries(visits)) {
                  const total = Object.values(work).reduce((sum, ms) => sum + ms, 0);
                  const compiling = work.parse + work.compile;
                  console.log(`The ${visit} visit: ${total.toFixed(1)} ms, ${compiling.toFixed(1)} ms of it parsing and compiling.`);
                }

                Choose an answer to see the explanation.

              5. Question 5 of 7On V8's warm run, the second time Chrome loads a script, what happens?

                Choose an answer to see the explanation.

              6. Question 6 of 7Why can adding ?v=2 to a script's URL slow down the next visits?

                Choose an answer to see the explanation.

              7. Question 7 of 7In the recordings, what did slowing the CPU do to the work on the worker threads?

                Choose an answer to see the explanation.

              Key takeaways

              • A script's bytes cost a download, then parsing, compiling and running, much of it on the main thread.
              • A picture of the same size cost the same download, then a short decode on worker threads.
              • V8 streams a script on a worker as it downloads. A function left for later is compiled on the main thread when first called.
              • Code caching: the second visit saves the compiled code, and the third skips compiling. Keep URLs stable and deploy less often.
              • A slower CPU makes the main thread's time grow. DevTools throttling only imitates a phone.
              • The cost grew with the size: about 0.4 ms a kilobyte at 4x for this app. Load only what the start needs, and measure yours.

              Remember the one-liner.
              Every kilobyte of JavaScript is paid for twice: once on the network, and again on the main thread.

              Sources

              The facts on this page come from these docs and articles:

              Coming next: The Performance panel, the first lesson of Measuring in the lab. It records a trace like the ones in this lesson, with throttling, and shows your real visitors' data beside it.

              CompleteFrontend Clear concepts. Working examples.