cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Lazy parsing & preparsing

Learn how V8 preparses JavaScript functions, delays full parsing until calls, avoids inner reparse work, and uses compile hints for startup.

By the end, you can
  • 01
    Explain lazy parsingSeparate full parsing from preparsing and describe why most startup functions begin with a lightweight pass.
  • 02
    Predict eager casesIdentify PIFEs, startup calls, and explicit compile hints that make V8 parse a function earlier.
  • 03
    Measure the trade-offUse DevTools and V8 function-event logs to spot duplicate preparse plus full-parse work.

Why engines delay work

A modern page often ships thousands of functions, but only a fraction run before the first screen is useful. If the engine fully parsed and compiled every body immediately, startup would spend CPU time and memory on menus, settings panels, validators, and feature code the user may never open.

Plain definition

Lazy parsing means the engine checks a function body with a lightweight preparser at load time, then waits to do the full parse and compile until the first call needs that function.

Real-life analogySkimming a book before reading a chapter

Imagine a teacher checking a textbook before class. They skim the table of contents and headings to know what exists, but they read a chapter closely only when that chapter is on today's plan. Lazy parsing uses the same budget: validate and remember structure now, read the details when execution reaches them.

In real life: Skim chapter titles and headings
In JavaScript: Preparse function bodies
In real life: Notice a broken table of contents
In JavaScript: Catch syntax and early errors
In real life: Mark which chapters refer to earlier ideas
In JavaScript: Record scope and outer-variable facts
In real life: Read one chapter carefully when assigned
In JavaScript: Full-parse and compile on first call

Where the analogy stops: A book can be skimmed by looking at headings. JavaScript syntax is more complex, so the preparser still has to parse real syntax; it just avoids building the full compiler-ready result.

This lesson builds on function expressions, closures, and loading performance. It also connects to the engine path from Meet the engines, the parser work in Parsing & ASTs, the variable facts in Scope analysis, and the startup trade-offs in Startup performance.

Full parse vs lazy parse

CORE CONTRAST

A full parse builds the representation a compiler can use for a function body. In V8, that feeds Ignition's bytecode compiler. A preparse is still a real syntax pass, but it records less: enough to know the function is valid, where it ends, and which scope facts matter later.

Parse-related events that are easy to confuse
TermWhat it meansWhy it matters
Full parseBuilds the real parser output for a function body, including the AST shape needed by the bytecode compiler.Needed before a function can run; expensive if many shipped functions never run.
PreparseChecks syntax and early errors and records enough scope and boundary facts to come back later.Fast startup path for ordinary functions; not the same as ignoring the body.
ReparseA previously preparsed function is fully parsed when the first call reaches it.Duplicate work when the function is called immediately after load.
Code cache hitChrome can reuse serialized bytecode for unchanged scripts on hot repeat visits.Avoids some parse/compile work, but it is a cache with version and source-matching limits.

The visible program below defines four functions. Startup calls only startApp(). That means the settings panel and the click handler body are useful code, but not startup code.

A page that defines more than startup callsPop out in the code editor (opens in a new tab)JavaScript
function startApp() {  registerClickHandler();  return showShell();} function registerClickHandler() {  return function onClick() {    return openSettingsPanel();  };} function showShell() {  return "shell ready";} function openSettingsPanel() {  return "settings ready";} console.log(startApp());

Line by line: line 1 defines the startup function; line 6 defines a helper that returns a future click handler; line 12 defines a small helper that is called now; line 16 defines a settings feature; line 20 calls only the startup path. Lazy parsing is the reason line 16 does not have to pay full parser cost during the first load.

The preparser

V8 FACTS

V8's 2019 article Blazingly fast parsing, part 2: lazy parsing explains why the preparser cannot simply count braces. JavaScript grammar is too complex; it must parse syntax enough to find the real function end and enforce early errors. The saving is that it avoids building the full AST and bytecode-ready structures for bodies that are not needed yet.

