What Actually Happens When You Run JavaScript

The Pipeline. From Source To Running Code.
Your JavaScript does not run directly. The engine transforms it first. V8. SpiderMonkey. JavaScriptCore. They all follow roughly the same steps.
Parse. The engine reads your source code and builds an AST. AST means Abstract Syntax Tree. A tree structure that represents the code.
Bytecode. The AST gets compiled to bytecode. Bytecode is a simpler instruction set that the engine can execute quickly. Not machine code yet. Just a middle step.
Interpret. The engine runs the bytecode in an interpreter. This is fast to start but slow to execute long-term.
Optimize. If a piece of code runs often. The engine compiles it to real machine code using a JIT compiler. Machine code is the actual instructions your CPU runs. Fast.
JIT means Just-In-Time. The compiler runs while the program is running. Not ahead of time.
Hidden Classes. Why Object Shape Matters.
JavaScript lets you add properties to objects whenever you want. Convenient. But it makes objects hard to optimize.
Engines solve this with hidden classes. Also called shapes or maps.
A hidden class is an internal structure that tracks what properties an object has and in what order. Every object gets one.
Same properties in the same order. Engine reuses the hidden class. Fast. Different orders. Different hidden classes. Slow.
Example.
const a = { x: 1, y: 2 };
const b = { x: 1, y: 2 };
// Same hidden class. Engine can optimize both together.
const c = { x: 1 };
c.y = 2;
const d = { y: 2 };
d.x = 1;
// Different hidden classes. Even though they end up with same properties.
The lesson. Initialize objects with the same shape in the same order. Every time.
Inline Caches. Remembering Shapes.
The hidden class lets the engine skip property lookups.
First time you access obj.name. The engine finds where name lives inside that hidden class. Then caches that location right next to the instruction.
Next time that instruction runs. If the object has the same hidden class. The engine reads directly from the cached location. No lookup.
This cache is called an inline cache or IC. It is one of the biggest reasons modern JavaScript is fast.
Every spot in your code where you access a property has its own inline cache. The cache tracks which hidden classes have shown up at that exact spot. Three possible states.
Monomorphic. Only one hidden class has ever been seen there. The engine just checks "same shape as before? yes. grab from the known location." One check. Fastest.
Polymorphic. A handful of different hidden classes have been seen. Usually 2 to 4. The engine now has to check each one in turn. "Is it shape A? No. Shape B? Yes. Grab from there." More checks. Slower.
Megamorphic. Too many different hidden classes have shown up. Past the engine's limit. The cache gives up and falls back to a full generic property lookup every time. Slowest.
JIT Compilation. From Warm To Hot.
The engine does not optimize everything. It watches your code and tracks which functions run often. A function that runs many times is called hot.
When a function gets hot. The engine sends it to the optimizing compiler. The compiler makes assumptions based on what it has seen.
"This function always receives two numbers." "This object always has these properties in this order." "This array always contains integers."
Under those assumptions. It generates fast machine code. Much faster than bytecode.
The key thing. The optimized code is not general. It is specialized for the patterns observed.
Deoptimization. When Assumptions Break.
Assumptions can be wrong. You pass a string where you used to pass a number. You add a new property to an object. You mix types in an array.
When that happens. The optimized code is no longer valid. The engine throws it away and falls back to the bytecode interpreter. This is called deoptimization.
Deoptimization is expensive. The optimization work is wasted. Your code runs slower until the engine decides to reoptimize. If it does.
Common causes.
Passing different types to a function that used to be consistent.
Adding or deleting properties on hot objects.
Mixing integers and floats in an array.
Using constructs the optimizer does not handle well.
Garbage Collection. How The Engine Frees Memory.
You do not manually free memory in JavaScript. The engine handles it. The system that does this is the garbage collector. Or GC.
The GC walks a graph. It starts from things called roots and follows every reference it finds. Anything reachable from a root is live. Anything it cannot reach is garbage.
What Counts As A Root
Roots are the anchor points. Things the engine always treats as live.
Global and module-level variables. Things on
window,globalThis, or declared at the top level of a module. Live for the lifetime of the program.Active stack variables. Local variables inside functions currently running on the call stack. Live only while their function is executing.
Closure-captured variables. Values an outer scope captured that a still-reachable closure holds on to.
Roots themselves are not collected. But what they point to can still become garbage. Set a global variable to null. The old value it used to point to is now unreachable. Free to collect. The root slot itself is untouched.
A local variable stops being a root the moment its function returns. Whatever it pointed to becomes garbage unless something else still holds a reference.
Generational Collection
Modern engines use generational garbage collection. Based on one observation. Most objects die young. A function creates objects. Uses them. Returns. Those objects are garbage almost immediately.
The GC splits memory into two zones.
Young generation. Small. Where new objects are born. Most die here.
Old generation. Larger. Where objects that survived a few young collections get promoted.
Minor GC vs Major GC
Minor GC. Collects only the young generation. Happens often. Fast. Pauses are usually a few milliseconds or less.
Major GC. Collects the old generation. Sometimes scans both generations together. Happens rarely. Slower because there is more memory to scan and more references to trace.
The split keeps most pauses short. You pay the long pause only when the old generation gets full.
Tips For Writing Performant JavaScript
You do not need to hand-tune everything. But these habits help the engine help you.
Keep Object Shapes Consistent
Same properties. Same order. Every time.
// Good
const user1 = { name: "Alice", age: 30, role: "admin" };
const user2 = { name: "Bob", age: 25, role: "user" };
// Bad
const user1 = { name: "Alice", age: 30 };
user1.role = "admin"; // new hidden class
Declare All Properties Upfront
If a property might be missing at first. Initialize it to null. Do not add it later.
// Good
const user = { name: "Alice", age: null };
user.age = 30; // same hidden class. just a value change.
// Bad
const user = { name: "Alice" };
user.age = 30; // new hidden class
Do Not Delete Properties
Set them to null instead. Delete changes the hidden class. Setting to null does not.
// Good
user.age = null;
// Bad
delete user.age;
Keep Argument Types Consistent
If a function takes a number. Always pass a number. Mixing types causes deoptimization.
function add(a, b) {
return a + b;
}
add(5, 10); // engine optimizes for numbers
add("hi", "bye"); // deopt. now reoptimizes for strings
Do Not Mix Integers And Floats In Arrays
The engine uses a different internal format for integer arrays vs float arrays. Mixing them forces a slower general format.
// Good
const ints = [1, 2, 3, 4];
const floats = [1.5, 2.5, 3.5];
// Bad
const mixed = [1, 2.5, 3, 4.5];
Use Set For Membership Checks
includes on an array is O(n). has on a Set is O(1). Huge difference when checking membership often.
// Slow. O(n * m)
const result = list1.filter((x) => list2.includes(x));
// Fast. O(n + m)
const set = new Set(list2);
const result = list1.filter((x) => set.has(x));
Use Map For Dynamic Keys
If you are adding and removing keys at runtime. Use a Map. Regular objects end up with constant hidden class changes. Maps do not.
// Bad
const cache = {};
cache[userId] = data;
// Good
const cache = new Map();
cache.set(userId, data);
Use === Not ==
== does type coercion behind the scenes. === compares directly. Faster and more predictable. There is no reason to use ==.
Watch Out For Closures Holding Large Data
A closure keeps alive everything it captures. Even if you only use a small piece. The GC cannot free the rest.
// Bad. keeps all of hugeData alive
function makeHandler() {
const hugeData = loadHugeData();
return () => console.log(hugeData.name);
}
// Good. only keeps the name
function makeHandler() {
const hugeData = loadHugeData();
const name = hugeData.name;
return () => console.log(name);
}





