JavaScript Performance Optimization
Performance optimization means making your JavaScript code run faster and consume fewer resources. A slow app loses users — research shows even a one-second delay in page load reduces conversions. Optimization covers how you write code, how you handle the DOM, how you manage memory, and how you load scripts.
Measure Before You Optimize
Never guess where the slowdown is. Use the browser's built-in performance API and DevTools profiler to find the actual bottleneck.
// Measure how long a block of code takes
performance.mark("start");
// ... your code here ...
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += i;
}
performance.mark("end");
performance.measure("My operation", "start", "end");
let [measure] = performance.getEntriesByName("My operation");
console.log("Time taken:", measure.duration.toFixed(2), "ms");
1. Minimize DOM Access
Reading from or writing to the DOM is slow. Every access forces the browser to recalculate layout. Batch reads and writes, and store DOM references in variables instead of querying repeatedly.
Slow: Repeated DOM Queries
// BAD — queries the DOM 1000 times
for (let i = 0; i < 1000; i++) {
document.getElementById("list").innerHTML += "<li>Item " + i + "</li>";
}
Fast: Build String First, Update DOM Once
// GOOD — touches the DOM only once
let html = "";
for (let i = 0; i < 1000; i++) {
html += "<li>Item " + i + "</li>";
}
document.getElementById("list").innerHTML = html;
Diagram: DOM Write Batching
Slow approach: Loop 1000 times: read DOM → reflow → write DOM → reflow 1000 reflows — very slow! Fast approach: Loop 1000 times: build string in memory (no DOM) Write once → 1 reflow — fast!
2. Use DocumentFragment for Bulk DOM Insertion
let fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
let li = document.createElement("li");
li.textContent = "Item " + i;
fragment.appendChild(li); // adding to fragment, not the real DOM
}
// Insert all items at once with a single reflow
document.getElementById("list").appendChild(fragment);
3. Debounce and Throttle Event Handlers
Events like scroll, resize, and keypress fire very rapidly. Running expensive code on every single event causes lag. Debounce and throttle control how often your code runs.
Diagram: Debounce vs Throttle
User types: k-e-y-s-t-r-o-k-e-s (fires 10 events) Without control: runs 10 times immediately Debounce (wait for pause): k e y s t r o k e s ...pause... → runs ONCE after pause Good for: search input, form validation Throttle (limit rate): k . . e . . s . . t . → runs every 200ms regardless of speed Good for: scroll handlers, resize handlers
Debounce Implementation
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
let searchInput = document.getElementById("search");
let handleSearch = debounce(function(event) {
console.log("Searching for:", event.target.value);
// API call here — runs only after user stops typing for 300ms
}, 300);
searchInput.addEventListener("input", handleSearch);
Throttle Implementation
function throttle(fn, limit) {
let lastCall = 0;
return function(...args) {
let now = Date.now();
if (now - lastCall >= limit) {
lastCall = now;
fn(...args);
}
};
}
let handleScroll = throttle(function() {
console.log("Scroll position:", window.scrollY);
}, 200); // fires at most once every 200ms
window.addEventListener("scroll", handleScroll);
4. Use requestAnimationFrame for Animations
Never use setTimeout or setInterval for animations. Use requestAnimationFrame — it syncs with the browser's paint cycle and pauses when the tab is not visible.
let box = document.getElementById("box");
let position = 0;
function animate() {
position += 2;
box.style.left = position + "px";
if (position < 400) {
requestAnimationFrame(animate); // schedule next frame
}
}
requestAnimationFrame(animate); // start the animation
Diagram: requestAnimationFrame vs setInterval
setInterval(fn, 16): Fires every 16ms regardless of browser readiness Can fire out of sync with screen refresh → jank requestAnimationFrame(fn): Fires exactly when browser is ready to paint a new frame Pauses when tab is inactive → saves CPU Result: smooth 60fps animations
5. Lazy Loading
Load resources only when they are needed — not all at page start.
Lazy Load Images (HTML attribute)
<img src="photo.jpg" loading="lazy" alt="Product photo">
Lazy Load a JavaScript Module
// Only import the heavy chart library when the user opens the dashboard
document.getElementById("open-dashboard").addEventListener("click", async function() {
let { renderChart } = await import("./chart.js");
renderChart();
});
6. Avoid Memory Leaks
Memory leaks happen when objects are accidentally kept in memory long after they're needed — often caused by forgotten event listeners or closures holding references.
// LEAK — event listener never removed
function setup() {
let data = new Array(1000000).fill("x"); // large array
document.addEventListener("click", function() {
console.log(data.length); // closure keeps 'data' alive forever
});
}
// FIX — remove the listener when done
function setup() {
let data = new Array(1000000).fill("x");
function handleClick() {
console.log(data.length);
}
document.addEventListener("click", handleClick);
// Later, when you no longer need it:
document.removeEventListener("click", handleClick);
}
7. Use Web Workers for Heavy Tasks
JavaScript runs on one thread. A heavy calculation blocks the UI, making the page freeze. Move it to a Web Worker — a separate thread — so the UI stays responsive.
// Main thread — offload heavy work
let worker = new Worker("worker.js");
worker.postMessage({ numbers: [1, 2, 3, ...large array] });
worker.onmessage = function(event) {
console.log("Result:", event.data.result); // UI updates smoothly
};
// worker.js — runs on a separate thread
self.onmessage = function(event) {
let sum = event.data.numbers.reduce((a, b) => a + b, 0);
self.postMessage({ result: sum });
};
Diagram: Web Worker Separation
Main Thread: Worker Thread:
UI rendering Heavy computation
Event handling (no DOM access)
User interaction Data processing
postMessage() ────────► receives data
processes it
receives result ◄────── postMessage()
Both run at the same time — UI stays smooth
8. Script Loading Strategies
<!-- Blocks HTML parsing — avoid for non-critical scripts -->
<script src="app.js"></script>
<!-- Downloads in parallel, executes after HTML parsed -->
<script src="app.js" defer></script>
<!-- Downloads in parallel, executes immediately when ready -->
<script src="app.js" async></script>
Diagram: defer vs async
Normal: HTML parse ──stop── Download → Execute → continue HTML defer: HTML parse ────────────────────────────── Download → Execute async: HTML parse ───── Download → Execute (interrupts HTML parse) Use defer for most scripts. Use async only for independent scripts (analytics).
Quick Performance Checklist
| Tip | Benefit |
|---|---|
| Cache DOM references in variables | Fewer DOM queries |
| Batch DOM updates with fragments | Fewer reflows |
| Debounce search / resize handlers | Fewer redundant calls |
| Throttle scroll handlers | Limits execution rate |
| Use requestAnimationFrame | Smooth animations |
| Lazy load images and modules | Faster initial load |
| Remove unused event listeners | Prevents memory leaks |
| Use Web Workers for CPU tasks | Keeps UI responsive |
| Use defer on script tags | Non-blocking page load |
Summary
JavaScript performance optimization focuses on minimizing DOM interactions, controlling event handler frequency with debounce and throttle, using requestAnimationFrame for animations, lazy loading assets, preventing memory leaks by removing unused listeners, and offloading heavy computation to Web Workers. Always measure with the performance API and DevTools before optimizing — fix real bottlenecks, not guessed ones.