The preparser also records just enough about inner functions for later scope analysis: declarations, references to outer variables, function boundaries, and data that lets the full parser resume without starting nested preparsing from scratch. That connects directly to closures: if an inner function references an outer variable, the engine must remember that fact before deciding where variables live.

Lazy startup: preparse now, full-parse on first call
Step 0 of 8Ready
Your turn: follow the blue line

Step through a page that defines more functions than startup calls. Watch ordinary functions start as preparsed, then become fully parsed only when a call reaches them.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  registerClickHandler();  return showShell();} function registerClickHandler() {  return function onClick() {    return openSettingsPanel();  };} function showShell() {  return "shell ready";} function openSettingsPanel() {  return "settings ready";} console.log(startApp());
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.
Lazy does not mean unchecked

A lazily parsed function still has valid JavaScript syntax. If it contained an early error, the script could not safely continue as if the body did not exist. The preparser is a correctness pass plus a performance shortcut.

Skipping inner functions when reparsing an outer function

NESTED CODE

Older lazy parsing could become non-linear for deeply nested code: an outer function was preparsed, then later fully parsed, and inner functions could be preparsed again along the way. V8's 2019 work stores inner-function preparse data so a later full parse of the outer body can skip inner bodies that still are not called.

Reparse the outer function without re-preparsing every inner body
Step 0 of 6Ready
Your turn: follow the blue line

Step through V8's lazy nested-function idea: preparse once, save inner-function facts, then skip uncalled inner bodies when an outer function is reparsed for execution.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
  function readTitle() {    return "Dashboard";  }   function renderChart() {    return "Chart";  }   return readTitle();} console.log(outerRoute());
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.

The key phrase is skip inner bodies, not ignore them. The engine already knows where readTitle and renderChart begin and end, and it knows the scope facts it must preserve. When readTitle() is called, that inner function gets its own full parse. Its sibling can keep waiting.

Possibly-invoked function expressions

PIFE

A function wrapped in parentheses, such as (function () { ... })(), is often an IIFE: an immediately-invoked function expression. V8 calls the recognizable parser signal a PIFE, a possibly-invoked function expression, because the parser notices the likely call pattern early and can eagerly parse and compile that body.

PIFE shape vs ordinary declaration callPop out in the code editor (opens in a new tab)JavaScript
(function bootPife() {  console.log("pife boot");})(); function bootNormal() {  console.log("normal boot");} bootNormal();

In V8's 2019 post, the original parenthesized pattern is joined by a Chrome 57-era recognition of common UglifyJS output such as a leading !function or a comma-separated ,function after a PIFE. The old optimize-js package used static heuristics to add these hints. That helped older V8 versions much more than it helps modern V8, because parser speed and inner-function skipping improved. As of September 2026, treat PIFE wrapping as a measured startup hint, not a formatting rule.

Eager, lazy, or explicit hint?
  • (function boot() { start(); })();
  • function boot() { start(); } boot();
  • button.addEventListener('click', function openPanel() { ... });
  • !function(){ boot(); }(),function(){ more(); }();
  • //# allFunctionsCalledOnLoad
  • export function openSettings() { ... }
Try it yourself
0 of 6 correct

Sort each code shape by how it influences the parser decision.

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

Reparsing costs

REAL V8 LOG

Lazy parsing is a trade-off. A function that is defined but never called avoids full parse work. A function that is called immediately can pay twice: first the lightweight preparse, then the full parse on demand. V8's function-event log makes that structure visible without relying on invented flags.

Probe used by the lesson testsJavaScript
function uncalledCold() { return 41; }function calledHot() { return 42; }console.log(calledHot());
Node 22.23.1 (V8 12.4) function-event summaryText
Node 22.23.1 (V8 12.4)
flags used: --log-function-events, --logfile, --no-logfile-per-isolate
uncalledCold: preparse-resolution; no full-parse; no first-execution
calledHot: preparse-resolution -> full-parse -> parse-function -> first-execution
pifeNow: full-parse at load; no parse-function event
normalNow: preparse-resolution -> full-parse -> parse-function -> first-execution

