completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

What optimizing compilers do

Learn how inlining, scalar replacement, folding, hoisting, bounds checks, and representation choices make hot JavaScript faster.

By the end, you can
  • 01
    Name the classic passesSay what inlining, scalar replacement, constant folding, dead-code elimination, moving work out of loops, and removing bounds checks each do to hot code.
  • 02
    Keep what code does apart from how it runsRead the before and after pictures as ideas, not as changes to your file or as real V8 output.
  • 03
    Check V8 facts with careCheck facts in Node 22 and V8 12.4, such as optimization traces, flags, and which numbers are Smis, without trusting speed numbers.

Same meaning, less work

A JavaScript engine first runs your code with an interpreter, which starts quickly but runs slowly. When a function runs very often, the engine calls it hot. It then hands the hot function to an optimizing compiler: the part of the engine that builds much faster machine code for it.

The compiler has one firm rule. The fast code must act exactly like your code. It must print the same things, throw the same errors, change the same values, and return the same answers. It only does fewer steps inside the engine.

Definition

The compiler first turns your function into its own list of small steps, called the IR (short for intermediate representation). An optimization pass is one rewrite of that IR that does less work but never changes what your program does. Each pass has one job. In this lesson, the passes:

  • copy a small function into the code that calls it;
  • skip making an object that no other code can see;
  • work out fixed math ahead of time;
  • move repeated work out of a loop;
  • drop a safety check that can never fail;
  • pick a cheaper way to store a number.

Each pass relies on guards: quick checks in the fast code that make sure the compiler's guesses still hold.

Each pass in this lesson has a before snippet and an after snippet. Both are plain JavaScript, so you can run them and see that the output stays the same. The after snippet is only a picture of the idea. It is not what a real compiler prints, such as TurboFan or Maglev in V8 (the engine in Chrome and Node). Real engines never change your file; they change only their own IR.

The table lists the six passes. Read each row from left to right: the pass, what it changes, and why that helps.

Six classic passes in this lesson
PassWhat it changesWhy it helps hot code
InliningCopies a small function's body into the code that calls it.It saves the jump into the function and back, and lets other passes see the body.
Scalar replacementKeeps an object that never leaves its function as plain values, one per property.The engine can skip making the object at all.
Constant foldingWorks out fixed math, such as 60 * 60, once while compiling.There is less math left to do while the code runs.
Dead-code eliminationRemoves code that can never run, or whose result nobody uses.The fast code gets shorter.
Loop-invariant code motionMoves work that gives the same answer in every round to before the loop.The same math is not repeated in every round.
Bounds-check eliminationRemoves the check that an array index is inside the array, when it can never fail.Busy loops skip a check on every read.

The Optimizing compilers lesson shows how a whole compiler is built. The Speculation & deoptimization lesson covers guards, and how the engine goes back to slower code when a guess turns out wrong. This lesson stays on the rewrites themselves.

Function inlining: copy a small helper into the loop

A helper is a small function that other code calls. Each call costs a little work: the engine must jump into the function and then come back. Inlining means the compiler copies the helper's body into the code that calls it, so there is no call any more. The compiler only does this when the call always goes to the same small function. Your program still acts as if the call happened.

Before: a helper called from a busy loopPop out in the code editor (opens in a new tab)JavaScript
function fee(price) {  return price + 7;} function totalWithHelper(items) {  let total = 0;  for (const item of items) {    total += fee(item);  }  return total;} console.log(totalWithHelper([10, 20, 30]));

Line 1 defines fee, which adds 7 to a price. Line 8 calls fee once for each item. The output is 81, because 17 + 27 + 37 is 81. If line 8 runs very often, and it always calls the same small fee, the compiler may inline it.

After: the helper's body sits inside the loopPop out in the code editor (opens in a new tab)JavaScript
function totalWithInlinedHelper(items) {  let total = 0;  for (const item of items) {    total += item + 7;  }  return total;} console.log(totalWithInlinedHelper([10, 20, 30]));

