cf.completefrontend
Open lab My progress
THE JAVASCRIPT FIELD GUIDE

How minifiers work

See how minifiers shrink JavaScript: removing spaces, shortening names, folding constants and dropping dead code, and which options can change what it does.

By the end, you can
  • 01
    Explain the three kinds of changeWhitespace and comments, shorter names, and compressing the code itself.
  • 02
    Predict what gets removedTell which names a minifier may rename, and which code is dead and can go.
  • 03
    Use the options safelyKnow which options, such as toplevel and property mangling, can change what a program does.

What a minifier does

A minifier makes JavaScript smaller without changing what it does, so pages download faster. It parses the code into a tree, changes the tree, and prints it with as few characters as possible. This lesson uses Terser, a minifier written in JavaScript.

Minify a small functionPop out in the code editor (opens in a new tab)JavaScript
import { minify } from "terser";

const code = [
  "function addTax(price) {",
  "  // tax is 20%",
  "  const rate = 0.2;",
  "  return price * (1 + rate);",
  "}",
  "console.log(addTax(10));",
].join("\n");
const result = await minify(code);
console.log(result.code);
console.log(code.length + " -> " + result.code.length + " characters");

Lines 3 to 10 hold the code, with a comment and a constant. Line 11 minifies it with Terser's default options, and line 12 prints function addTax(a){return 1.2*a}console.log(addTax(10));.

The comment and spaces are gone, price is now a, and 1 + rate became 1.2. Line 13 prints 116 -> 56 characters: less than half.

The three kinds of change, one at a timePop out in the code editor (opens in a new tab)JavaScript
import { minify } from "terser";

const code = [
  "function addTax(price) {",
  "  // tax is 20%",
  "  const rate = 0.2;",
  "  return price * (1 + rate);",
  "}",
  "console.log(addTax(10));",
].join("\n");
const steps = [
  ["spaces and comments", { compress: false, mangle: false }],
  ["+ shorter names", { compress: false }],
  ["+ compress", {}],
];
for (const [label, options] of steps) {
  const { code: out } = await minify(code, options);
  console.log(out.length, label);
}

Lines 12 to 14 turn Terser's two main options on one by one. It prints 83 with only spaces and comments removed, 69 with shorter names, and 56 with compression too.

Four cards in a row joined by arrows: 116 characters as written, 83 with no spaces or comments, 69 with shorter names, and 56 compressed.116 charsas written83 charsno spaces or comments69 charsshorter names56 charscompressed
Terser's three kinds of change, one after another, on the tax code above. Each step keeps what the program does and makes it shorter.
Real-life analogyWriting a text message

Picture squeezing a long note into a text message. You drop the extra words, use short forms your friend will understand, do the sums yourself, and cut anything that no longer matters. The message is shorter, but it must still mean the same thing.

In real life: Dropping extra spaces and greetings
In JavaScript: Removing whitespace and comments
In real life: "tmrw" for "tomorrow", if the reader knows it
In JavaScript: Shortening a local name
In real life: "2 dozen" becomes "24"
In JavaScript: Folding a constant
In real life: Cutting a sentence about a plan you cancelled
In JavaScript: Removing dead code
The main idea

A minifier removes whitespace and comments, shortens names, and compresses the code itself. Every change must keep what the program does; the options that might not are off by default.

Whitespace and name mangling

Spaces, line breaks and comments never change what code does, so they all go. Renaming is mangling, and it is more careful: a name can only be shortened if the minifier can see every place it is used.

What gets renamedPop out in the code editor (opens in a new tab)JavaScript
import { minify } from "terser";

const code = [
  "function total(items) {",
  "  let sum = 0;",
  "  for (const item of items) sum += item.price;",
  "  return sum;",
  "}",
  "console.log(total([{ price: 2 }, { price: 3 }]));",
].join("\n");
console.log((await minify(code, { compress: false })).code);
console.log((await minify(code, { compress: false, toplevel: true })).code);

Line 11 mangles without compressing. Inside total, items, sum and item become o, t and c: nothing outside the function can see them. But total itself stays, and so do price and console.

Line 12 adds toplevel: true, and now total becomes o too. That is only safe when nothing else uses the name: in a module or a bundle, yes; in a classic script shared with other scripts, maybe not.

Two columns. Inside total, shortened: items becomes o, sum becomes t, item becomes c. Outside, kept: total, a top-level name; price, a property; console, a global.inside totalitems → osum → titem → cshortenedoutsidetotal (top-level).price (property)console (global)kept
Names inside total can only be used inside it, so they are safe to shorten. The top-level name, the property price and the global console may be used by other code, so Terser keeps them by default.

