JavaScript Performance: Making Your Apps Fast (2026)
Performance isn't about premature optimization — it's about understanding what makes JavaScript slow and knowing how to fix it when it matters.
Measuring Performance Correctly
// ❌ Wrong ways to measure:
const start = Date.now(); // Low resolution (ms only)
doSomething();
console.log(Date.now() - start); // Inaccurate
// ✅ Right way: Performance API (microsecond precision!)
const { performance, PerformanceObserver } = require('perf_hooks');
// Measure a specific operation:
function measure(name, fn) {
const start = performance.now();
const result = fn();
const duration = performance.now() - start;
console.log(`${name}: ${duration.toFixed(2)}ms`);
return result;
}
// Mark + measure for complex flows:
performance.mark('fetch-start');
await fetchData();
performance.mark('parse-start');
parseData();
performance.mark('render-start');
renderUI();
performance.mark('done');
performance.measure('fetch', 'fetch-start', 'parse-start');
performance.measure('parse', 'parse-start', 'render-start');
performance.measure('render', 'render-start', 'done');
// Get all measurements:
const entries = performance.getEntriesByType('measure');
entries.forEach(e => console.log(`${e.name}: ${e.duration.toFixed(2)}ms`));
// Observer pattern (non-blocking monitoring):
const obs = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 100) {
console.warn(`Slow operation: ${entry.name} took ${entry.duration.toFixed(0)}ms`);
}
}
});
obs.observe({ entryTypes: ['measure'] });
The V8 Engine & Hidden Classes
// V8 uses "hidden classes" to optimize property access.
// When objects have the same shape → same hidden class → optimized!
// ✅ Same shape (FAST):
class Point {
constructor(x, y) { this.x = x; this.y = y; }
}
const points = Array.from({ length: 100000 }, () => new Point(
Math.random(), Math.random()
));
// All Point instances share the same hidden class → blazing fast!
// ❌ Different shapes (SLOW):
const mixed = [
{ x: 1, y: 2 },
{ x: 1, y: 2, z: 3 }, // Different shape!
{ x: 1 }, // Different shape!
{ x: 1, y: 2, color: 'red' } // Different shape!
];
// Each object gets its own hidden class → deoptimized access!
// Rule: Initialize ALL properties in the constructor,
// even if they're null/undefined initially.
// Don't add properties after construction in hot loops.
// Arrays: Use typed arrays for numeric data:
const regularArray = [1, 2, 3, 4, 5]; // Generic array
const typedArray = new Float64Array([1, 2, 3, 4, 5]); // Typed, contiguous memory
// Typed arrays are 3-10x faster for numeric operations and use less memory!
Async Performance Patterns
// ❌ Sequential awaits (slow — each waits for the previous):
async function getUserData(userId) {
const user = await db.users.findById(userId); // Wait...
const posts = await db.posts.findByUser(userId); // Then wait...
const comments = await db.comments.findFor(posts); // Then wait...
return { user, posts, comments };
}
// Total time: sum of all three operations
// ✅ Parallel awaits (fast — all run simultaneously):
async function getUserDataFast(userId) {
const [user, posts, comments] = await Promise.all([
db.users.findById(userId),
db.posts.findByUser(userId),
db.comments.findForPosts(userId),
]);
return { user, posts, comments };
}
// Total time: max of the three operations (usually 3x faster!)
// ✅ Concurrent with limit (don't overwhelm external APIs):
async function fetchAll(urls, concurrency = 5) {
const results = [];
const executing = [];
for (const url of urls) {
const promise = fetch(url).then(r => r.json()).then(data => {
results.push({ url, data });
executing.splice(executing.indexOf(promise), 1);
});
executing.push(promise);
if (executing.length >= concurrency) {
await Promise.race(executing); // Wait for one to finish before starting next
}
}
await Promise.all(executing); // Wait for remaining
return results;
}
// Worker threads for CPU-intensive tasks:
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
if (isMainThread) {
function runInWorker(path, data) {
return new Promise((resolve, reject) => {
const worker = new Worker(__filename, { workerData: data });
worker.on('message', resolve);
worker.on('error', reject);
worker.on('exit', (code) => {
if (code !== 0) reject(new Error(`Worker stopped with code ${code}`));
});
});
}
async function processLargeDataset(dataset) {
const result = await runInWorker('./heavy-computation.js', dataset);
return result;
}
} else {
// This runs in the worker thread (doesn't block main event loop!)
const result = heavyComputation(workerData);
parentPort.postMessage(result);
}
Memory Management & Leaks
// Common memory leak patterns:
// Leak #1: Event listeners never removed
function setupLeak() {
const element = document.getElementById('button');
element.addEventListener('click', () => doSomething());
// If this function is called repeatedly without cleanup → leak!
}
// Fix: Store reference and remove later, or use AbortController:
function setupSafe() {
const controller = new AbortController();
const element = document.getElementById('button');
element.addEventListener('click', () => doSomething(), { signal: controller.signal });
// Cleanup: controller.abort() removes all listeners at once
return controller; // Call .abort() when done
}
// Leak #2: Closures holding references
function createLeakyFunction() {
const hugeData = new Array(1000000).fill('data'); // Large array
return function() {
console.log(hugeData[0]); // Closure keeps hugeData alive forever!
};
}
// Fix: Null out large references after use:
function createSafeFunction() {
let hugeData = new Array(1000000).fill('data');
return function() {
const result = hugeData[0];
hugeData = null; // Release reference!
return result;
};
}
// Leak #3: Map/Set as caches without bounds
const cache = new Map();
function getData(id) {
if (!cache.has(id)) cache.set(id, fetchHugeData(id)); // Grows forever!
return cache.get(id);
}
// Fix: Use LRU cache or set max size:
const MAX_CACHE_SIZE = 1000;
function getCachedData(id) {
if (!cache.has(id)) {
if (cache.size >= MAX_CACHE_SIZE) {
// Remove oldest entry (Map preserves insertion order)
const oldestKey = cache.keys().next().value;
cache.delete(oldestKey);
}
cache.set(id, fetchHugeData(id));
}
return cache.get(id);
}
// Monitoring memory usage:
setInterval(() => {
const mem = process.memoryUsage();
console.log({
rss: `${(mem.rss / 1024 / 1024).toFixed(0)}MB`, // Resident Set Size
heapUsed: `${(mem.heapUsed / 1024 / 1024).toFixed(0)}MB`,
heapTotal: `${(mem.heapTotal / 1024 / 1024).toFixed(0)}MB`,
external: `${(mem.external / 1024 / 1024).toFixed(0)}MB`,
});
}, 30000);
// Force garbage collection (only with --expose-gc flag):
if (global.gc) global.gc();
DOM Performance
// ❌ Slow: Multiple DOM operations (reflow + repaint each time!)
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
document.body.appendChild(div); // Reflow + repaint every iteration!
}
// ✅ Fast: Batch DOM operations (single reflow + repaint)
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
fragment.appendChild(div); // No reflow! Fragment is not in DOM yet.
}
document.body.appendChild(fragment); // Single reflow + repaint
// ✅ Even faster: innerHTML for bulk content (one parse operation):
document.body.innerHTML += `<div>${items.map(i => `Item ${i}`).join('</div><div>')}</div>`;
// Layout thrashing: Reading/writing alternately causes multiple reflows
// ❌ Bad (layout thrashing):
elements.forEach(el => {
el.style.height = el.scrollHeight + 'px'; // Read → Write (reflow!)
});
// ✅ Good (batch reads, then batch writes):
const heights = elements.map(el => el.scrollHeight); // Read all first
elements.forEach((el, i) => {
el.style.height = heights[i] + 'px'; // Write all after
});
// requestAnimationFrame for visual updates:
function animateScroll(targetY) {
function step() {
const currentY = window.scrollY;
const diff = targetY - currentY;
if (Math.abs(diff) < 1) return;
window.scrollTo(0, currentY + diff * 0.1);
requestAnimationFrame(step); // Browser decides optimal timing
}
requestAnimationFrame(step);
}
Quick Wins Checklist
Before optimizing anything:
1. Profile first! Never guess where the bottleneck is
2. Optimize the critical path (what user sees first)
3. Common quick wins:
□ Defer non-critical JS (<script defer>, dynamic import())
□ Minify + compress assets (gzip/brotli)
□ Add caching headers (Cache-Control, ETag)
□ Lazy-load images (loading="lazy", IntersectionObserver)
□ Use CSS containment (contain: layout paint)
□ Virtualize long lists (react-virtualized, etc.)
□ Debounce/throttle scroll/resize/input handlers
□ Use web workers for heavy computation
□ Enable HTTP/2 or HTTP/3 on your server
□ Preload critical resources (<link rel="preload">)
4. What NOT to optimize:
- Code that runs once at startup
- Error handling paths (rarely executed)
- Developer experience over micro-optimizations
- Anything you haven't measured
What's the biggest performance win you've ever achieved? What performance myth drives you crazy?
Follow @armorbreak for more practical developer guides.
Top comments (0)