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
- Event loop & blocking code
- Debouncing & throttling events
- Memoization & caching strategies
- Efficient DOM manipulation
- Network & bundle optimization
- Memory management & leaks
š” Running Code Locally: While this online editor runs real JavaScript, some advanced examples may have limitations. For the best experience:
- Download Node.js to run JavaScript on your computer
- Use your browser's Developer Console (Press F12) to test code snippets
- Create a .html file with <script> tags and open it in your browser
šļø 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:
| Problem | Technique | When to Use |
|---|---|---|
| Too many event fires | Debounce/Throttle | Search boxes, scroll handlers |
| Repeated expensive calculations | Memoization | Complex transforms, API results |
| Slow includes() checks | Use Set instead | Membership checks in loops |
| UI freezing during work | Chunk processing | Large data processing |
| Large initial bundle | Code splitting | Lazy 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:
- Measure performance
- Identify bottlenecks
- Apply proven optimization techniques
- Write code that scales
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:
- The Performance tab
- The Network tab
- console.time() and console.timeEnd()
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:
- Complex calculations
- Synchronous API calls
To avoid blocking code, use:
- setTimeout()
- requestAnimationFrame()
- Chunk processing
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.
- Delays execution until after a certain amount of time has passed since the last time the function was called.
- Useful for search boxes, where you only want to make an API call after the user has stopped typing.
- Limits the rate at which a function can be called.
- Useful for scroll handlers, where you only want to execute a function a certain number of times per second.
// 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:
- Reduce the number of API calls
- Speed up complex calculations
- Improve the performance of React components
// 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
// 49995000Now 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.
- Using an array to search for an element is O(n)
- Using a set to search for an element is O(1)
- Using an object to search for an element is O(1)
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:
- Using document fragments
- Batching updates
- Using virtual DOM
Avoid layout thrashing by:
- Reading and writing DOM properties in separate phases
- Using requestAnimationFrame()
8. Network Optimization
Network requests can be slow and expensive.
Optimize network requests by:
- Compressing images
- Minifying code
- Using HTTP/2
- Caching API responses
Reduce bundle size by:
- Using code splitting
- Removing unused code
- Using tree shaking
9. Memory Management
Memory leaks can cause your app to slow down over time.
Avoid memory leaks by:
- Removing event listeners
- Clearing timers
- Avoiding circular references
- Using weak references
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:
- Image processing
- Data analysis
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:
- ā Big-O complexity thinking in JavaScript
- ā Event loop & blocking code understanding
- ā Debouncing & throttling for event performance
- ā Memoization & caching strategies
- ā Data structure choices (Array, Set, Map, Object)
- ā DOM optimization & layout thrashing
- ā Smooth animations with requestAnimationFrame
- ā Performance measurement with DevTools
- ā Lazy loading images, components, and modules
- ā Web Workers for heavy computation
- ā Memory leak prevention
- ā Network & bundle optimization
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
| Technique | Use Case |
|---|---|
| Debounce | Delay execution until pause (search) |
| Throttle | Limit execution frequency (scroll) |
| Memoize | Cache expensive function results |
| Lazy Load | Load resources only when needed |
| Web Worker | Run 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
- Previous: Design Patterns
- Next: Deep Dive into JavaScript Execution Context & Call Stack ā How JS engines execute code, create frames, and manage the call stack
- Quick reference: JavaScript cheat sheet