Terser reuses short names a lot: the function and its parameter can both be called o, because they live in different scopes. The scope analysis lesson explains how tools tell scopes apart.

Dead code and constant folding

Compression rewrites the code itself. Two of its main tools: constant folding works out expressions whose values are already known, and dead-code elimination removes code that can never run or that does nothing.

Fold and removePop out in the code editor (opens in a new tab)JavaScript
import { minify } from "terser";

const code = [
  "const SECONDS_PER_DAY = 60 * 60 * 24;",
  "const DEBUG = false;",
  "function log(message) {",
  "  if (DEBUG) console.log(\"debug: \" + message);",
  "}",
  "function unused() {",
  "  return \"never called\";",
  "}",
  "log(\"start\");",
  "console.log(SECONDS_PER_DAY);",
].join("\n");
const { code: out } = await minify(code, { toplevel: true, mangle: false });
console.log(out);

Line 15 compresses with toplevel: true and no renaming, so the changes are easy to see. Ten lines of code become one: console.log(86400);.

Step through what Terser removed
Step 0 of 7Ready
Your turn: follow the blue line

A replay of what Terser did to this code, not a view inside a JavaScript engine. The final output is Terser's real output.

Running in
  1. script
Next: line 15
import { minify } from "terser"; const code = [  "const SECONDS_PER_DAY = 60 * 60 * 24;",  "const DEBUG = false;",  "function log(message) {",  "  if (DEBUG) console.log(\"debug: \" + message);",  "}",  "function unused() {",  "  return \"never called\";",  "}",  "log(\"start\");",  "console.log(SECONDS_PER_DAY);",].join("\n");console.log(out);
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.

Press Step to see each change, line by line. This replays what Terser did; it isn't a view inside the JavaScript engine.

Four rows. 60 times 60 times 24 becomes 86400. if DEBUG is removed. log start is removed. function unused is removed. At the bottom: console.log(86400).60 * 60 * 2486400if (DEBUG) …removedlog("start")removedfunction unusedremovedconsole.log(86400);
Folding works out 60 * 60 * 24. The rest is dead code: a branch that can't run, a call that does nothing, and a function nobody calls. One statement is left.

Bundlers use the same idea for build flags. They replace a name such as process.env.NODE_ENV with a string, and the minifier then removes the branches for the other value. Terser's version of this is compress.global_defs, used in exercise 3.

Terser, esbuild, SWC and Oxc

Your build tool usually picks the minifier for you. Read each row of the table as: "this minifier is written in this language, and is used here."

Minifiers you will meet
MinifierWritten inWhere you meet it
TerserJavaScriptwebpack 5's default minifier, through TerserPlugin. An option in Vite.
esbuildGoesbuild --minify. Vite's old default; Vite now marks it deprecated.
SWCRustNext.js minifies with SWC.
Oxc minifierRustVite's default minifier for client builds.

The newer ones, written in Go and Rust, are much faster. Vite's documentation says its default, the Oxc minifier, is 30 to 90 times faster than Terser, with output only 0.5 to 2% bigger. They all do the same three kinds of change.

Read each row of the next table as: "this Terser option starts like this, and does this."

Terser options in this lesson
OptionDefaultWhat it does
compressOnRewrites code: folds constants and drops dead code.
mangleOnShortens local variable and function names.
toplevelOffAlso renames and drops top-level names.
keep_fnamesOffKeeps function names, so .name still works.
mangle.propertiesOffAlso renames object properties. Easy to break code with.
compress.global_defsNoneReplaces named globals, such as DEBUG, with values.

After minifying, servers usually compress the file again with gzip or Brotli before sending it. The two work well together: the minifier removes what the program doesn't need, and the compressor squeezes repeated text.

Safe and unsafe transforms

A transform is safe when no program can tell the difference. Some options are only safe if your code doesn't do certain things. Here is a program that reads a function's name:

Function names and toplevelPop out in the code editor (opens in a new tab)JavaScript
import { minify } from "terser";

function run(code) {
  const logs = [];
  new Function("console", code)({ log: (value) => logs.push(JSON.stringify(value)) });
  return logs.join(" ");
}

const code = "function addTax(price) {\n  return price * 1.2;\n}\nconsole.log(addTax.name);";
for (const options of [{}, { toplevel: true }, { toplevel: true, keep_fnames: true }]) {
  const { code: out } = await minify(code, options);
  console.log(JSON.stringify(options), run(out));
}

