🚀AdvancedPerformance & Memory

Performance Optimization

Profile JavaScript execution, understand V8 optimisation internals, and apply concrete techniques to reduce parse time, layout thrashing, and unnecessary allocations.

Overview

Performance in JavaScript operates on multiple levels. The first is algorithmic complexity no amount of micro-optimisation recovers O(n²) behaviour that should be O(n log n). After that, you have parse and compile time (how quickly the browser turns text into bytecode), execution time (hot-path CPU cycles), and rendering performance (layout, paint, and composite in the browser). Each layer requires different measurement tools and different fixes. V8, Chrome's JavaScript engine, employs a tiered compilation strategy. Code starts interpreted by Ignition (fast startup, no optimisation), and functions that are called frequently ("hot" functions) are compiled by TurboFan to optimised machine code. TurboFan builds on assumptions (called hidden classes and inline caches) about object shapes and types. If you violate these assumptions by adding properties after construction, changing a property's type, or deleting properties V8 deoptimises the function and falls back to interpreted execution. Browser rendering performance is a separate concern. JavaScript that runs during animation frames must complete in under 16ms to maintain 60fps. The main thread bottleneck is layout thrashing: reading a layout property (offsetHeight, getBoundingClientRect) forces the browser to synchronously recalculate layout, then writing a style triggers another layout alternating reads and writes in a loop can be hundreds of times slower than batching all reads then all writes. requestAnimationFrame, the Intersection Observer, and ResizeObserver exist specifically to avoid this.

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.

Loading editor…

Completed: 0 / 5 exercises

Knowledge Check

Question 1 of 4

What is a "hidden class" in V8, and why does it matter for performance?