Line 4 holds item + 7, the body of fee, right inside the loop. Line 9 prints the same 81. With the body in plain view, the compiler can run more passes on it, such as working out fixed math or picking a cheaper way to store the numbers.

Before: the loop line total += fee(item) calls the helper fee, which returns price + 7. After: the loop line is total += item + 7, with no call. Both print 81.beforetotal += fee(item)callsfee: return price + 7a call every roundaftertotal += item + 7helper line copied inno call at all
Before, each loop round calls fee. After, the helper's one line is copied into the loop, so there is no call. Both versions print 81.
Step through the helper calls before inlining
Step 0 of 8Ready
Your turn: follow the blue line

Step through the helper calls that a compiler may inline. The replay follows real JavaScript calls. The inlined code above is only a picture of the change inside the engine.

Running in
  1. script
Next: line 6
function fee(price) {  return price + 7;} function totalWithHelper(items) {  for (const item of items) {    total += fee(item);  }  return total;} console.log(totalWithHelper([10, 20, 30]));
CallStoreChangeResultBlue line: runs next. 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.
What inlining does not change

Inlining happens only inside the engine. For your program, the function still exists: error messages and the debugger can still show it. The engine also skips inlining when a function is large, or when one call can reach different functions. Think of inlining as a hidden copy kept safe by guards, not as a change to your file.

Escape analysis and scalar replacement

Making an object costs work. The engine must find memory for it, and later clean that memory up. Some objects are used only inside one function and then forgotten. Two passes work together on that case:

  • Escape analysis checks whether an object escapes, which means whether any code outside the function could ever get hold of it. An object escapes if it is returned, saved in an outside variable, or passed to another function.
  • Scalar replacement runs when the object does not escape. It keeps each property as a separate plain value, called a scalar, and skips making the object.
Before: a point object that never leaves the functionPop out in the code editor (opens in a new tab)JavaScript
function routeFee(x, y) {  const point = { x, y };  return point.x * 2 + point.y * 3;} console.log(routeFee(4, 5));

Line 2 makes a point object with x and y. Line 3 reads point.x and point.y and returns a number. The point is never returned, saved outside, passed to other code, or compared with another object. So it does not escape. Line 6 prints 23, because 4 * 2 + 5 * 3 is 23.

After: the object becomes two plain valuesPop out in the code editor (opens in a new tab)JavaScript
function routeFeeScalarPicture(x, y) {  const point_x = x;  const point_y = y;  return point_x * 2 + point_y * 3;} console.log(routeFeeScalarPicture(4, 5));

The after picture prints the same 23. Lines 2 and 3 hold point_x and point_y, one plain value for each property. Your code still makes an object. The fast code can skip it, because no other code could ever tell the difference.

Left, in your code: a point object holding x: 4 and y: 5. An arrow labeled never leaves points right. Right, in fast code: two plain values, point_x = 4 and point_y = 5. Both print 23.your codex: 4y: 5pointone objectnever leavesfast codepoint_x = 4point_y = 5two plain numbers
The point is only used inside routeFee. No other code ever sees it, so fast code can keep just its two numbers and skip making the object. Both versions print 23.
Step through a point object that never leaves
Step 0 of 5Ready
Your turn: follow the blue line

Watch a short-lived object that never leaves its function. Fast code can keep two plain values instead, but your program still acts as if the object was made.

Running in
  1. script
Next: line 6
function routeFee(x, y) {  const point = { x, y };  return point.x * 2 + point.y * 3;} 
CallStoreChangeResultBlue line: runs next. 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.
Real-life analogyWriting the total straight in a notebook

You are adding up a total. You could write a few numbers on a scrap note first. But if nobody ever asks for the note, you can skip it and write the numbers straight in your notebook.

In real life: You write numbers on a scrap note
In JavaScript: The code makes { x, y }
In real life: Nobody needs the note itself
In JavaScript: The object does not escape the function
In real life: You write the numbers straight in the notebook
In JavaScript: The properties become two plain values
In real life: If someone asks for the note, you must write it
In JavaScript: If the object escapes, it must really be made