The tests spawn child Node processes with --log-function-events, --logfile, and --no-logfile-per-isolate, after verifying those flags with node --v8-options. They delete NODE_TEST_* environment variables and remove the generated log directory after parsing it.

Explicit compile hints

CHROME 136

V8's 2025 post Giving V8 a Heads-Up says Chrome 136 ships file-based explicit compile hints. The exact magic comment is //# allFunctionsCalledOnLoad, placed at the top of a JavaScript file. It tells V8 that all functions in that file are expected to be called during page load, so eager compilation may save on-demand stalls and duplicate parsing.

File-level explicit compile hintPop out in the code editor (opens in a new tab)JavaScript
//# allFunctionsCalledOnLoadfunction hydrateHeader() {  return "header";} function attachSearch() {  return "search";} console.log(hydrateHeader() + " + " + attachSearch());
Use hints sparingly

The same V8 post warns that compiling too much consumes time and memory. The WICG explainer separates the shipped file-level feature from per-function hints, which remain a design sketch rather than stable Chrome syntax as of September 2026.

Measure it

DEVTOOLS + FLAGS

For a real website, start in Chrome DevTools' Performance panel and record a reload. Search the main thread for Compile Script and Compile Code events, then connect those events to the bundle and the route that caused them. For controlled experiments, V8's function-event log is useful, but always check available flags in the runtime you are testing.

Measurement checklistShell
# Browser trace: open DevTools > Performance and record reload.# Look for V8 "Compile Script" and "Compile Code" events. node --v8-options | grep -E -- "--(lazy|log-function-events|logfile)"node --log-function-events --no-logfile-per-isolate --logfile=v8.log app.js

Code caching is a separate repeat-visit mechanism. The V8 posts on code caching for developers and improved code caching explain that Chrome can serialize bytecode for unchanged scripts and later deserialize it. That can reduce parse and compile time on hot visits, but it does not make a first visit or changed source free.

Lazy engine simulator

ACORN MODEL

The playground below parses the visible source with acorn, extracts a real function tree, and runs a teaching model over it. The counters are deliberately simple parse-work units, not V8 timings, but the relationships are the ones you just saw: lazy preparsing, eager PIFEs, duplicate work for immediately-called ordinary functions, and whole-file compile hints.

Lazy engine simulator
Source parsed by acornPop out in the code editor (opens in a new tab)JavaScript
function startApp() {  registerClickHandler();  return showShell();} function registerClickHandler() {  return function onClick() {    return openSettingsPanel();  };} function showShell() {  return "shell ready";} function openSettingsPanel() {  return "settings ready";} console.log(startApp());
Lazy default5 functions
lazypreparse 18, full 12, total 30
compiled3/5
double parsedstartApp, registerClickHandler, showShell
skipped full parseonClick, openSettingsPanel
top-level callsstartApp
Compare totalsparse units
Lazy default30
Eager all18
File hint18
Try it yourself
SourceStrategy

The model preparses ordinary functions, eagerly compiles recognized PIFEs, then full-parses only startup calls. A called function that was preparsed is counted as double parsed.

Teaching model: acorn extracts the real function tree from the visible source; the counters model parser work, not exact V8 timings.

Production choices

In production, the biggest win is usually shipping less startup code: split optional features, avoid pulling admin tools into the public route, and keep large libraries behind the interaction that needs them. Lazy parsing helps once source text is already present; it does not replace code splitting or byte budgets.

  1. Measure startup compile events on a throttled profile.
  2. Identify functions that are called during load versus later interactions.
  3. Prefer moving optional code out of the initial bundle when possible.
  4. Use PIFE shape or file-level compile hints only for profiled core startup code.
  5. Retest memory and interaction timing after making code more eager.
A review question

If a file contains ten functions and only two run during load, a file-wide compile hint may make the first two smoother while making the other eight wasteful. If all ten really run during load, the hint can move work earlier and make it easier to parallelize.

Common misconceptions

