cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Loading performance

Learn how to reduce JavaScript bytes, split code, choose async or defer, cache smarter, and protect Core Web Vitals on real devices.

By the end, you can
  • 01
    Budget JavaScript honestlySeparate compressed transfer bytes from parse, compile, and execute cost, then enforce route and chunk budgets.
  • 02
    Load code deliberatelyChoose route splitting, interaction-time import(), resource hints, lazy media, async, defer, or modules for the job.
  • 03
    Protect user metricsConnect loading choices to LCP, INP, CLS, repeat-visit caching, bfcache, Lighthouse, and field data.

Ship less, start sooner

Loading performance is the discipline of making the browser do less work before the page is useful. The fastest JavaScript is often the code you do not send, followed by code you send later, then code the browser can reuse on the next visit.

Definition

Loading performance means controlling how much JavaScript, CSS, media, and data arrive on the critical path; when each resource is discovered; and how much main thread work runs before the first useful paint and interaction.

Real-life analogyPacking for a trip

You do not carry winter boots, a toolbox, and a spare monitor onto every flight. You pack what the first day needs, send bulky items separately, and reuse what is already waiting at the destination. Loading performance asks the same questions about code.

In real life: Carry-on bag
In JavaScript: Initial JavaScript for the first route
In real life: Checked bag
In JavaScript: Lazy chunks loaded after navigation or interaction
In real life: Packing list
In JavaScript: Bundle analyzer report and budget
In real life: Hotel storage
In JavaScript: HTTP cache, service worker cache, and code cache

Where the analogy stops: A suitcase does not parse, compile, or hydrate. JavaScript does, so a small-looking item can still be expensive when it runs on a slow phone.

This lesson builds directly on bundlers, dynamic import, page lifecycle, Cache API and file storage, service workers, and engines and runtimes. It also links forward to measuring performance and keeping the main thread responsive.

The cost of JavaScript

BYTES + CPU

JavaScript has a longer bill than many resources. A compressed file must download, decompress into source text, parse, compile, and execute. Images can be huge, but byte-for-byte a script usually asks more from the CPU and the main thread before the page responds.

The JavaScript startup bill
StageWhat the browser pays
DownloadThe network transfers compressed bytes. Brotli or gzip helps, but the browser must still receive the file before it can run.
DecompressThe compressed response expands into larger source text. Budgets should track both compressed transfer and uncompressed JavaScript.
ParseThe engine turns source text into syntax structures. More source means more parser work before execution can finish.
CompileEngines such as V8 compile JavaScript to bytecode or optimized machine code. Repeat visits may reuse cached bytecode.
ExecuteYour code runs on the main thread unless you move work to a worker. Hydration and startup handlers can block input.

The 2024 Web Almanac JavaScript chapter, published on March 3, 2025 from the June 2024 HTTP Archive crawl, reports median JavaScript payloads of 558 KB on mobile and 613 KB on desktop. The same chapter says mobile JavaScript grew 14% year over year. web.dev's JavaScript startup guidance reminds us that average mobile phones can have slow CPUs, limited cache, and memory pressure, so a fast office laptop is not representative.

Budget both transfer and engine work

Track compressed kilobytes because the network sees them. Track uncompressed kilobytes because the engine parses those. Then inspect parse, compile, and execute time in the Performance panel instead of guessing from bytes alone.

Bundle analysis turns this into a team habit: list every entry and lazy chunk, label whether it is needed for the current route, set a budget, and fail pull requests when the budget grows without a conscious trade-off.

A real budget checker

STEP THROUGH

The next function is intentionally small, but it is real. It accepts a list of chunks and a budget, totals initial bytes, lazy bytes, total bytes, and estimated parse/compile time, then reports exactly which limit failed.

Budget checker: find the expensive chunks
Step 0 of 10Ready
Your turn: follow the blue line

Step through a real JavaScript budget checker. It totals compressed transfer bytes, estimates parse/compile time from uncompressed bytes, and reports each budget failure.

Running in
  1. script