Constant folding and dead-code elimination

Two more passes often work as a pair:

  • Constant folding works out math whose answer never changes, once, while compiling. A constant is a value that never changes.
  • Dead-code elimination (DCE for short) removes dead code: code that can never run, or whose result nobody ever uses.

Folding can show that an if test is always false. Then DCE can remove that whole branch.

Before: fixed math and a debug branch that never runsPop out in the code editor (opens in a new tab)JavaScript
const DEBUG = false;const SECONDS_PER_HOUR = 60 * 60; function expiry(seconds) {  const total = seconds + SECONDS_PER_HOUR;  if (DEBUG) {    console.log("debug", total);  }  return total;} console.log(expiry(30));

Line 2 is always 3600. Line 1 sets DEBUG to false, so the if on line 6 never runs. The program prints only 3630. It never prints debug.

After: the answer is ready and the branch is gonePop out in the code editor (opens in a new tab)JavaScript
const SECONDS_PER_HOUR = 3600; function expiryFoldedPicture(seconds) {  const total = seconds + SECONDS_PER_HOUR;  return total;} console.log(expiryFoldedPicture(30));

Line 1 now holds the ready answer, 3600. The if branch is gone, and DEBUG is gone with it, because nothing reads it any more. Line 8 prints the same 3630.

Top: 60 * 60 becomes 3600, worked out once. Bottom: the if (DEBUG) branch is removed, because DEBUG is always false and it can never run.60 * 603600worked out onceif (DEBUG) {...}removedit can never run
60 * 60 is always 3600, so the compiler works it out once. DEBUG is always false, so the debug branch can never run and is removed. The program still prints 3630.
Real-life analogyDoing 2 + 3 once

You see 2 + 3 many times on a page. You work it out once and remember 5, instead of adding again each time. A compiler can do the same with fixed math before the hot code runs.

In real life: You know 2 + 3 is always 5
In JavaScript: 60 * 60 is the same on every run
In real life: You work out the answer once
In JavaScript: Constant folding works out the fixed math
In real life: You reuse the answer each time
In JavaScript: The fast code keeps the ready answer, 3600

Loop-invariant code motion: move repeated work out

A loop runs the same lines many times. Some work inside a loop gives the same answer in every round. That work is called loop-invariant; invariant means "does not change". Loop-invariant code motion (LICM for short) moves that work before the loop, so it runs only once. Moving code up like this is also called hoisting. The compiler only does it when the move can't change what the program does.

Before: the same square root in every roundPop out in the code editor (opens in a new tab)JavaScript
function scaledTotal(items, rate) {  let total = 0;  for (const item of items) {    total += item * Math.sqrt(rate);  }  return total;} console.log(scaledTotal([9, 16], 9));

Math.sqrt works out a square root: Math.sqrt(9) is 3. Line 4 works out Math.sqrt(rate) in every round. item changes each round, but rate does not. With items [9, 16] and a rate of 9, line 9 prints 75, because 9 * 3 + 16 * 3 is 75.

After: work out the scale oncePop out in the code editor (opens in a new tab)JavaScript
function scaledTotalHoistedPicture(items, rate) {  const scale = Math.sqrt(rate);  let total = 0;  for (const item of items) {    total += item * scale;  }  return total;} console.log(scaledTotalHoistedPicture([9, 16], 9));

Line 2 works out the square root once and saves it in scale. Line 5 uses scale in each round, and line 10 prints the same 75.

Before: the loop works out 9 * Math.sqrt(9), then 16 * Math.sqrt(9), so the square root runs 2 times. After: scale = 3 is worked out once before the loop, then the loop uses 9 * scale and 16 * scale. Both print 75.before9 * Math.sqrt(9)16 * Math.sqrt(9)square root 2 timesafterscale = 3once9 * scale, 16 * scalesquare root 1 time
rate is 9 in every round, so Math.sqrt(rate) is always 3. The after picture works it out once, before the loop. Both versions print 75.

