Performance Optimization

Reviewed & published by Brayan K

Master production-level performance optimization techniques for building fast, scalable applications.

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.

What You'll Learn in This Lesson

šŸ’” Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:

šŸŽļø Real-World Analogy: Race Car Pit Crew

Think of performance optimization like a Formula 1 pit crew. A few seconds saved in the pit stop can win or lose a race. Similarly, a few hundred milliseconds saved in your JavaScript can determine whether users stay or leave. Every optimization counts: tire changes (debouncing), fuel efficiency (memoization), aerodynamics (reducing bundle size), and smooth driving (avoiding layout thrashing).

šŸ“‹ Performance Optimization Quick Guide:

ProblemTechniqueWhen to Use
Too many event firesDebounce/ThrottleSearch boxes, scroll handlers
Repeated expensive calculationsMemoizationComplex transforms, API results
Slow includes() checksUse Set insteadMembership checks in loops
UI freezing during workChunk processingLarge data processing
Large initial bundleCode splittingLazy load routes/components

Introduction

Performance is the single most important feature of any application.

Users will abandon slow apps, no matter how beautiful or feature-rich.

This lesson will teach you how to think like a performance engineer.

You'll learn how to:

By the end of this lesson, you'll be able to build apps that are not just functional, but lightning fast.

1. The Importance of Measurement

The first rule of performance optimization:

It's easy to assume where the slow parts of your code are, but assumptions are often wrong.

Always use the browser's Developer Tools to measure performance.

The most important tools are:

These tools will give you concrete data about where your app is spending its time.

2. Understanding the Event Loop

JavaScript is single-threaded, meaning it can only do one thing at a time.

The Event Loop is what allows JavaScript to handle asynchronous operations without blocking the main thread.

When you make an API call or set a timer, the browser handles that operation in the background.

When the operation is complete, the browser puts a message on the message queue.

The Event Loop constantly checks the message queue and executes any messages it finds.

If you block the main thread, the Event Loop can't process messages, and your app will freeze.

3. Avoiding Blocking Code

Blocking code is any code that takes a long time to execute and prevents the Event Loop from processing messages.

Examples of blocking code:

To avoid blocking code, use:

These techniques allow you to break up long-running tasks into smaller chunks that can be executed without blocking the main thread.

4. Debouncing and Throttling

Debouncing and throttling are techniques for limiting the rate at which a function is executed.

// These are the two classic wrappers. Nothing is printed when you run
// this — the block only DEFINES them. The next lesson (Debouncing &
// Throttling) drives them with a fake clock so you can watch them fire.

function debounce(func, delay) {
  let timeout;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(context, args), delay);
  };
}

function throttle(func, limit) {
  let lastFunc;
  let lastRan;
  return function() {
    const context = this;
    const args = arguments;
    if (!lastRan) {
      func.apply(context, args);
      lastRan = Date.now();
    } else {
      clearTimeout(lastFunc);
      lastFunc = setTimeout(function() {
        if ((Date.now() - lastRan) >= limit) {
          func.apply(context, args);
          lastRan = Date.now();
        }
      }, limit - (Date.now() - lastRan));
    }
  };
}

5. Memoization and Caching

Memoization is a technique for caching the results of expensive function calls and returning the cached result when the same inputs occur again.

Caching is a more general term for storing data so that future requests for that data can be served faster.

Memoization is a specific type of caching that applies to function calls.

Use memoization and caching to:

// memoize() takes a slow function and hands back a faster wrapper.
function memoize(func) {
  const cache = {};                          // the memory: key -> answer

  return function () {                       // this wrapper replaces func
    const key = JSON.stringify(arguments);   // e.g. calling with 10000 gives '{"0":10000}'

    // Use "in", not "if (cache[key])". A cached answer of 0, "" or false is
    // falsy, so a truthiness check would silently redo the expensive work.
    if (key in cache) {
      return cache[key];                     // cache HIT - return instantly
    }

    const result = func.apply(this, arguments); // cache MISS - do the real work
    cache[key] = result;                        // remember it for next time
    return result;
  };
}

