How to Measure JavaScript Parse and Compile Cost on Real Devices

This guide gives JavaScript Performance Budgets their conversion factor, within JavaScript Bundle Optimization & Code Splitting. Budgets expressed in kilobytes are only convincing when someone can say what a kilobyte costs in milliseconds on the devices your users hold. That number is not universal: it depends on the engine, the device's CPU, whether code is cached, and what the code does when it is evaluated.

Modern engines are fast at parsing — V8 parses lazily, compiles in the background where it can, and caches compiled code for repeat visits — so the cost per kilobyte has fallen over the years. But on a mid-tier phone, cold-loading a few hundred kilobytes of JavaScript still costs on the order of 100–300ms of main-thread time before any application logic runs, and that time sits directly in Total Blocking Time and in the input delay of the first interactions.

Loading one 420KB (uncompressed) bundle on a mid-tier phone, cold cache Main-thread timeline of compiling, evaluating and executing a large bundle on a mid-tier phone, compared with the 50 millisecond long-task line. Loading one 420KB (uncompressed) bundle on a mid-tier phone, cold cache Main thread compile (eager) evaluate modules lazy compile on call app startup 0ms 50ms 100ms 150ms 200ms 250ms 300ms 350ms 400ms 450ms 50ms long-task line Lazy compilation moves some cost to first call, which is why startup functions appear slower in profiles than their logic suggests.

Rapid Diagnosis

  • Record a throttled, cold-cache trace. DevTools Performance panel, 4x CPU, "Disable cache" checked. In Bottom-Up, group by Activity and look at "Compile Script", "Compile Code", "Evaluate Script" and "Parse HTML" totals.
  • Compare cold and warm loads. Reload without disabling the cache; V8's code cache should cut compile time substantially on the second load. If it does not, scripts may be changing between loads (inline scripts with dynamic content, unique query strings).
  • Look at which scripts dominate. Group Bottom-Up by URL; vendor chunks and polyfills are frequent leaders.
  • Profile on a real device at least once. Remote debugging an Android phone over USB gives true numbers that throttling only approximates.

Root Cause Analysis

1. Size on the critical path. Compile time scales roughly with the amount of code compiled at startup, which depends on uncompressed size and how much of it runs immediately.

2. Eager top-level code. Modules that do work when evaluated — building lookup tables, registering components, configuring SDKs — force compilation and execution at load, regardless of whether the result is used soon.

3. Cache misses. V8's code cache keys on the script URL and content. Changing hashes on every deploy (for unchanged code) or injecting per-request content into scripts defeats it, forcing full compilation on repeat visits.

4. Low-end CPUs. The same code compiles three to five times slower on a budget phone than a laptop. Desktop measurements consistently understate the problem.

Compile + evaluate time for the same bundle by device (cold) Bar chart of compile and evaluate time for one bundle across four device classes. Compile + evaluate time for the same bundle by device (cold) Desktop laptop 48ms Flagship phone 72ms Mid-tier Android 175ms Low-end Android 340ms 50ms

Step-by-Step Measurement

1. Measure in the lab with tracing

Use Puppeteer to capture a trace with the relevant categories and sum compile and evaluate durations per script.

javascript
// scripts/js-cost.mjs — cold-load compile/evaluate cost per script URL.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
const cdp = await page.createCDPSession();
await cdp.send('Emulation.setCPUThrottlingRate', { rate: 4 });
await page.setCacheEnabled(false);
await page.tracing.start({ categories: ['devtools.timeline', 'v8', 'disabled-by-default-v8.compile'] });
await page.goto(process.argv[2], { waitUntil: 'networkidle0' });
const trace = JSON.parse(await page.tracing.stop());
const byUrl = {};
for (const e of trace.traceEvents) {
  if (!['v8.compile', 'EvaluateScript', 'v8.compileModule', 'v8.evaluateModule'].includes(e.name) || !e.dur) continue;
  const url = e.args?.data?.url || e.args?.fileName || '(unknown)';
  byUrl[url] = (byUrl[url] ?? 0) + e.dur / 1000;
}
console.table(Object.entries(byUrl).sort((a, b) => b[1] - a[1]).slice(0, 10));
await browser.close();
// trace event names differ slightly between Chrome versions — check them in a
// recorded trace if totals come out empty.
// trade-off: CPU throttling scales main-thread work but not background
// compilation threads equally, so throttled lab numbers are approximations.