For this array of numbers, the move is safe. A real compiler must also check a few things first:

  • Math.sqrt is still the built-in function, not one your code put in its place.
  • Turning rate into a number can't run any of your own code.
  • rate really stays the same during the loop.

If any of these stops being true, the compiler must not move the work.

What your program sees comes first

A compiler can't freely move code that might:

  • run your own JavaScript;
  • throw an error at a different moment;
  • read a value that changes;
  • touch a proxy (an object that runs your code when you use it) or a getter (a function that runs when a property is read).

"Invariant" always means the compiler has proved the value can't change, under JavaScript's rules and its current guards.

Bounds-check elimination: prove the index is safe

When you read values[i], the engine first checks that i is inside the array: not below 0, and below values.length. This is a bounds check; the bounds are the first and last valid positions. Bounds-check elimination removes this check when the compiler can prove that i is always inside the array.

Before: the engine's hidden check, written outPop out in the code editor (opens in a new tab)JavaScript
function sumWithModelledBoundsChecks(values) {  let total = 0;  for (let i = 0; i < values.length; i += 1) {    if (i < 0 || i >= values.length) throw new RangeError("out of bounds");    total += values[i];  }  return total;} console.log(sumWithModelledBoundsChecks([3, 4, 5]));

You never write line 4 in real JavaScript. It only shows the check that the engine does for you. The loop on line 3 already makes sure that i starts at 0 and stops before values.length. So in this loop, the check on line 4 can never fail.

After: the check that can't fail is gonePop out in the code editor (opens in a new tab)JavaScript
function sumAfterBoundsCheckElimination(values) {  let total = 0;  for (let i = 0; i < values.length; i += 1) {    total += values[i];  }  return total;} console.log(sumAfterBoundsCheckElimination([3, 4, 5]));

Both snippets print 12, because 3 + 4 + 5 is 12. The fast code still keeps guards. If the array or its length changes in a way the proof didn't expect, the engine keeps the checks, or goes back to slower code. Going back like that is called deoptimization.

Representation selection: Smis, doubles, and words

JavaScript has one number type for whole numbers and fractions alike. Inside, the engine can store a number in different ways. Each way is called a representation. When guards prove it is safe, the compiler picks the cheapest one:

  • A Smi (small integer) is a whole number small enough to fit right inside the value itself, with no extra memory. An integer is a whole number.
  • A double is a 64-bit number that can hold fractions, like 1.5.
  • A word32 is a 32-bit whole number. The computer's chip can work with these very fast.
A whole-number add and a fraction addPop out in the code editor (opens in a new tab)JavaScript
function addWord32(x) {  return (x + 1) | 0;} function addNumber(x) {  return x + 0.5;} console.log(addWord32(41));console.log(addNumber(1.5));

Line 2 uses | 0. The | is the bitwise OR operator. Like every bitwise operator, it first turns its numbers into 32-bit whole numbers, which drops any fraction. OR with 0 then leaves that whole number as it is. So line 9 prints 42.

Line 6 keeps the fraction: line 10 prints 2, because 1.5 + 0.5 is 2. The compiler can also pick a word32 by itself, with no | 0 in your code, when its guards prove the result stays the same.

Each row below is one way the engine can store a number. None of them is a JavaScript type: your code only ever sees numbers.

Representations are engine choices, not extra JavaScript types
RepresentationWhat it isWhen it helps
SmiA small whole number kept right inside the value, with no extra memory.Fast whole-number math and array positions, while the numbers stay small.
DoubleA 64-bit number that can hold fractions.Needed for numbers like 1.5 and for many Math results.
Word32A 32-bit whole number, like the result of x | 0.Fast when the compiler proves the numbers stay 32-bit whole numbers. Writing | 0 yourself also changes your results outside that range.

The next code only runs in Node, with a special setting. %IsSmi is a V8 helper that answers "is this value stored as a Smi?". Helpers that start with % only work when Node starts with --allow-natives-syntax, so this block has no Run button. The ** operator means "to the power of".