const expensiveFunction = (n) => {
  console.log('Calculating...');   // prints once per cache MISS - your proof
  let result = 0;
  for (let i = 0; i < n; i++) {
    result += i;                   // adds 0 + 1 + 2 + ... + (n-1)
  }
  return result;
};

const memoizedFunction = memoize(expensiveFunction);

console.log(memoizedFunction(10000));  // cache miss: does the work -> 49995000
console.log(memoizedFunction(10000));  // cache hit: same answer, no work

// āœ… Expected output:
// Calculating...
// 49995000
// 49995000

Now finish a memoizer yourself. The wrapper below is written for you except for the two lines that are the cache — a lookup and a store. A Map is used instead of a plain object because a Map accepts any key type and has no inherited keys to trip over.

// šŸŽÆ YOUR TURN — fill in the blanks marked with ___

let realCalls = 0;                    // counts how often the SLOW work really ran

function slowSquare(n) {
  realCalls++;                        // proof that the real function executed
  let result = 0;
  for (let i = 0; i < n; i++) result += n;   // a deliberately slow n * n
  return result;
}

function memoize(fn) {
  const cache = new Map();            // key = the argument, value = the answer

  return function (n) {
    // 1) Have you already answered this exact argument?
    if (___) {                        // šŸ‘‰ replace ___ with  cache.has(n)
      return cache.get(n);            // cache hit - skip the slow loop entirely
    }

    // 2) First time seeing it: do the work, then remember the answer
    const answer = fn(n);
    ___;                              // šŸ‘‰ replace ___ with  cache.set(n, answer)
    return answer;
  };
}

const fastSquare = memoize(slowSquare);

console.log(fastSquare(2000));        // 4000000  (slow path)
console.log(fastSquare(2000));        // 4000000  (cached)
console.log(fastSquare(3000));        // 9000000  (slow path - new argument)
console.log(fastSquare(2000));        // 4000000  (cached)
console.log("Real calls:", realCalls);

// āœ… Expected output once both blanks are filled:
// 4000000
// 4000000
// 9000000
// 4000000
// Real calls: 2
//
// If you see "Real calls: 4" the cache is never being hit — check blank 1.

6. Data Structures and Algorithms

The choice of data structure and algorithm can have a huge impact on performance.

Choose the right data structure for the job.

Learn about Big-O notation to understand the performance characteristics of different algorithms.

Big-O is shorthand for "how does the work grow as the data grows?". O(n) means doubling the data doubles the work; O(1) means the data size doesn't matter at all. The example below makes that concrete: instead of trusting a stopwatch, it counts the comparisons each approach performs.

// Goal: check 5 usernames against a list of 50,000 already-taken names.

// Build the list of taken usernames: "user0", "user1", ... "user49999"
const taken = [];
for (let i = 0; i < 50000; i++) {
  taken.push("user" + i);
}

// The names to look up (the last one was never registered)
const wanted = ["user0", "user25000", "user49999", "user49999", "ada"];

// --- Option A: an array. includes() walks it from the front. -----------
// This copy of includes() counts each comparison so you can SEE the cost.
let arraySteps = 0;
function arrayHas(list, name) {
  for (let i = 0; i < list.length; i++) {
    arraySteps++;                      // one comparison done
    if (list[i] === name) return true; // found it - stop early
  }
  return false;                        // not found - it read the whole list
}

// --- Option B: a Set. has() hashes the name and jumps straight to it. ---
const takenSet = new Set(taken);       // built once, reused for every lookup

const arrayStart = Date.now();
const arrayAnswers = wanted.map(name => arrayHas(taken, name));
const arrayMs = Date.now() - arrayStart;

const setStart = Date.now();
const setAnswers = wanted.map(name => takenSet.has(name));
const setMs = Date.now() - setStart;