Next: line 48
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function checkJavaScriptBudget(chunks, budget) {  const initialCompressedKb = chunks    .filter((chunk) => chunk.initial)    .reduce((total, chunk) => total + chunk.compressedKb, 0);  const asyncCompressedKb = chunks    .filter((chunk) => !chunk.initial)    .reduce((total, chunk) => total + chunk.compressedKb, 0);  const totalCompressedKb = chunks    .reduce((total, chunk) => total + chunk.compressedKb, 0);  const totalUncompressedKb = chunks    .reduce((total, chunk) => total + chunk.uncompressedKb, 0);  const overBudget = [];   if (initialCompressedKb > budget.maxInitialCompressedKb) {    overBudget.push({      name: "initial total",      overByKb: initialCompressedKb - budget.maxInitialCompressedKb,    });  }   for (const chunk of chunks) {    if (!chunk.initial && chunk.compressedKb > budget.maxAsyncChunkCompressedKb) {      overBudget.push({        name: chunk.name,        overByKb: chunk.compressedKb - budget.maxAsyncChunkCompressedKb,      });    }  }   if (totalCompressedKb > budget.maxTotalCompressedKb) {    overBudget.push({      name: "total JavaScript",      overByKb: totalCompressedKb - budget.maxTotalCompressedKb,    });  }   return {    initialCompressedKb,    asyncCompressedKb,    totalCompressedKb,    estimatedParseCompileMs: Math.round(      totalUncompressedKb * budget.mobileParseCompileMsPerKb,    ),    overBudget,  };}   { name: "app-shell", compressedKb: 82, uncompressedKb: 250, initial: true },  { name: "product-route", compressedKb: 63, uncompressedKb: 210, initial: true },  { name: "charting-library", compressedKb: 118, uncompressedKb: 410, initial: false },]; const budget = {  maxInitialCompressedKb: 140,  maxAsyncChunkCompressedKb: 90,  maxTotalCompressedKb: 260,  mobileParseCompileMsPerKb: 0.2,}; const report = checkJavaScriptBudget(chunks, budget);console.log(  "initial " + report.initialCompressedKb + " KB, async " + report.asyncCompressedKb + " KB",);console.log(  "over budget: " + report.overBudget    .map((item) => item.name + " +" + item.overByKb + " KB")    .join("; "),);console.log("estimated parse/compile " + report.estimatedParseCompileMs + " ms");
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Run the budget checker as ordinary JavaScriptPop out in the code editor (opens in a new tab)JavaScript
function checkJavaScriptBudget(chunks, budget) {  const initialCompressedKb = chunks    .filter((chunk) => chunk.initial)    .reduce((total, chunk) => total + chunk.compressedKb, 0);  const asyncCompressedKb = chunks    .filter((chunk) => !chunk.initial)    .reduce((total, chunk) => total + chunk.compressedKb, 0);  const totalCompressedKb = chunks    .reduce((total, chunk) => total + chunk.compressedKb, 0);  const totalUncompressedKb = chunks    .reduce((total, chunk) => total + chunk.uncompressedKb, 0);  const overBudget = [];   if (initialCompressedKb > budget.maxInitialCompressedKb) {    overBudget.push({      name: "initial total",      overByKb: initialCompressedKb - budget.maxInitialCompressedKb,    });  }   for (const chunk of chunks) {    if (!chunk.initial && chunk.compressedKb > budget.maxAsyncChunkCompressedKb) {      overBudget.push({        name: chunk.name,        overByKb: chunk.compressedKb - budget.maxAsyncChunkCompressedKb,      });    }  }   if (totalCompressedKb > budget.maxTotalCompressedKb) {    overBudget.push({      name: "total JavaScript",      overByKb: totalCompressedKb - budget.maxTotalCompressedKb,    });  }   return {    initialCompressedKb,    asyncCompressedKb,    totalCompressedKb,    estimatedParseCompileMs: Math.round(      totalUncompressedKb * budget.mobileParseCompileMsPerKb,    ),    overBudget,  };} const chunks = [  { name: "app-shell", compressedKb: 82, uncompressedKb: 250, initial: true },  { name: "product-route", compressedKb: 63, uncompressedKb: 210, initial: true },  { name: "charting-library", compressedKb: 118, uncompressedKb: 410, initial: false },]; const budget = {  maxInitialCompressedKb: 140,  maxAsyncChunkCompressedKb: 90,  maxTotalCompressedKb: 260,  mobileParseCompileMsPerKb: 0.2,}; const report = checkJavaScriptBudget(chunks, budget);console.log(  "initial " + report.initialCompressedKb + " KB, async " + report.asyncCompressedKb + " KB",);console.log(  "over budget: " + report.overBudget    .map((item) => item.name + " +" + item.overByKb + " KB")    .join("; "),);console.log("estimated parse/compile " + report.estimatedParseCompileMs + " ms");