Node only: which numbers are Smis?JavaScript
console.log("node", process.version.replace(/\.\d+$/, ".x"));console.log("v8", process.versions.v8.split(".").slice(0, 2).join("."));console.log("pointer compression", process.config.variables.v8_enable_pointer_compression);console.log("2^30 - 1 is Smi", %IsSmi(2 ** 30 - 1));console.log("2^30 is Smi", %IsSmi(2 ** 30));console.log("2^31 - 1 is Smi", %IsSmi(2 ** 31 - 1));console.log("2^31 is Smi", %IsSmi(2 ** 31));console.log("1.5 is Smi", %IsSmi(1.5));console.log("(3.9 | 0)", 3.9 | 0);
Checked in Node

The lesson's tests run this code in Node 22, which uses V8 12.4. Line 3 prints whether the build uses pointer compression, a memory-saving setting that makes Smis smaller. So lines 4 to 7 check both edges, 2 ** 30 and 2 ** 31, instead of guessing one size. In every build, 1.5 is not a Smi, and 3.9 | 0 prints 3.

The Tagged values, Smis & heap numbers lesson shows how values are stored in memory. Here you only need one idea: the compiler can pick a cheaper way to store a number when its guards prove the result stays the same.

Experiment: step through a tiny optimizer

Now put three passes together. The next replay uses a short list of steps written for this lesson, a bit like a tiny IR. It is not V8's real IR. It runs three passes in order: constant folding, then dead-code elimination, then loop-invariant code motion. Watch the list change while the program's answer stays 3675.

Step through three optimization passes
Step 0 of 5Ready
Your turn: follow the blue line

Run three passes on a tiny list of steps: work out fixed math, remove dead code, and move repeated work out of the loop.

Running in
  1. script
Next: line 1
  "const SECONDS = 60 * 60",  "const DEBUG = false",  "let total = 0",  "loop: const scale = Math.sqrt(rate)",  "loop: const price = items[i] * scale",  "loop: total = total + price",  "if (DEBUG) console.log(total)",  "return total + SECONDS",]; const folded = foldConstants(ops);const cleaned = removeDeadBranches(folded);const hoisted = hoistLoopInvariantWork(cleaned);console.log(render(hoisted));
CallStoreChangeResultBlue line: runs next. 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.

Next, try the free version. Turn one pass off at a time and watch what changes:

  • Constant folding changes 60 * 60 into 3600.
  • Dead-code elimination removes the if (DEBUG) step, so the list gets one step shorter.
  • Loop-invariant code motion moves the square root before the loop.
  • The program output stays 3675 every time.
Playground: turn passes on and off
The code that runs the passesJavaScript
const passes = new Set(["fold", "dce", "licm"]);const result = optimizeTinyIR(passes); console.log(result.operations.join("\n"));console.log("operation count", result.opCount);console.log("program output", result.output);
The list after the passes3675
7 steps3675 program output
  1. const SECONDS = 3600;
  2. const DEBUG = false;
  3. let total = 0;
  4. const scale = Math.sqrt(rate); // hoisted before the loop
  5. loop: const price = items[i] * scale;
  6. loop: total = total + price;
  7. return total + SECONDS;
Step 3 of 37 steps
Turn passes on or off

Passes on: constant folding, dead-code elimination, loop-invariant code motion. The list has 7 steps, and the program still gives 3675.

This is a model written for the lesson. It does not look inside your browser's engine. It runs a tiny optimizer on a list of steps and checks that the output stays the same.
Which pass is this?
  • The body of a small fee(item) helper is copied into a busy loop.
  • An if (DEBUG) branch disappears once the compiler knows DEBUG is always false.
  • Math.sqrt(rate) moves before a loop because rate never changes inside it.
  • A short-lived { x, y } that never leaves its function becomes two plain values.
  • Adding two numbers that are always small whole numbers uses a Smi or 32-bit add instead of the general number add.
  • A loop with i < array.length lets fast code skip the check on each array read.
