JavaScript Performance Optimization

Reviewed & published by Brayan K

Performance optimization in JavaScript is the practice of making code run faster and feel more responsive by reducing wasted work, minimizing costly DOM updates, managing memory carefully, and avoiding blocking the main thread.

Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Performance testing requires a real browser environment. Download Node.js to run JavaScript, use Chrome DevTools Performance tab for profiling, or create a .html file to test DOM operations.

Master event loop mechanics, DOM optimization, memory management, and production-grade performance patterns.

What You'll Learn

The Golden Rule: Avoid Blocking the Main Thread

JavaScript runs on a single-threaded event loop. Any task longer than 50ms is considered "slow" and causes frame drops. The key is breaking heavy work into smaller chunks.

// Understanding the Main Thread
// JavaScript runs on a SINGLE thread - long tasks block EVERYTHING

// ❌ BAD - This blocks the UI for seconds
function blockingTask() {
  const start = Date.now();
  // Simulate heavy computation
  let count = 0;
  for (let i = 0; i < 100000000; i++) {
    count += Math.sqrt(i);
  }
  console.log("Blocking task took:", Date.now() - start, "ms");
  return count;
}

// ✅ GOOD - Break into chunks using setTimeout
function nonBlockingTask(callback) {
  const chunkSize = 1000000;
  let i = 0;
  let count = 0;
  
  function processChunk() {
    const end = Math.min(i + chunkSize, 100000000);
    
    while (i < end) {
      count += Math.sqrt(i);
      i++;
    }
    
    if (i < 100000000) {
      // Yield to event loop - UI can update!
      setTimeout(processChunk, 0);
    } else {
      callback(count);
    }
  }
  
  processChunk();
}

// Demo the difference
console.log("Starting non-blocking task...");
console.log("UI would stay responsive during this!");

// The 50ms rule: Any task > 50ms causes frame drops
console.log("\n💡 Chrome considers tasks > 50ms as 'slow'");
console.log("Break heavy work into smaller chunks!");

Debouncing & Throttling Events

Events like scroll, resize, and input can fire hundreds of times per second. Debounce waits for activity to stop; Throttle limits execution rate.

// Debouncing & Throttling - Essential for Event Performance