In production you would feed this kind of function from a bundler manifest, a stats file, or a CI artifact. This site is a Next.js static export, so each route has its own chunks, but this lesson does not quote local build numbers because a fresh production build was intentionally not run while the shared dev server is active.

Code splitting and lazy loading

ROUTES + INTERACTIONS

Route-level splitting keeps the first page from downloading code for every other page. App frameworks such as Next.js make route chunks by default, and bundlers create more split points when they see import(). The key question is whether the user needs the code for the first useful task.

Load a chart only after the user asksPop out in the code editor (opens in a new tab)JavaScript
const button = document.querySelector("#load-chart");const output = document.querySelector("#chart-output"); button.addEventListener("click", async () => {  button.disabled = true;  output.textContent = "Loading chart code...";  const chart = await import("./chart.js");  chart.renderChart(output);});

This is different from hiding work behind a timeout. The network request for chart.js does not start until the click handler reaches import(). The dynamic import lesson covers the promise and module namespace behavior in detail.

Resource hints that look similar but mean different things
HintMeaningScopeWatch out
preloadFetch a resource needed by the current page soonCurrent navigationMust include as, and wrong preloads waste bandwidth
modulepreloadFetch a module and its dependency graph earlierCurrent navigationUse for important modules discovered late
prefetchFetch likely future route resources at low priorityNext navigationShould not compete with current LCP resources
Preload, modulepreload, and prefetch are not interchangeableHTML
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin><link rel="modulepreload" href="/assets/product-route.js"><link rel="prefetch" href="/assets/settings-route.js" as="script">

Lazy loading is not only for JavaScript. Native loading="lazy" keeps below-the-fold images and iframes off the initial path. fetchpriority lets you tell the browser when a discovered resource is unusually important or unimportant, such as a likely LCP hero image versus a far-down product thumbnail.

Lazy media and fetch priorityHTML
<img  src="/products/chair.webp"  alt="Oak chair"  width="640"  height="480"  loading="lazy"  decoding="async"  fetchpriority="low"> <img  src="/hero.webp"  alt="New dashboard preview"  width="1280"  height="720"  fetchpriority="high">
Lazy is a trade-off

Lazy loading a below-the-fold carousel is a win. Lazy loading the only code that renders the hero or the first button can delay LCP or INP. Split by user journey, not by wishful thinking.

async, defer, and module scripts

HTML RULES

Script attributes decide whether HTML parsing stops, whether document order is preserved, and whether DOMContentLoaded waits. MDN summarizes the rules: async classic scripts fetch during parsing and evaluate as soon as available; defer scripts run after parsing and before DOMContentLoaded; module scripts defer by default; async modules are allowed.

The HTML specification is the source for those script-processing queues; MDN is the readable reference used here for the day-to-day rules. The simulator below encodes only those loading rules, not every network-priority or engine detail inside a browser.

Classic vs async vs defer vs module scripts
ScriptWhen it runsOrderDOMContentLoadedUse it for
Classic <script src>Blocks parsing when discoveredDocument orderDOMContentLoaded waits because parsing cannot finish until it runsSmall critical inline bootstrap or legacy dependency you cannot defer
async classicDownloads during parsing, runs as soon as readyNoDoes not delay DOMContentLoadedIndependent analytics, ads, or widgets
defer classicDownloads during parsing, runs after parsingYesDOMContentLoaded waits for itApp code that needs the parsed DOM
type="module"Deferred by default; dependency graph fetches as modulesDocument order for entry modules in this modelDOMContentLoaded waits for the entry module graphModern app entry points
async type="module"Module graph runs as soon as readyNoDoes not wait for document orderIndependent module widget
Five script tags, five loading contractsHTML
<script src="legacy.js"></script><script src="analytics.js" async></script><script src="app.js" defer></script><script type="module" src="routes.js"></script><script type="module" async src="independent-widget.js"></script>

CSS can be render-blocking too. A stylesheet in the document head can delay first paint until it arrives and is parsed. Splitting critical CSS, avoiding unused CSS, and not hiding the hero behind late styles matter alongside JavaScript loading.

Script loading: predict execution and DOMContentLoaded
Step 0 of 8Ready
Your turn: follow the blue line

Step through a script-loading timeline. This is a small deterministic model, not a browser engine, but it encodes the rules from the HTML spec and MDN for classic, async, defer, and module scripts.

Running in
  1. script