console.log("Array answers:", arrayAnswers.join(", "));
console.log("Set answers:  ", setAnswers.join(", "));
console.log("Array comparisons:", arraySteps);
console.log("Set comparisons:  ", wanted.length, "(about one per lookup)");
console.log("Array took " + arrayMs + " ms, Set took " + setMs + " ms");

// āœ… Expected output (the first four lines exactly):
// Array answers: true, true, true, true, false
// Set answers:   true, true, true, true, false
// Array comparisons: 175002
// Set comparisons:   5 (about one per lookup)
//
// A fifth line reports the milliseconds, e.g. "Array took 2 ms, Set took
// 0 ms". Those two numbers change on every run and on every machine, which
// is exactly why you count operations as well as timing. The comparison
// counts never change: the miss ("ada") is the worst case, because the
// array must read all 50,000 entries before it can say "no".

7. DOM Manipulation

DOM manipulation is often a performance bottleneck.

Minimize DOM manipulation by:

Avoid layout thrashing by:

8. Network Optimization

Network requests can be slow and expensive.

Optimize network requests by:

Reduce bundle size by:

9. Memory Management

Memory leaks can cause your app to slow down over time.

Avoid memory leaks by:

Use the browser's Memory tab to identify memory leaks.

10. Web Workers

Web Workers allow you to run JavaScript code in the background, without blocking the main thread.

Use Web Workers for:

Web Workers have limited access to the DOM, so they are best suited for tasks that don't require DOM manipulation.