// DEBOUNCE: Wait until user STOPS doing something
function debounce(fn, delay) {
  let timeoutId;
  
  return function(...args) {
    // Cancel previous timer
    clearTimeout(timeoutId);
    
    // Set new timer
    timeoutId = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

// THROTTLE: Run at most once per interval
function throttle(fn, limit) {
  let lastCall = 0;
  
  return function(...args) {
    const now = Date.now();
    
    if (now - lastCall >= limit) {
      lastCall = now;
      fn.apply(this, args);
    }
  };
}

// Practical examples
const debouncedSearch = debounce((query) => {
  console.log("Searching for:", query);
  // API call here
}, 300);

const throttledScroll = throttle(() => {
  console.log("Scroll position:", window.scrollY);
  // Update UI here
}, 100);

// Test debounce - only logs once after typing stops
console.log("=== Debounce Demo ===");
debouncedSearch("h");
debouncedSearch("he");
debouncedSearch("hel");
debouncedSearch("hell");
debouncedSearch("hello"); // Only this fires after 300ms

// Test throttle
console.log("\n=== Throttle Demo ===");
for (let i = 0; i < 5; i++) {
  throttledScroll(); // Only some will execute
}

console.log("\n💡 Use debounce for: search, resize, validation");
console.log("💡 Use throttle for: scroll, mousemove, game loops");

DOM Optimization — The DOM is Slow

DOM operations are some of the slowest parts of JavaScript. Batch updates, use DocumentFragment, and minimize layout calculations.

// DOM Optimization - The DOM is SLOW!

// ❌ BAD - Adding elements one by one (1000 layout updates)
function slowAppend(container) {
  console.time("Slow append");
  for (let i = 0; i < 1000; i++) {
    const div = document.createElement("div");
    div.textContent = "Item " + i;
    container.appendChild(div); // Layout update each time!
  }
  console.timeEnd("Slow append");
}

// ✅ GOOD - Using DocumentFragment (1 layout update)
function fastAppend(container) {
  console.time("Fast append");
  const fragment = document.createDocumentFragment();
  
  for (let i = 0; i < 1000; i++) {
    const div = document.createElement("div");
    div.textContent = "Item " + i;
    fragment.appendChild(div);
  }
  
  container.appendChild(fragment); // ONE update!
  console.timeEnd("Fast append");
}

// ✅ EVEN FASTER - Using innerHTML with template
function fastestAppend(container) {
  console.time("Fastest append");
  const html = Array.from({ length: 1000 }, (_, i) => 
    "<div>Item " + i + "</div>"
  ).join("");
  
  container.innerHTML = html;
  console.timeEnd("Fastest append");
}

// Demo (simulated)
console.log("DOM Performance Comparison:");
console.log("Slow (individual appends): ~50ms");
console.log("Fast (DocumentFragment): ~5ms");
console.log("Fastest (innerHTML): ~2ms");

console.log("\n💡 Key DOM optimizations:");
console.log("1. Batch DOM updates");
console.log("2. Use DocumentFragment");
console.log("3. Clone templates");
console.log("4. Minimize layout reads/writes");

Layout Thrashing — The Silent Killer

Alternating between reading and writing layout properties forces the browser to recalculate layout on every operation. This can make code 20-100× slower.

// Layout Thrashing - The Silent Performance Killer

// ❌ BAD - Forces layout calculation on EVERY iteration
function layoutThrashing(boxes) {
  console.time("Layout thrashing");
  
  for (let i = 0; i < boxes.length; i++) {
    // READ - forces layout calculation
    const width = boxes[i].offsetWidth;
    
    // WRITE - invalidates layout
    boxes[i].style.width = (width + 10) + "px";
    // Next read will force ANOTHER layout!
  }
  
  console.timeEnd("Layout thrashing");
}

// ✅ GOOD - Batch reads, then batch writes
function optimizedLayout(boxes) {
  console.time("Optimized layout");
  
  // BATCH ALL READS FIRST
  const widths = boxes.map(box => box.offsetWidth);
  
  // THEN BATCH ALL WRITES
  boxes.forEach((box, i) => {
    box.style.width = (widths[i] + 10) + "px";
  });
  
  console.timeEnd("Optimized layout");
}

// Properties that trigger layout (EXPENSIVE):
const layoutTriggers = [
  "offsetTop", "offsetLeft", "offsetWidth", "offsetHeight",
  "scrollTop", "scrollLeft", "scrollWidth", "scrollHeight",
  "clientTop", "clientLeft", "clientWidth", "clientHeight",
  "getComputedStyle()", "getBoundingClientRect()"
];

console.log("Properties that trigger layout:");
layoutTriggers.forEach(prop => console.log("  - " + prop));

console.log("\n💡 Rule: Read all values first, then write all values");
console.log("💡 Never alternate between reads and writes in a loop!");

GPU-Accelerated Animations

Animating transform and opacity runs on the GPU and is incredibly fast. Animating top, left, width triggers expensive layout.

// GPU-Accelerated Animations

// ❌ SLOW - Animating layout properties
// These trigger layout + paint on EVERY frame
const slowAnimationProperties = [
  "top", "left", "right", "bottom",
  "width", "height", "margin", "padding"
];

// ✅ FAST - GPU-accelerated properties
// These only trigger composite - runs on GPU!
const fastAnimationProperties = [
  "transform",  // translateX, translateY, scale, rotate
  "opacity"     // fade in/out
];

// Example: Smooth animation using requestAnimationFrame
function animateBox(box) {
  let position = 0;
  let lastTime = 0;
  
  function animate(currentTime) {
    // Calculate delta time for smooth animation
    const deltaTime = currentTime - lastTime;
    lastTime = currentTime;
    
    // Move at consistent speed regardless of frame rate
    position += deltaTime * 0.1;
    
    // ✅ Use transform instead of left/top
    box.style.transform = "translateX(" + position + "px)";
    
    if (position < 500) {
      requestAnimationFrame(animate);
    }
  }
  
  requestAnimationFrame(animate);
}

console.log("=== Animation Performance ===");
console.log("\n❌ SLOW (triggers layout):");
slowAnimationProperties.forEach(p => console.log("  - " + p));

console.log("\n✅ FAST (GPU-accelerated):");
fastAnimationProperties.forEach(p => console.log("  - " + p));

console.log("\n💡 Use transform: translateX/Y instead of top/left");
console.log("💡 Use opacity instead of visibility for fade");
console.log("💡 Add will-change: transform for animation hints");

Memoization — Cache Expensive Results

If something is expensive, compute it once and cache the result. Memoization can turn exponential time complexity into linear.

// Memoization - Cache Expensive Calculations

// Basic memoization function
function memoize(fn) {
  const cache = new Map();
  
  return function(...args) {
    const key = JSON.stringify(args);
    
    if (cache.has(key)) {
      console.log("Cache hit for:", key);
      return cache.get(key);
    }
    
    console.log("Computing for:", key);
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

// Expensive calculation
function fibonacci(n) {
  if (n < 2) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

// Without memoization: O(2^n) - EXPONENTIAL
// With memoization: O(n) - LINEAR

// Memoized version
const memoFib = memoize(function fib(n) {
  if (n < 2) return n;
  return memoFib(n - 1) + memoFib(n - 2);
});

console.log("=== Memoization Demo ===");
console.log("\nComputing fibonacci(10):");
console.log("Result:", memoFib(10));

console.log("\nComputing fibonacci(10) again:");
console.log("Result:", memoFib(10)); // Cache hit!

console.log("\nComputing fibonacci(15):");
console.log("Result:", memoFib(15)); // Uses cached fib(10)!

// Practical memoization with TTL (Time To Live)
function memoizeWithTTL(fn, ttl = 60000) {
  const cache = new Map();
  
  return function(...args) {
    const key = JSON.stringify(args);
    const cached = cache.get(key);
    
    if (cached && Date.now() - cached.time < ttl) {
      return cached.value;
    }
    
    const result = fn.apply(this, args);
    cache.set(key, { value: result, time: Date.now() });
    return result;
  };
}

console.log("\n💡 Use memoization for:");
console.log("- Expensive calculations");
console.log("- API response caching");
console.log("- Computed values from props");

🎯 Your Turn — Prove the Cache Works

A memoised function looks identical from the outside, which is exactly why it is easy to write one that never actually caches anything. So this exercise counts the real calls. Three blanks, and the second number in the output is the marker: get it right and the expensive work happens three times instead of seven.

// 🎯 YOUR TURN - fill in the three blanks marked ___
// Memoisation means "remember what you already worked out". The counter
// proves whether your cache is actually being used.

let calls = 0;
function slowSquare(n) {
  calls++;                 // pretend this line is an expensive calculation
  return n * n;
}

// 1) The cache needs a key/value store that handles any key type.
function memoize(fn) {
  const cache = new ___();          // 👉 replace ___ with Map
  return function (n) {
    if (cache.___(n)) return cache.get(n);   // 👉 replace ___ with has
    const result = fn(n);           // a miss: do the real work this once
    cache.set(n, result);           // then remember it
    return result;
  };
}

const fastSquare = memoize(slowSquare);

const inputs = [4, 7, 4, 4, 7, 9, 4];
const results = inputs.map(___);    // 👉 replace ___ with fastSquare
console.log("results: " + results.join(", "));
console.log("slowSquare actually ran " + calls + " times for " + inputs.length + " calls");

// ✅ Expected output once the blanks are filled:
// results: 16, 49, 16, 16, 49, 81, 16
// slowSquare actually ran 3 times for 7 calls
//
// Three, because there are only three DIFFERENT inputs: 4, 7 and 9. If your
// second line says 7, the cache is never being hit - check blank 2.

Efficient Data Structures

Choosing the wrong data structure can cause massive performance loss. Use Map for key-value, Set for membership testing.

// Efficient Data Structures

// Object vs Map for lookups
console.log("=== Object vs Map Performance ===");

// Object lookup
const userObject = {};
for (let i = 0; i < 10000; i++) {
  userObject["user_" + i] = { id: i, name: "User " + i };
}

// Map lookup (FASTER for frequent operations)
const userMap = new Map();
for (let i = 0; i < 10000; i++) {
  userMap.set("user_" + i, { id: i, name: "User " + i });
}

console.log("Object has prototype chain overhead");
console.log("Map is optimized for frequent additions/deletions");

// Array vs Set for membership testing
console.log("\n=== Array vs Set for 'includes' ===");

const array = Array.from({ length: 10000 }, (_, i) => i);
const set = new Set(array);

// Array.includes() is O(n)
// Set.has() is O(1)

console.log("Array.includes(): O(n) - searches linearly");
console.log("Set.has(): O(1) - instant lookup");

// Practical example
function findDuplicatesWithArray(items) {
  const seen = [];
  return items.filter(item => {
    if (seen.includes(item)) return true;
    seen.push(item);
    return false;
  });
  // O(n²) - SLOW for large arrays
}

function findDuplicatesWithSet(items) {
  const seen = new Set();
  return items.filter(item => {
    if (seen.has(item)) return true;
    seen.add(item);
    return false;
  });
  // O(n) - FAST!
}

console.log("\n💡 Use Map when:");
console.log("- Keys can be any type");
console.log("- Frequent additions/deletions");
console.log("- Need .size property");

console.log("\n💡 Use Set when:");
console.log("- Checking membership");
console.log("- Removing duplicates");
console.log("- Set operations (union, intersection)");

Worked Example — Count the Work, Not the Milliseconds

Benchmarks in milliseconds are noisy: a background tab, a garbage collection, a laptop dropping its clock speed, and your numbers move. So this example counts operations instead, which is the same on every machine, and prints the timings only as a footnote.

The job is simple — find the ids that appear in both of two lists — done twice. Once by scanning, once with a Set. Same answer both times. Ten million comparisons versus four thousand lookups.

// WORKED EXAMPLE - the biggest performance win is almost never a clever trick.
// It is choosing a data structure whose cost does not explode as data grows.

// Two lists of ids that half overlap.
const N = 4000;
const listA = [];
const listB = [];
for (let i = 0; i < N; i++) listA.push(i);
for (let i = N / 2; i < N + N / 2; i++) listB.push(i);

// ---------- SLOW: for every id in A, scan through B ----------
// The inner loop is written out rather than using .includes() so the
// comparisons can be counted. .includes() does exactly this work internally.
let comparisons = 0;
function commonSlow(a, b) {
  const found = [];
  for (const x of a) {
    for (const y of b) {
      comparisons++;
      if (x === y) { found.push(x); break; }   // stop early once matched
    }
  }
  return found;
}

const slowStart = Date.now();
const slowResult = commonSlow(listA, listB);
const slowMs = Date.now() - slowStart;

// ---------- FAST: put B into a Set once, then ask it directly ----------
// A Set answers "do you contain this?" in roughly one step, however big it is.
let lookups = 0;
function commonFast(a, b) {
  const index = new Set(b);      // built once: b.length steps
  const found = [];
  for (const x of a) {
    lookups++;
    if (index.has(x)) found.push(x);
  }
  return found;
}

const fastStart = Date.now();
const fastResult = commonFast(listA, listB);
const fastMs = Date.now() - fastStart;

// ---------- Same answer, wildly different amount of work ----------
console.log("matches found (slow): " + slowResult.length);
console.log("matches found (fast): " + fastResult.length);
console.log("same answer? " + (slowResult.length === fastResult.length));
console.log("slow did " + comparisons + " comparisons");
console.log("fast did " + lookups + " lookups (plus one pass to build the Set)");

// Milliseconds depend on your machine, so they are not written below as an
// expected value. The comparison counts already tell you the story.
console.log("(this run: slow " + slowMs + "ms, fast " + fastMs + "ms - your numbers will differ)");

// ✅ Expected output (the first five lines are the same on every machine):
// matches found (slow): 2000
// matches found (fast): 2000
// same answer? true
// slow did 10001000 comparisons
// fast did 4000 lookups (plus one pass to build the Set)
//
// A sixth line follows with this run's milliseconds. It is left out of the
// expected output on purpose: your machine decides it.
//
// Ten million comparisons against four thousand, for an identical answer.
// Now change N from 4000 to 8000 and run it again: the fast count doubles,
// the slow count roughly QUADRUPLES. That gap is what "O(n squared)" means
// in practice, and it is why this beats every micro-optimisation you will
// ever read about.

🎯 Mini-Challenge — One Pass Instead of N Passes

Now the same idea on the shape of code you will genuinely write: grouping a list of orders by customer. The obvious version loops the whole order list once per customer. The fast version reads the orders once and builds an index as it goes.

The slow version and the step counters are provided. You write groupFast — brief in the comments, no starter logic.

// 🎯 MINI-CHALLENGE: one pass instead of N passes
//
// Grouping a list "per customer" by filtering once per customer is the most
// common accidental O(n x m) in real code. The slow version is written for
// you, with a step counter. You write the one-pass version.
//
// Write groupFast(orders):
//   1. const index = new Map()
//   2. for each order: increment fastSteps, then - if index does not already
//      have order.customer - set that key to an empty array. Push order.id
//      onto index.get(order.customer).
//   3. return index
//
// Then print each customer and their ids (a Map can be looped with
//   for (const [key, value] of theMap) ), and finally both step counts.

const orders = [
  { id: "o1", customer: "ann" }, { id: "o2", customer: "bob" }, { id: "o3", customer: "cat" },
  { id: "o4", customer: "ann" }, { id: "o5", customer: "bob" }, { id: "o6", customer: "cat" },
  { id: "o7", customer: "ann" }, { id: "o8", customer: "bob" }, { id: "o9", customer: "cat" }
];
const customers = ["ann", "bob", "cat"];

let slowSteps = 0;
function groupSlow(orders, customers) {          // already written
  const out = new Map();
  for (const c of customers) {
    const mine = [];
    for (const o of orders) {
      slowSteps++;
      if (o.customer === c) mine.push(o.id);
    }
    out.set(c, mine);
  }
  return out;
}

let fastSteps = 0;

// your code here

groupSlow(orders, customers);
const grouped = groupFast(orders);
for (const [customer, ids] of grouped) {
  console.log(customer + ": " + ids.join(", "));
}
console.log("slow took " + slowSteps + " steps, fast took " + fastSteps);

// ✅ Expected output:
// ann: o1, o4, o7
// bob: o2, o5, o8
// cat: o3, o6, o9
// slow took 27 steps, fast took 9
//
// Three times the work for three customers. With 500 customers it is 500
// times the work - and the fast version still takes exactly 9 steps.

Memory Management & Leak Prevention

Memory leaks cause apps to slow down and crash. Always clean up event listeners, clear timers, and avoid capturing large data in closures.

// Memory Management & Garbage Collection

// ❌ Memory Leak #1: Forgotten Event Listeners
function leakyComponent() {
  const handler = () => console.log("Click!");
  
  // Listener added but never removed
  document.addEventListener("click", handler);
  
  // When component is destroyed, listener remains!
}

// ✅ Fixed: Clean up listeners
function cleanComponent() {
  const handler = () => console.log("Click!");
  
  document.addEventListener("click", handler);
  
  // Return cleanup function
  return () => {
    document.removeEventListener("click", handler);
  };
}

// ❌ Memory Leak #2: Closures capturing large data
function leakyClosure() {
  const hugeData = new Array(1000000).fill("x");
  
  return function() {
    // hugeData is captured and can't be garbage collected!
    console.log(hugeData.length);
  };
}

// ✅ Fixed: Only capture what you need
function cleanClosure() {
  const hugeData = new Array(1000000).fill("x");
  const length = hugeData.length; // Extract what we need
  
  return function() {
    console.log(length); // hugeData can now be collected
  };
}

// ❌ Memory Leak #3: Timers not cleared
function leakyTimer() {
  setInterval(() => {
    console.log("Running forever...");
  }, 1000);
  // Never cleared!
}

// ✅ Fixed: Clear timers
function cleanTimer() {
  const intervalId = setInterval(() => {
    console.log("Running...");
  }, 1000);
  
  // Clear when done
  return () => clearInterval(intervalId);
}

// Object pooling to reduce GC pressure
class ObjectPool {
  constructor(factory, reset) {
    this.pool = [];
    this.factory = factory;
    this.reset = reset;
  }
  
  acquire() {
    return this.pool.pop() || this.factory();
  }
  
  release(obj) {
    this.reset(obj);
    this.pool.push(obj);
  }
}

// Usage
const vectorPool = new ObjectPool(
  () => ({ x: 0, y: 0 }),
  (v) => { v.x = 0; v.y = 0; }
);

console.log("=== Memory Management ===");
console.log("\nCommon memory leaks:");
console.log("1. Forgotten event listeners");
console.log("2. Closures capturing large data");
console.log("3. Timers not cleared");
console.log("4. Detached DOM nodes");
console.log("5. Growing caches without limits");

Microtasks vs Macrotasks

Understanding the event loop is crucial for performance. Microtasks (Promises) run before rendering; macrotasks (setTimeout) run after.

// Microtasks vs Macrotasks

// MICROTASKS (run BEFORE next render):
// - Promise callbacks
// - queueMicrotask()
// - MutationObserver

// MACROTASKS (run AFTER render):
// - setTimeout
// - setInterval
// - setImmediate
// - I/O callbacks
// - UI rendering

console.log("=== Event Loop Demo ===");

console.log("1. Synchronous code");

setTimeout(() => {
  console.log("4. setTimeout (macrotask)");
}, 0);

Promise.resolve().then(() => {
  console.log("3. Promise.then (microtask)");
});

queueMicrotask(() => {
  console.log("3b. queueMicrotask (microtask)");
});

console.log("2. More synchronous code");

// Output order:
// 1. Synchronous code
// 2. More synchronous code
// 3. Promise.then (microtask)
// 3b. queueMicrotask (microtask)
// 4. setTimeout (macrotask)

console.log("\n💡 Why this matters:");
console.log("- Heavy microtasks BLOCK rendering");
console.log("- Use setTimeout to yield to render");

// ❌ BAD - Blocks render with heavy microtask
function badHeavyWork() {
  Promise.resolve().then(() => {
    // Heavy work here blocks the next frame!
    for (let i = 0; i < 10000000; i++) {}
  });
}

// ✅ GOOD - Yields to allow render
function goodHeavyWork() {
  setTimeout(() => {
    // Browser can render first
    for (let i = 0; i < 10000000; i++) {}
  }, 0);
}

// ✅ BEST - Use requestIdleCallback for non-urgent work
function bestHeavyWork() {
  // Runs only when browser is idle
  requestIdleCallback(() => {
    // Process in spare time
  });
}

Lazy Loading & Virtual Scrolling

Only load what the user needs right now. Dynamic imports, lazy images, and virtual scrolling can dramatically improve perceived performance.

Performance Profiling & Measurement

You can't optimize what you can't measure. Use the Performance API, console.time, and Chrome DevTools to find bottlenecks.

⚡ Performance Mastery Summary

Practice quiz

According to the golden rule, any task longer than how many milliseconds is considered slow and causes frame drops?

  • 10ms
  • 500ms
  • 50ms
  • 5000ms

Answer: 50ms. Chrome considers tasks over 50ms as slow; break heavy work into chunks.

How does the lesson keep a heavy computation from blocking the UI?

  • Break it into chunks and yield to the event loop (e.g. setTimeout)
  • Run it twice
  • Use a bigger loop
  • Disable rendering

Answer: Break it into chunks and yield to the event loop (e.g. setTimeout). Splitting work into chunks and yielding lets the UI update between chunks.

Debounce is best used for which scenarios?

  • scroll and game loops
  • GPU animation
  • garbage collection
  • search, resize, and validation

Answer: search, resize, and validation. Debounce waits for activity to stop, ideal for search, resize, and validation.

Which DOM approach causes only ONE layout update for 1000 elements?

  • appendChild one by one
  • Using a DocumentFragment
  • Reading offsetWidth each time
  • Calling getBoundingClientRect

Answer: Using a DocumentFragment. A DocumentFragment batches the additions into a single update.

What causes layout thrashing?

  • Alternating layout reads and writes in a loop
  • Using const
  • Caching results
  • Using transform

Answer: Alternating layout reads and writes in a loop. Interleaving reads (offsetWidth) and writes forces repeated layout recalculation.

Which CSS properties are GPU-accelerated and best for animation?

  • top and left
  • width and height
  • transform and opacity
  • margin and padding

Answer: transform and opacity. transform and opacity only trigger compositing on the GPU; layout properties are slow.

Memoizing fibonacci changes its time complexity from O(2^n) to what?

  • O(n^2)
  • O(n)
  • O(1)
  • O(log n)

Answer: O(n). Caching subresults turns exponential O(2^n) into linear O(n).

Which runs BEFORE the next render: microtasks or macrotasks?

  • Macrotasks (setTimeout)
  • Neither
  • Both equally
  • Microtasks (Promise callbacks)

Answer: Microtasks (Promise callbacks). Microtasks like Promise callbacks run before rendering; macrotasks like setTimeout run after.

What technique renders only the visible rows of a very long list?

  • Eager loading
  • Virtual scrolling
  • Inlining
  • Layout thrashing

Answer: Virtual scrolling. Virtual scrolling renders just the visible items, e.g. 10,000 items as ~20 DOM nodes.

Which is a memory leak the lesson warns about?

  • Returning a value
  • Using Map
  • A closure capturing large data that can't be garbage collected
  • Calling performance.now()

Answer: A closure capturing large data that can't be garbage collected. Closures capturing huge data (and uncleared listeners/timers) prevent garbage collection.

Continue this course