Next: line 40
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
function simulateScriptTimeline(scripts, parserDuration = 100) {  let parserDelay = 0;  const events = [];  const deferred = [];   for (const script of scripts) {    const discoveredAt = script.at + parserDelay;    const readyAt = discoveredAt + script.downloadMs;     if (script.kind !== "module" && !script.async && !script.defer) {      events.push({ time: readyAt, label: "execute " + script.name });      parserDelay += script.downloadMs + 1;      continue;    }     if (script.async) {      events.push({ time: readyAt, label: "execute " + script.name });    } else {      deferred.push({ ...script, discoveredAt });    }  }   const parserDoneAt = parserDuration + parserDelay;  let clock = parserDoneAt;   for (const script of deferred) {    const readyAt = script.discoveredAt + script.downloadMs;    const startAt = Math.max(clock, readyAt);    events.push({ time: startAt, label: "execute " + script.name });    clock = startAt + 1;  }   events.sort((a, b) => a.time - b.time || a.label.localeCompare(b.label));  return {    executionOrder: events.map((event) => event.label.replace("execute ", "")),    domContentLoadedAt: clock,  };}   { name: "analytics.js", at: 10, downloadMs: 70, async: true },  { name: "vendor.js", at: 20, downloadMs: 40, defer: true },  { name: "app.js", at: 30, downloadMs: 10, kind: "module" },  { name: "legacy.js", at: 40, downloadMs: 20 },]; const timeline = simulateScriptTimeline(scripts, 100);console.log(timeline.executionOrder.join(" -> "));console.log("DOMContentLoaded @" + timeline.domContentLoadedAt + "ms");
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Toggle a script attribute and watch the timeline
Current script tagsHTML
<script src="dom-ready.js" defer></script><script src="feature.js" defer></script>
Timeline resultdefer
executiondom-ready.js -> feature.js
DOMContentLoadedDOMContentLoaded @102ms
parser done100ms
Try it yourself
Feature script tag

Defer keeps order, downloads during parsing, and runs before DOMContentLoaded. It is the usual choice for classic app scripts.

This playground uses the same simulator as the step-through above. Change one attribute, then compare execution order and DOMContentLoaded.

Caching and code caching

REPEAT VISITS

A good first load still matters, but repeat visits should not pay the same network bill. HTTP caching, content-hashed filenames, CDN edge caching, service worker strategies, bfcache, and V8 code caching all reduce different kinds of repeated work.

Immutable caching works because the filename changesHTTP
Cache-Control: public, max-age=31536000, immutableETag: "app-shell.8f3a1c" /assets/app-shell.8f3a1c.js  -> cache for a year/assets/app-shell.91bd22.js  -> new URL after content changes

Cache-Control: max-age=31536000, immutable is safe only when a changed file gets a new URL. Without a content hash, users can stay stuck on old JavaScript. ETag and 304 Not Modified save response bytes after revalidation, but they still require a request. A CDN edge cache moves reusable responses closer to users, while browser caching removes many repeat requests completely.

Service worker cache-first shape for scriptsJavaScript
self.addEventListener("fetch", (event) => {  const request = event.request;   if (request.destination === "script") {    event.respondWith(      caches.open("static-v4").then(async (cache) => {        const cached = await cache.match(request);        if (cached) return cached;        const response = await fetch(request);        cache.put(request, response.clone());        return response;      }),    );  }});

Service workers can make an app resilient and fast, but they are a programmable cache layer. The service workers lesson covers lifecycle, scope, cleanup, and offline fallbacks. The Cache API lesson explains why a named Cache stores Request to Response pairs and does not obey HTTP expiration automatically.

V8 code caching is not HTTP caching

V8 can serialize compiled bytecode for identical JavaScript source and reuse it on a hot repeat visit, often alongside the HTTP cache. If the source changes byte-for-byte, the code cache is invalid. It can reduce parse/compile cost; it does not remove download, memory, or execution work for new code.

bfcache-friendly leaving signalsJavaScript
// Avoid this pattern for analytics or cleanup.window.addEventListener("unload", () => {  navigator.sendBeacon("/analytics", "left page");}); // Prefer signals that keep the back/forward cache available.document.addEventListener("visibilitychange", () => {  if (document.visibilityState === "hidden") {    navigator.sendBeacon("/analytics", "hidden");  }});