Note: this one is read-only. A worker needs two separate files served over HTTP, so new Worker('worker.js') throws in the editor above (and in a file:// page). Save the two halves below as main.js and worker.js, load main.js from an HTML page on a local server, and the result appears in the browser console.

// main.js
const worker = new Worker('worker.js');

worker.postMessage({ num: 1000000000 });

worker.onmessage = (event) => {
  console.log('Result:', event.data);
};

// worker.js
self.onmessage = (event) => {
  let result = 0;
  for (let i = 0; i < event.data.num; i++) {
    result += i;
  }
  self.postMessage(result);
};

šŸŽÆ Mini-Challenge: Kill the O(n²)

Time to work without a safety net. A warehouse system scans order IDs and needs to know which ones were scanned twice. The version below works, but it compares every ID with every other ID — that is O(n²), so ten times the orders means a hundred times the work.

Your job: get the same answer with one pass and a Set.

// Build 5,000 order IDs, then add 3 duplicates on the end
const ids = [];
for (let i = 0; i < 5000; i++) ids.push("ORD-" + i);
ids.push("ORD-7", "ORD-350", "ORD-4999");

// The slow reference version — correct, but O(n²). Leave it alone; it is
// only here so you can check your own answer against it.
function findDuplicatesSlow(list) {
  const found = [];
  for (let i = 0; i < list.length; i++) {
    for (let j = i + 1; j < list.length; j++) {
      if (list[i] === list[j] && !found.includes(list[i])) found.push(list[i]);
    }
  }
  return found;
}

// šŸŽÆ MINI-CHALLENGE: write findDuplicatesFast(list)
// 1. Make a Set called "seen" and a Set called "dupes"
// 2. Loop over list ONCE. If seen already has the id, add it to dupes;
//    otherwise add it to seen.
// 3. Return the dupes as an array (spread it: [...dupes])
function findDuplicatesFast(list) {
  // your code here
}

const t0 = Date.now();
const slow = findDuplicatesSlow(ids);
const t1 = Date.now();
const fast = findDuplicatesFast(ids);
const t2 = Date.now();

console.log("Slow found:", slow.join(", "));
console.log("Fast found:", fast.join(", "));
console.log("Same answer?", slow.sort().join() === fast.sort().join());
console.log("slow ms", t1 - t0, "fast ms", t2 - t1);

// āœ… Expected output when your version is right (first three lines exactly):
// Slow found: ORD-7, ORD-350, ORD-4999
// Fast found: ORD-7, ORD-350, ORD-4999
// Same answer? true
//
// A fourth line times both, e.g. "slow ms 65 fast ms 1". Those numbers
// depend on your machine and change every run — the first three lines do not.
// The gap is the point: the same answer, roughly fifty times cheaper.
// Until you write the function the editor shows only:
//   āŒ TypeError: Cannot read properties of undefined (reading 'join')
// A function with no return statement returns undefined, and undefined has
// no .join(). That error replaces the whole console, so an empty-looking
// output usually means one throw near the end, not that nothing ran.

What You've Mastered

You've now mastered Performance Optimization at a true expert level:

This level of knowledge is what professional engineers use to build AAA game UIs, real-time dashboards, high-traffic social media sites, online shops serving millions, and web apps expected to run for hours without slowing.

šŸ“‹ Quick Reference — Performance

TechniqueUse Case
DebounceDelay execution until pause (search)
ThrottleLimit execution frequency (scroll)
MemoizeCache expensive function results
Lazy LoadLoad resources only when needed
Web WorkerRun heavy tasks off main thread

Lesson Complete — Performance!

Your apps are now faster, smoother, and more efficient. Performance is what separates junior developers from senior engineers.

Practice quiz

What is the first rule of performance optimization in this lesson?

  • Always cache everything
  • Rewrite in another language
  • Measure, don't guess
  • Avoid functions

Answer: Measure, don't guess. Measure first with DevTools and timers instead of guessing where slow code is.

Because JavaScript is single-threaded, what happens if you block the main thread?

  • The event loop can't process messages and the app freezes
  • Nothing changes
  • It speeds up
  • It spawns a new thread

Answer: The event loop can't process messages and the app freezes. Blocking the main thread stops the event loop, freezing the UI.

Which technique delays execution until the user stops triggering an event?

  • Throttling
  • Memoization
  • Tree shaking
  • Debouncing

Answer: Debouncing. Debouncing waits until activity stops, ideal for search boxes.

Which technique limits a function to run at most once per interval?

  • Debouncing
  • Throttling
  • Caching
  • Lazy loading

Answer: Throttling. Throttling caps how often a function runs, ideal for scroll handlers.

What does memoization cache?

  • The results of expensive function calls keyed by inputs
  • DOM nodes
  • Event listeners
  • Network sockets

Answer: The results of expensive function calls keyed by inputs. Memoization stores results so repeated calls with the same inputs return the cached value.

For membership checks, which is faster than Array.includes()?

  • A for loop
  • Another array
  • Set.has() which is O(1)
  • JSON.stringify

Answer: Set.has() which is O(1). Set lookup is O(1), while searching an array is O(n).

Which tools help avoid blocking the main thread with long tasks?

  • alert and confirm
  • Web Workers, setTimeout, requestAnimationFrame, chunk processing
  • innerHTML
  • document.write

Answer: Web Workers, setTimeout, requestAnimationFrame, chunk processing. These let you break or offload long-running work so the UI stays responsive.

How can you minimize costly DOM manipulation?

  • Update one node at a time in a loop
  • Use eval
  • Add more event listeners
  • Use document fragments and batch updates

Answer: Use document fragments and batch updates. Document fragments, batching, and a virtual DOM reduce expensive DOM operations.

Which is a way to reduce bundle size?

  • Add more libraries
  • Code splitting, removing unused code, and tree shaking
  • Inline everything
  • Disable caching

Answer: Code splitting, removing unused code, and tree shaking. Code splitting, dead-code removal, and tree shaking shrink the bundle.

What are Web Workers best suited for?

  • DOM-heavy rendering
  • Styling elements
  • Heavy background computation off the main thread
  • Reading cookies

Answer: Heavy background computation off the main thread. Web Workers run heavy work in the background but have limited DOM access.

Continue this course