Try it yourself
0 of 6 correct

Put each card under the kind of pass it describes. If you pick the wrong one, the card explains why.

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

Real Node and V8 evidence without timing claims

Speed numbers change from run to run and from computer to computer, so this lesson never claims a speedup. Instead, the lesson's tests start separate Node programs and check facts that stay the same:

  • the Node version starts with v22. and the V8 version starts with 12.4.;
  • the optimization flags exist (a flag is a setting you give Node when you start it, like --trace-opt);
  • a function really gets optimized;
  • the trace output mentions TurboFan, V8's optimizing compiler, and inlining;
  • turning some passes off does not change the function's result.

Before each run, the tests also clear the test runner's own NODE_TEST_* settings, so each Node program starts clean.

The next code also needs --allow-natives-syntax, so it has no Run button. Line 13 tells V8 to get ready to optimize hotTotal. Line 14 runs it 20 times to warm it up. Line 15 asks V8 to optimize it on the next call. Line 16 prints the result, 45. Line 17 prints a status number. V8 packs several yes-or-no facts about the function into that number, such as whether it is optimized.

Node only: optimize a function on purposeJavaScript
function helper(x) {  return x + 7;} function hotTotal(n) {  let total = 0;  for (let i = 0; i < n; i += 1) {    total += helper(i);  }  return total;} %PrepareFunctionForOptimization(hotTotal);for (let round = 0; round < 20; round += 1) hotTotal(20);%OptimizeFunctionOnNextCall(hotTotal);console.log("result", hotTotal(5));console.log("status", %GetOptimizationStatus(hotTotal));
Flags checked in this Node 22 build

The tests look for --trace-opt, --trace-turbo-inlining, --turbo-inlining, --turbo-escape, --trace-turbo-escape, and --allow-natives-syntax in the list that node --v8-options prints. They also run the same program with --no-turbo-inlining (inlining off) and --no-turbo-escape (escape analysis off). The output stays 45 every time.

What the test proves

It proves facts about this Node build: Node starts with v22., V8 starts with 12.4., TurboFan can optimize the code, the inlining trace has a line with Inlining in it, and turning passes off keeps the same result. It does not claim the code got faster.

Use this knowledge safely

Don't write code aimed at one pass. These passes explain why simple, steady hot code is easier for engines to speed up. But clear code and real measurements come first.

  • Write clear functions first. Don't copy helper bodies into loops by hand.
  • In hot code you have measured, pass the same kinds of values, and objects with the same properties, when that fits your data.
  • Keep short-lived objects when they make code easier to read. Change them only if a profile shows that making objects is slow.
  • Warm up a benchmark with realistic values first, and use its results so the engine can't skip the work.
  • Use V8 flags and % helpers only in local checks and tests, never in app code.
  • Measure before and after any change you make for speed.

Suppose a profile (a report from a tool like Chrome DevTools that shows where the time goes) points at a slow loop. This lesson gives you ideas to test. Maybe one call reaches different functions. Maybe a short-lived object escapes. Maybe the loop repeats the same work. Maybe the numbers are mixed, so the compiler can't pick a cheap way to store them. Test each idea with a measurement. Don't change code on a hunch.

Common misconceptions

  • "The after snippets are what V8 prints." They are pictures of rewrites a compiler might make. They are not real compiler output.
  • "Inlining always makes code faster." It makes the fast code bigger, the engine limits how much it inlines, and it can lead to more deoptimizations.
  • "If the compiler skips making an object, the object never existed." Your program still acts exactly as if the object was made.
  • "Bounds-check elimination makes JavaScript arrays unsafe." It only removes extra checks that can never fail, and only where the compiler has proved that.
  • "Smi is a JavaScript type." It is one way the engine stores a number. Your program still sees an ordinary number.

Each row in the table is an idea that is easy to mix up with another. Read what it means, then what it is not.