The back/forward cache stores a whole page snapshot for instant history navigation. Global unload handlers are a classic bfcache breaker; prefer pagehide or visibilitychange for analytics and quick saves. Review page lifecycle before adding leave handlers.

Core Web Vitals

FIELD METRICS

Core Web Vitals are field metrics, not just lab scores. As of web.dev's Web Vitals guidance last updated October 31, 2024, a good page experience means LCP at or below 2.5 seconds, INP at or below 200 milliseconds, and CLS at or below 0.1, evaluated at the 75th percentile of page loads separately for mobile and desktop.

How JavaScript affects the current Core Web Vitals
MetricNameGood thresholdHow JavaScript can hurt it
LCPLargest Contentful PaintGood at 2.5 seconds or lessClient-rendered hero markup, late hero image discovery, or low-priority image fetches delay it
INPInteraction to Next PaintGood at 200 milliseconds or lessHydration, long startup tasks, and huge event handlers keep the main thread busy
CLSCumulative Layout ShiftGood at 0.1 or lessLate-injected banners, images without dimensions, and client-only content can move the page

JavaScript can delay LCP by client-rendering the hero, by discovering the hero image only after hydration, or by loading critical CSS late. It can delay INP with long hydration tasks, big event handlers, and startup work. It can cause CLS by injecting banners, ads, or client content after layout without reserving space.

Measure supported Web Vitals entries with PerformanceObserverPop out in the code editor (opens in a new tab)JavaScript
const supported = "PerformanceObserver" in globalThis  ? PerformanceObserver.supportedEntryTypes || []  : [];const watched = ["largest-contentful-paint", "layout-shift", "event"]  .filter((type) => supported.includes(type)); console.log("Can watch: " + (watched.join(", ") || "none in this browser")); if (watched.length > 0) {  const observer = new PerformanceObserver((list) => {    for (const entry of list.getEntries()) {      console.log(entry.entryType + " @" + Math.round(entry.startTime));    }  });  observer.observe({ type: watched[0], buffered: true });  observer.disconnect();}

In production, many teams use the web-vitals library because it handles browser differences and reports final metric values. It is not installed in this lesson, so the next block is a non-runnable usage shape.

web-vitals library usage shapeJavaScript
import { onCLS, onINP, onLCP } from "web-vitals"; onLCP((metric) => sendToAnalytics("LCP", metric.value));onINP((metric) => sendToAnalytics("INP", metric.value));onCLS((metric) => sendToAnalytics("CLS", metric.value));
Which Web Vital does this improve?
  • Render the hero image in the initial HTML and give it fetchpriority="high".
  • Split a rarely used editor so hydration has less JavaScript on the product page.
  • Set image width and height before lazy images load.
  • Serve app.8f3a1c.js with Cache-Control: immutable.
  • Remove a global unload handler so pages can use bfcache.
  • Reserve space for a consent banner inserted after hydration.
  • Break a 700 ms startup task into chunks before enabling buttons.
  • Preload the CSS that reveals above-the-fold hero content.
Try it yourself
0 of 8 correct

Sort each optimization by the metric it most directly helps. Some changes are valuable without mapping to one Core Web Vital.

Choose a category for every card. You can change an answer at any time; Reset clears them all.
Lighthouse is lab data, not your whole audience

Lighthouse is excellent for a reproducible trace and local debugging. Field data from RUM or CrUX tells you what real devices, networks, and pages experienced. Use both: lab to explain a bottleneck, field data to prioritize and confirm it.

Production workflow

MEASURE + ENFORCE

Start every performance change by measuring. The measuring performance lesson shows marks, measures, DevTools traces, and RUM. The main thread lesson shows how long tasks hurt interaction. Loading performance uses those signals to decide what to remove, split, preload, or cache.

  1. Open a bundle analyzer and list initial, async, and shared chunks for the slow route.
  2. Record a Performance panel trace on a throttled mobile profile and inspect parse, compile, execute, and hydration time.
  3. Set budgets for initial compressed JS, largest async chunk, total route JS, and long tasks.
  4. Move optional work behind route boundaries or interaction-time import().
  5. Use HTTP caching and content hashes for immutable assets; use service workers only when you can own cleanup.
  6. Check LCP, INP, and CLS in field data after the change ships.
A practical review question

If a dependency is needed only after a user opens an editor, it should not be in the product page's initial route chunk. If it is needed to render the first screen, lazy loading it may make the page look faster in bytes while making LCP worse.