Expected outcome: a ranked list of scripts by compile and evaluate milliseconds under throttling.

2. Derive your bytes-to-milliseconds factor

Divide each script's compile + evaluate time by its uncompressed size. The median across your bundles is your project's conversion factor for budgets — for example "about 0.4ms per KB on our mid-tier profile".

3. Measure in the field with Long Animation Frames

LoAF script entries report invokerType: 'classic-script' or 'module-script' for script evaluation, with durations. Summing them during load in RUM gives field evaluation cost per script URL on real devices.

javascript
new PerformanceObserver((list) => {
  for (const f of list.getEntries()) for (const s of f.scripts) {
    if (s.invokerType === 'classic-script' || s.invokerType === 'module-script') {
      evalCost[s.sourceURL] = (evalCost[s.sourceURL] ?? 0) + s.duration;
    }
  }
}).observe({ type: 'long-animation-frame', buffered: true });
// trade-off: LoAF reports only scripts within long frames (over 50ms), so short
// evaluations are omitted. The data shows the worst offenders, not a full total.

Expected outcome: field evidence of which scripts cost the most to evaluate on real hardware — see finding slow scripts with LoAF script attribution.

4. Check code caching on repeat visits

Compare cold and warm traces. V8 caches compiled code for scripts loaded more than once with identical content; module scripts and service-worker-cached scripts get more aggressive caching. If warm compile time is close to cold, investigate cache-busting.

Cold vs warm load of the same bundle Comparison of compile and evaluate costs on a first visit versus a repeat visit with V8 code caching. Cold vs warm load of the same bundle Cold (first visit) • Full download • Full parse and compile • Evaluate all modules • ~175ms on mid-tier phone Warm (code cache hit) • From HTTP cache • Compiled code reused • Evaluate still runs • ~80ms on mid-tier phone

Verification

Track compile + evaluate time for your top routes as a lab metric over time, alongside bundle size. The two should move together; when size falls but time does not, look for eager top-level code. After optimisations, confirm field evaluation cost (from LoAF data) falls for the affected scripts, and that INP for interactions in the first seconds improves.

Worked Example: Same Size, Different Cost

Two chunks of nearly identical size (about 120KB uncompressed each) appeared in a trace with very different costs: 22ms and 94ms of compile + evaluate on the mid-tier profile. The cheaper one was mostly function definitions — compiled lazily, executed later on demand. The expensive one was a configuration module that, at evaluation time, built a large object of validation schemas and ran them once to "warm up". Making schema construction lazy (built on first use) cut the module's startup cost to 18ms without changing its size. Budgets in bytes would never have found it; the per-script timing did.

Common Mistakes

  • Measuring on a developer laptop without throttling. The numbers will look negligible and mislead every decision.
  • Measuring only with warm caches. First visits from search and ads are cold, and they are the visits Core Web Vitals most often fail on.
  • Treating compressed size as the cost driver. Compile time follows uncompressed size and code structure, not gzip output.
  • Ignoring evaluation. Top-level side effects can cost more than compilation; profile what modules do when imported.

FAQ

Does V8's lazy parsing make bundle size irrelevant?

No. Lazy parsing defers full compilation of function bodies until they are called, which helps, but the engine still pre-parses all code to find function boundaries and syntax errors, and code that runs at startup is compiled anyway. Smaller bundles still load faster; lazy parsing only reduces the penalty for code that is shipped but not run.

Is WebAssembly cheaper to compile than JavaScript?

Wasm is designed for fast, streaming compilation and is typically cheaper per byte to compile than JavaScript, though modules are often large. It is not a general replacement; it pays off for compute-heavy libraries (codecs, image processing), not for UI code.

Do Safari and Firefox have similar costs?

The shape is similar — parse, compile, evaluate, with caching for repeat visits — but absolute numbers differ by engine and device. Apple devices generally have fast CPUs, so Safari users rarely sit at the slow end of your distribution. Measure Chromium on mid-tier Android for the pessimistic case.