Similar ideas that are easy to mix up
IdeaWhat it meansWhat it is not
Your codeThe JavaScript you wrote, and the rules it must follow.The compiler's IR, which can look like a different program inside the engine.
OptimizationA rewrite inside the engine that keeps the same results, based on the values seen and what the compiler can prove.A promise that every run gets faster.
DeoptimizationGoing back to slower code when a guess turns out wrong.A bug, or a change in your results.
RepresentationHow the engine stores a number inside, once it has proved that is safe.A JavaScript type your code can see or rely on.

Practice exercises

Exercise 1 · Warm-upPredict the inlined helper total

What number does this print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
function fee(price) { return price + 7; }
function total(items) { return items.reduce((sum, item) => sum + fee(item), 0); }
console.log(total([10, 20, 30]));

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

    Exercise 2 · Warm-upWork out the point example

    What does routeFee(4, 5) print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    function routeFee(x, y) {
      const point = { x, y };
      return point.x * 2 + point.y * 3;
    }
    console.log(routeFee(4, 5));

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

      Exercise 3 · PracticeFind the dead branch

      Look back at the folding example. The compiler knows DEBUG is always false. Which branch can dead-code elimination remove?

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

        Exercise 4 · PracticeSpot the work that never changes

        Which expression can loop-invariant code motion move before the loop?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        for (const item of items) {
          total += item * Math.sqrt(rate);
        }

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

          Exercise 5 · PracticePredict a word32 conversion

          What does this print?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          console.log(3.9 | 0);

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

            Exercise 6 · ChallengeApply it to a real app

            What should you do before you change code because of an idea about the compiler?

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            // A product list feels slow when filtering 5,000 items.
            // Before you change any helpers by hand, your next step is: ____

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

              Quiz: check your understanding

              For each question, first work out what the JavaScript prints. Then name the rewrite that keeps that output.

              Compiler optimizations quiz · 8 questionsScore: first tries count
              1. Question 1 of 8What is the main job of an optimization pass?

                Choose an answer to see the explanation.

              2. Question 2 of 8What does this inlining example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                function fee(price) { return price + 7; }
                function total(items) { return items.reduce((sum, item) => sum + fee(item), 0); }
                console.log(total([10, 20, 30]));

                Choose an answer to see the explanation.

              3. Question 3 of 8Which passes can skip making a short-lived { x, y } object that never leaves its function?

                Choose an answer to see the explanation.

              4. Question 4 of 8What does this constant-folding example print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const DEBUG = false;
                const SECONDS = 60 * 60;
                function expiry(x) {
                  if (DEBUG) console.log('debug');
                  return x + SECONDS;
                }
                console.log(expiry(30));

                Choose an answer to see the explanation.

              5. Question 5 of 8Why can Math.sqrt(rate) move before the lesson's loop?

                Choose an answer to see the explanation.

              6. Question 6 of 8What does this bitwise representation example print last?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                console.log((41 + 1) | 0);
                console.log(3.9 | 0);

                Choose an answer to see the explanation.

              7. Question 7 of 8Which sentence about bounds-check elimination is true?

                Choose an answer to see the explanation.

              8. Question 8 of 8What did the Node checks prove, without using any timings?

                Choose an answer to see the explanation.

              Key takeaways

              • An optimizing compiler rewrites its own IR, never your file, and the fast code must act exactly like your code.
              • Inlining copies a small helper into the code that calls it, which lets other passes see more.
              • Escape analysis finds objects that never leave a function; scalar replacement then keeps their properties as plain values.
              • Constant folding, dead-code elimination, loop-invariant code motion, and bounds-check elimination remove or move work only when the compiler can prove it is safe.
              • The compiler can store a number as a Smi, a double, or a word32; your code still only sees ordinary numbers.

              Remember the one-liner.
              An optimization is a proven shortcut inside the engine: your code means the same thing, and hot code does less work.

              Up next: On-stack replacement. It shows how a long loop can switch to fast code while it is still running, without waiting for the next call.

              CompleteFrontend Clear concepts. Working examples.