Lines 3 to 7 run a piece of code and collect what it prints. Line 10 tries three option sets, and line 12 prints what each minified program prints.

By default, it prints "addTax", as before. With toplevel, Terser inlines the function as an anonymous one, and it prints "". With keep_fnames as well, the name survives.

Three rows for addTax.name. With default options it prints addTax, the same. With toplevel it prints an empty string, changed. With toplevel and keep_fnames it prints addTax, the same.default"addTax"sametoplevel""changedtoplevel, keep_fnames"addTax"same
The program prints addTax.name. With toplevel, Terser turns the function into an anonymous one, so the name is gone. keep_fnames keeps it.

Properties are riskier still, because data from outside uses their real names:

Renaming propertiesPop out in the code editor (opens in a new tab)JavaScript
import { minify } from "terser";

const code = [
  "const order = JSON.parse('{\"customerName\":\"Ada\"}');",
  "const card = { heading: order.customerName };",
  "console.log(card.heading);",
].join("\n");
const { code: out } = await minify(code, { mangle: { properties: true } });
console.log(out);

Line 8 turns on property mangling. In the output, order.customerName became order.o, but the JSON text still says customerName. So the minified program prints undefined. (heading stayed, because by default Terser leaves property names that browser objects use.)

Minifiers also avoid some changes on their own:

Names near evalPop out in the code editor (opens in a new tab)JavaScript
import { minify } from "terser";

const code = [
  "function run(code) {",
  "  const secret = 42;",
  "  return eval(code);",
  "}",
  "console.log(run(\"secret + 1\"));",
].join("\n");
console.log((await minify(code)).code);

Line 10 prints the output: code and secret are not renamed. The text given to eval on line 6 could use any name, here secret, so Terser keeps every name it can see.

Turn the options on and off

Turn Terser's options on and off. The playground shows Terser's real output, its size, and what both versions print when they run.

Playground: turn Terser's options on and off
Terser's output (cm--)JavaScript
const DEBUG=!1;function formatPrice(a){return"$"+(Math.round(100*a)/100).toFixed(2)}function addTax(a){return 1.2*a}console.log(formatPrice(addTax(10)),addTax.name);
Run bothsame output
286 → 165 characters42% smaller
original prints"$12.00" "addTax"
minified prints"$12.00" "addTax"
Try it yourself
165 characters, 42% smaller. It prints the same.

Every change kept what the program prints. Try turning on toplevel with mangle and compress, and leave keep_fnames off.

Terser made all 16 outputs in this lesson's tests; the page shows the saved one for your options, then runs both versions here.

Turn on toplevel with compress and mangle: the code drops to 102 characters, but addTax.name now prints "n". Turn on keep_fnames and the output grows a little, but prints the same again.

Rules of thumb for real builds:

  • Use your build tool's default minifier and options; they are chosen to be safe.
  • Don't use function or class names for logic, such as value.constructor.name === "Cart".
  • Only mangle properties you mark yourself, for example names that start with _.
  • Run your tests against the minified build, and keep source maps for debugging it.
Done by default, opt-in, or avoided?
  • Removing spaces and comments
  • Renaming a local variable
  • Turning 60 * 60 * 24 into 86400
  • Removing if (false) { … }
  • Renaming a top-level function
  • Renaming an object's properties
  • Renaming variables a direct eval could read
  • Renaming console or Math
Try it yourself
0 of 8 correct

Put each change with when Terser makes it: by default, only if you turn it on, or never, even when it's on.

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

Common mix-ups

Most mix-ups come from thinking minifying is only about spaces, or that every change is always safe.

  • "Minifying only removes spaces." No. It also renames, folds constants and removes dead code.
  • "Minified code runs faster." Not much by itself. It downloads and parses faster, because there is less of it.
  • "Every name gets shortened." No. Top-level names, properties and globals are kept by default.
  • "Minifying makes code secret." No. It is harder to read, but anyone can still read and run it.
  • "More options always means smaller and still correct." No. toplevel and property mangling can change what a program does.

The table below puts three of these side by side. Read each row as: "people often think this, but this is true."

What is true about minifiers
TopicWhat people thinkWhat is true
MinifyingIt only removes spaces.It also renames and rewrites code, and drops code that can't run.
Top-level namesThey are always safe to rename.Other scripts may use them, so toplevel is off by default.
PropertiesThey are renamed like variables.Not by default: data from JSON, the DOM or other code uses the real names.