“Lazy parsing skips syntax errors.”

The preparser checks syntax and early errors. Lazy code is not unchecked code.

“Lazy parsing and lazy loading are the same.”

Lazy loading delays fetching code. Lazy parsing delays full parser work after source text is already available.

“PIFE wrapping is always faster.”

It helps only when the function is needed on startup. Over-eager parsing costs memory and can slow load.

“Code caching removes the need to care about parse cost.”

Code caching is heuristic and repeat-visit oriented. First visits, changed files, and execution still matter.

“Compile hints are per-function today.”

The stable Chromium feature covered here is file-level. Do not invent per-function syntax.

Nearby performance terms
TermWhat it delays or reusesImportant boundary
Lazy parsingDelays full parsing/compilation of function bodiesStill checks syntax and records scope facts.
Lazy loadingDelays fetching a script or chunkA lazy-loaded chunk can still be lazily parsed after it arrives.
Code cachingReuses compiled bytecode for identical sourceHelps repeat visits; first visits and changed files still pay.
Explicit compile hintsAsk V8 to eagerly compile a whole fileUseful for core files where most functions really run on load.

Practice exercises

5 EXERCISES
Exercise 1 · Warm-upName the two passes

Type the two parse-stage names in the order printed.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const seen = ["preparse", "full parse"];
console.log(seen.join(" -> "));

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

    Exercise 2 · PracticeRecognize the parenthesized immediate call

    Type the acronym for the eager-parsing hint shape.

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    (function boot() {
      return "eager candidate";
    })();
    console.log("PIFE");

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

      Exercise 3 · PracticeAdd the duplicate parse work

      Type the total printed by the small cost model.

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const costs = { preparse: 5, fullParse: 12 };
      console.log(costs.preparse + costs.fullParse);

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

        Exercise 4 · PracticeWrite the file-level hint

        Type the exact compile-hint comment from the lesson.

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const firstLine = "//# allFunctionsCalledOnLoad";
        console.log(firstLine);

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

          Exercise 5 · ChallengePick the browser trace events

          Type the two DevTools event names joined by a plus sign.

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          const events = ["Compile Script", "Compile Code"];
          console.log(events.join(" + "));

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

            Quiz: check your understanding

            7 QUESTIONS
            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7Why do engines lazy-parse most function bodies during startup?

              Choose an answer to see the explanation.

            2. Question 2 of 7What does this startup program print?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const seen = ["preparse", "full parse"];
              console.log(seen.join(" -> "));

              Choose an answer to see the explanation.

            3. Question 3 of 7Which shape did V8 historically treat as a possibly-invoked function expression?

              Choose an answer to see the explanation.

            4. Question 4 of 7What is the duplicate work when an immediately-needed function was not eagerly parsed?

              Choose an answer to see the explanation.

            5. Question 5 of 7What does the exact file-level compile hint print in this exercise?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const firstLine = "//# allFunctionsCalledOnLoad";
              console.log(firstLine);

              Choose an answer to see the explanation.

            6. Question 6 of 7Which statement about per-function compile hints is accurate as of September 2026?

              Choose an answer to see the explanation.

            7. Question 7 of 7Where can you measure parse and compile work in a browser?

              Choose an answer to see the explanation.

            Key takeaways

            • V8 lazy-parses ordinary function bodies because most functions shipped to a page are not called during startup.
            • The preparser still checks syntax and early errors and records scope facts; it does not ignore the body.
            • Calling a lazy function immediately creates duplicate work: preparse first, full parse on first call.
            • PIFEs and file-level compile hints move selected code toward eager parsing, which helps only when profiling says the code is load-critical.
            • Code caching helps repeat visits; it is separate from first-load lazy parsing decisions.

            Remember the one-liner.
            Lazy parsing buys startup time by proving function bodies are valid now and doing the full compiler-ready parse only for bodies execution reaches.

            Next: Scope analysis & variable allocation, where those preparser facts become decisions about where variables live.

            CompleteFrontend Clear concepts. Working examples.