Common misconceptions

  • “Gzip size is the only size that matters.” Transfer size matters, but parse, compile, memory, and execution follow the uncompressed source.
  • “Lazy loading is automatically faster.” It is faster only when the delayed resource is not needed for the current view or task.
  • “async is better than defer.” Async is for independent scripts. App code that depends on DOM order usually wants defer or modules.
  • “A green Lighthouse run means real users are fine.” Lab scores are useful, but field data captures slow devices, background tabs, and real network conditions.
  • “Caching fixes too much JavaScript.” Caching helps repeat visits; first visits, code changes, memory, and execution still pay.
Similar performance terms, different decisions
TermMeaningUse it for
Compressed bytesWhat the network transfersUse for transfer budgets and CDN cost
Uncompressed bytesWhat the engine parses after decompressionUse for parse/compile risk and dependency reviews
Lazy loadingMoves work laterHelps only when the delayed work is truly not needed for the first task
CachingAvoids repeat transfer or compilationDoes not make the first visit free and can hide stale-code bugs

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upTotal the compressed chunks

Type the total compressed JavaScript printed by the small reducer.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const chunks = [82, 63, 118];
console.log(chunks.reduce((total, kb) => total + kb, 0));

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

    Exercise 2 · PracticePredict script execution order

    Type the execution order from the simulator, arrows included.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const order = ["legacy.js", "analytics.js", "vendor.js", "app.js"];
    console.log(order.join(" -> "));

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

      Exercise 3 · PracticeChoose the ordered script attribute

      Which attribute keeps external classic scripts ordered without blocking the parser?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const attribute = "defer";
      console.log(attribute);

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

        Exercise 4 · PracticeName the Web Vital

        Type the metric most directly improved by the hero optimization.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const hero = { renderedByServer: true, hasWidthAndHeight: true, fetchPriority: "high" };
        console.log(hero.renderedByServer && hero.hasWidthAndHeight && hero.fetchPriority === "high" ? "LCP" : "INP");

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

          Exercise 5 · ChallengeExplain the cache header choice

          What cache strategy does a content-hashed filename make safe?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const file = "/assets/app.8f3a1c.js";
          console.log(file.includes("8f3a1c") ? "immutable" : "revalidate");

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

            Check your understanding

            8 QUESTIONS
            Lesson quiz · 8 questionsScore: first tries count
            1. Question 1 of 8Why is JavaScript usually more expensive per byte than an image?

              Choose an answer to see the explanation.

            2. Question 2 of 8What does the budget checker print first?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const chunks = [82, 63, 118];
              console.log(chunks.reduce((total, kb) => total + kb, 0));

              Choose an answer to see the explanation.

            3. Question 3 of 8Which loading pattern preserves order and runs after parsing but before DOMContentLoaded?

              Choose an answer to see the explanation.

            4. Question 4 of 8What does this script order model print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const order = ["legacy.js", "analytics.js", "vendor.js", "app.js"];
              console.log(order.join(" -> "));

              Choose an answer to see the explanation.

            5. Question 5 of 8What should you use for a route the user might visit next, but not for the current LCP path?

              Choose an answer to see the explanation.

            6. Question 6 of 8Which statement about repeat visits is accurate?

              Choose an answer to see the explanation.

            7. Question 7 of 8What does the hero optimization example print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const hero = { renderedByServer: true, hasWidthAndHeight: true, fetchPriority: "high" };
              console.log(hero.renderedByServer && hero.hasWidthAndHeight && hero.fetchPriority === "high" ? "LCP" : "INP");

              Choose an answer to see the explanation.

            8. Question 8 of 8How should Lighthouse and field data be used together?

              Choose an answer to see the explanation.

            Key takeaways

            • JavaScript costs download, decompression, parse, compile, and execution.
            • Budgets should track initial chunks, lazy chunks, total transfer, and CPU risk.
            • Use route splitting and interaction-time import() only when the code is not needed for the first task.
            • defer and module scripts preserve ordered startup; async is for independent work.
            • HTTP caching, service workers, V8 code caching, and bfcache solve different repeat-visit problems.
            • LCP, INP, and CLS must be confirmed with field data at the 75th percentile.

            Remember the one-liner.
            Ship the smallest useful route, load optional code at the moment of need, and verify the result on real users rather than your laptop.

            Next in the Performance module: Finding memory leaks, where you will use heap snapshots to find what keeps objects alive.

            CompleteFrontend Clear concepts. Working examples.