Practice exercises

Each exercise runs the real Terser. Guess first, then run it.

Exercise 1 · Warm-upWhich names survive?

What does line 12 print?

Starter codePop out in the code editor (opens in a new tab)JavaScript
import { minify } from "terser";

const code = [
  "function total(items) {",
  "  let sum = 0;",
  "  for (const item of items) sum += item.price;",
  "  return sum;",
  "}",
  "console.log(total([{ price: 2 }]));",
].join("\n");
const { code: out } = await minify(code);
console.log(out.includes("total"), out.includes("items"));

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

    Exercise 2 · Warm-upFold an expression

    What does line 3 print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    import { minify } from "terser";
    
    console.log((await minify("console.log(2 * 3 + 4);")).code);

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

      Exercise 3 · PracticeRemove the debug code

      This keeps a DEBUG && check. Change line 9 so Terser knows DEBUG is false. What does your program print?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      import { minify } from "terser";
      
      const code = [
        "if (DEBUG) {",
        "  console.log(\"debug mode\");",
        "}",
        "console.log(\"ready\");",
      ].join("\n");
      const { code: out } = await minify(code);
      console.log(out);

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

        Exercise 4 · PracticeKeep the function's name

        This prints "". Change line 4 so the name is kept. What does your program print?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        import { minify } from "terser";
        
        const code = "function addTax(price) {\n  return price * 1.2;\n}\nconsole.log(addTax.name);";
        const { code: out } = await minify(code, { toplevel: true });
        const logs = [];
        new Function("console", out)({ log: (value) => logs.push(JSON.stringify(value)) });
        console.log(logs.join(" "));

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

          Exercise 5 · ChallengeMangle only your own properties

          This prints undefined. Change line 8 so only properties starting with _ are renamed. What does your program print?

          Starter codePop out in the code editor (opens in a new tab)JavaScript
          import { minify } from "terser";
          
          const code = [
            "const order = JSON.parse('{\"customerName\":\"Ada\"}');",
            "const cart = { _cache: order.customerName };",
            "console.log(cart._cache);",
          ].join("\n");
          const { code: out } = await minify(code, { mangle: { properties: true } });
          const logs = [];
          new Function("console", out)({ log: (value) => logs.push(String(value)) });
          console.log(logs.join(" "));

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

            Exercise 6 · ChallengeNames near eval

            What does line 11 print?

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            import { minify } from "terser";
            
            const code = [
              "function run(code) {",
              "  const secret = 42;",
              "  return eval(code);",
              "}",
              "console.log(run(\"secret + 1\"));",
            ].join("\n");
            const { code: out } = await minify(code);
            console.log(out.includes("secret"), out.includes("code"));

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

              Check your understanding

              For each question, ask: who else could see this name or this code, and could any program tell the difference? Each answer explains why it's right or wrong.

              The minifiers quiz · 6 questionsScore: first tries count
              1. Question 1 of 6Which names does Terser shorten with its default options?

                Choose an answer to see the explanation.

              2. Question 2 of 6What do these two function names print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const named = function addTax() {};
                console.log(JSON.stringify(named.name), JSON.stringify((function () {}).name));

                Choose an answer to see the explanation.

              3. Question 3 of 6What does reading a renamed property print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const order = JSON.parse('{"customerName":"Ada"}');
                console.log(order.customerName, order.o);

                Choose an answer to see the explanation.

              4. Question 4 of 6Why is toplevel off by default?

                Choose an answer to see the explanation.

              5. Question 5 of 6When can a minifier remove a piece of code?

                Choose an answer to see the explanation.

              6. Question 6 of 6Why doesn't Terser rename variables in a function that uses eval?

                Choose an answer to see the explanation.

              Key takeaways

              A minifier makes code smaller by changing it only in ways no program should notice.

              • Minifiers remove whitespace and comments, shorten names, and compress the code itself.
              • Local names are shortened; top-level names, properties and globals are kept by default.
              • Constant folding works out known values; dead-code elimination removes code that can't matter.
              • Terser, esbuild, SWC and the Oxc minifier do the same jobs; the newer ones are faster.
              • toplevel, missing keep_fnames and property mangling can change what code does.

              Remember the one-liner.
              Remove what nobody can see, shorten what only you use, and work out what's already known.

              Coming next: Build a tiny JavaScript interpreter. You'll write a tokenizer, a parser and an evaluator of your own.

              CompleteFrontend Clear concepts. Working examples.