Performance Optimization
Profile JavaScript execution, understand V8 optimisation internals, and apply concrete techniques to reduce parse time, layout thrashing, and unnecessary allocations.
Overview
Key Points
- •Measure before optimising: use Chrome DevTools Performance tab, console.time(), and performance.now()
- •V8 uses hidden classes objects with the same property creation order share a hidden class and fast property lookup
- •Initialise all properties in the constructor; adding properties dynamically after construction breaks hidden class sharing
- •Avoid deoptimisation triggers: polymorphic call sites, changing property types, using arguments object in hot functions
- •Layout thrashing: alternating DOM reads (offsetWidth) and writes (style.width) in a loop forces repeated synchronous layout recalculations
- •Batch DOM reads before writes; use requestAnimationFrame to schedule visual updates outside the main synchronous flow
- •Avoid creating garbage in hot paths object pooling, typed arrays, and pre-allocated buffers reduce GC pressure
- •Code splitting and lazy loading reduce initial parse and compile cost only parse what you need at startup
Examples
Hidden classes and V8 optimisation keeping object shapes consistent
// BAD: different property insertion order = different hidden classes
function makePoint_BAD(x, y, hasZ) {
const p = {};
p.x = x;
p.y = y;
if (hasZ) p.z = 0; // some points have 'z', some don't two different shapes
return p;
}
// GOOD: always the same shape V8 optimises call sites with one hidden class
function makePoint_GOOD(x, y, z = 0) {
return { x, y, z }; // all three properties always present, same shape
}
// BAD: changing a property's type deoptimises functions that use it
const counter = { count: 0 };
counter.count = 0; // integer V8 specialises for Smi (small int)
counter.count = 'reset'; // now a string deoptimises anything that reads count
// GOOD: keep types stable
const c = { count: 0 };
c.count = 1; // always a number
c.count = 2; // still a number stays optimised
// Measuring with performance.now()
const start = performance.now();
for (let i = 0; i < 1_000_000; i++) makePoint_GOOD(i, i, 0);
console.log(`Good points: ${(performance.now() - start).toFixed(2)}ms`);In V8 the first call to a function specialises it for the types and shapes it received. A deoptimisation is expensive avoid it in hot loops.
Fixing layout thrashing in DOM manipulation
// BAD: forces layout recalculation on every iteration
function badResize(elements) {
for (const el of elements) {
const width = el.offsetWidth; // READ forces layout
el.style.width = width * 2 + 'px'; // WRITE invalidates layout
// Each iteration: READ forces layout because previous WRITE dirtied it
}
}
// GOOD: batch all reads then all writes
function goodResize(elements) {
// Phase 1: read all values (one layout calculation)
const widths = elements.map(el => el.offsetWidth);
// Phase 2: write all values (one layout invalidation, batch applied)
elements.forEach((el, i) => {
el.style.width = widths[i] * 2 + 'px';
});
}
// BEST: schedule in rAF, keep reads and writes separate
function rafResize(elements) {
requestAnimationFrame(() => {
const widths = elements.map(el => el.offsetWidth);
requestAnimationFrame(() => {
elements.forEach((el, i) => {
el.style.width = widths[i] * 2 + 'px';
});
});
});
}FastDOM is a popular library that automates batching of DOM reads and writes across the entire codebase.
Object pooling to reduce GC pressure in hot paths
// Without pooling: allocates a new object on every call
function createParticle_SLOW(x, y) {
return { x, y, vx: Math.random() - 0.5, vy: Math.random() - 0.5, life: 1 };
}
// Object pool: reuse objects instead of allocating new ones
class ParticlePool {
#pool = [];
#active = new Set();
acquire(x, y) {
const p = this.#pool.pop() ?? { x: 0, y: 0, vx: 0, vy: 0, life: 0 };
p.x = x;
p.y = y;
p.vx = Math.random() - 0.5;
p.vy = Math.random() - 0.5;
p.life = 1;
this.#active.add(p);
return p;
}
release(p) {
this.#active.delete(p);
this.#pool.push(p); // return to pool for reuse
}
update(dt) {
for (const p of this.#active) {
p.x += p.vx * dt;
p.y += p.vy * dt;
p.life -= dt;
if (p.life <= 0) this.release(p);
}
}
}
const pool = new ParticlePool();
// In a 60fps game loop: no GC pressure from particle allocation
for (let i = 0; i < 100; i++) pool.acquire(Math.random() * 800, 0);
pool.update(0.016);Object pooling shines in animation loops, game engines, and any scenario where the same shape of object is created and discarded at high frequency.
Common Mistakes
- Optimising before profiling most "obvious" optimisations don't move the needle; the real bottleneck is always somewhere unexpected
- Using the arguments object in hot functions it prevents optimisation in V8; use rest parameters (...args) instead
- Interleaving DOM reads and writes in a loop this causes forced synchronous layouts, also known as "layout thrashing"
- Creating new functions inside render loops (e.g. new closures on every frame) these generate GC pressure and can cause jank
Note
The 80/20 rule applies strongly to performance: 80% of execution time is typically in 20% of the code. Profile first with the Chrome Performance panel, identify the actual hot functions, and optimise those. A 10x speedup in a function that accounts for 5% of time gives you a 0.5% total improvement not worth it.
Exercises
Write JavaScript code to solve each exercise. Run it and check your output.
Completed: 0 / 5 exercises
Knowledge Check
Question 1 of 4