Performance Optimisation
Making JavaScript faster — measuring before optimising, common bottlenecks, V8 engine hints, lazy loading, and memory management.
What it is, in plain English: Performance optimisation in JavaScript starts with measurement — you can't know what to fix until you know what's slow. The three main performance concerns are: runtime speed (how fast code executes), load time (how fast the page becomes interactive), and memory (avoiding leaks). The first rule: measure first, optimise second. Premature optimisation adds complexity without guaranteed benefit.
Where this bites you
Memoising an async function caches the Promise it returns, and that includes a Promise that rejected. One network blip on the first call, and for the whole cache lifetime (10 minutes in the getWeather example) every call gets the same old failure back instantly, even though the network recovered seconds later.
function memoizeWithTTL(fn, ttlMs = 5 * 60 * 1000) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
const entry = cache.get(key);
if (entry && Date.now() - entry.time < ttlMs) return entry.value;
const value = fn.apply(this, args);
cache.set(key, { value, time: Date.now() });
return value;
};
}
let networkUp = false; // the first call hits a network blip
async function fetchWeatherFromAPI(city) {
if (!networkUp) throw new Error("network error");
return `${city}: 31°C`;
}
const getWeather = memoizeWithTTL(fetchWeatherFromAPI, 10 * 60 * 1000);
await getWeather("Pune").catch(e => console.log("call 1:", e.message));
networkUp = true; // the network is back...
await getWeather("Pune").catch(e => console.log("call 2:", e.message)); // ...but the cache isn't
// call 1: network error
// call 2: network errorMeasure first, then fix
The first rule of performance: measure before optimising.
Making guesses about what's slow is almost always wrong. Use the profiler —
then fix what the data says is slow.
Measuring performance
// console.time / timeEnd — easiest way to measure a block:
console.time("data processing");
const result = processData(largeArray);
console.timeEnd("data processing");
// "data processing: 234.5ms"
// performance.now() — more precise, good for comparisons:
const start = performance.now();
doSomethingExpensive();
const elapsed = performance.now() - start;
console.log(`Took ${elapsed.toFixed(2)}ms`);
// Browser DevTools — Performance tab (the real tool):
// 1. Open DevTools → Performance tab
// 2. Click Record → do the slow thing → Stop
// 3. See a flame chart: which functions took the most time
// 4. Fix the slowest ones first (biggest blocks in the chart)Debounce and throttle — the two most impactful optimisations
// DEBOUNCE — wait until user stops doing something
// Use for: search input, form validation, resize handlers
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
const handleSearch = debounce((query) => {
fetch(`/api/search?q=${query}`); // only fires 300ms after user stops typing
}, 300);
searchInput.addEventListener("input", (e) => handleSearch(e.target.value));
// THROTTLE — run at most once per interval
// Use for: scroll events, mouse moves, window resize
function throttle(fn, interval) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
const handleScroll = throttle(() => updateScrollIndicator(), 100);
window.addEventListener("scroll", handleScroll);
// Max 10 updates per second, no matter how fast the user scrollsAvoiding common slow patterns
// ✗ Slow — querying DOM inside a loop:
for (let i = 0; i < items.length; i++) {
document.querySelector(".list").appendChild(createItem(items[i])); // reflows each time!
}
// ✓ Fast — build once, insert once:
const fragment = document.createDocumentFragment();
items.forEach(item => fragment.appendChild(createItem(item)));
document.querySelector(".list").appendChild(fragment); // one reflow
// ✗ Slow — reading layout properties inside a write loop (layout thrashing):
elements.forEach(el => {
const h = el.offsetHeight; // forces layout recalculation
el.style.height = h + 10 + "px"; // then writes
});
// ✓ Fast — read all, then write all:
const heights = elements.map(el => el.offsetHeight); // read phase
elements.forEach((el, i) => el.style.height = heights[i] + 10 + "px"); // write phase
Try It Yourself
Official Sources
- MDN Web Docs — Performance
- web.dev — Performance
- Chrome DevTools — Performance Features Reference
- MDN Web Docs — Web Workers API
The Codex links only to official documentation and the ECMAScript specification. No third-party tutorials.