JavaScript Performance Tips That Actually Make a Difference
Not micro-optimizations. Real performance wins for real apps.
1. DOM Updates Are Expensive (Batch Them!)
// ❌ Slow: Multiple DOM writes
const list = document.getElementById('list');
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item.name;
list.appendChild(li); // DOM update each iteration!
});
// ✅ Fast: Document fragment (single DOM update)
const fragment = document.createDocumentFragment();
items.forEach(item => {
const li = document.createElement('li');
li.textContent = item.name;
fragment.appendChild(li); // No DOM update yet!
});
list.appendChild(fragment); // Single DOM update!
// Or even faster: innerHTML with one write
list.innerHTML = items.map(item => `<li>${escapeHtml(item.name)}</li>`).join('');
2. Debounce and Throttle User Input
// Debounce: Wait until user stops typing
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// Throttle: Run at most once per time period
function throttle(fn, interval) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
// Usage:
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce((e) => {
fetchSearchResults(e.target.value); // Only fires after user stops typing for 300ms
}, 300));
window.addEventListener('resize', throttle(() => {
recalculateLayout(); // Runs at most once per 200ms
}, 200));
3. Lazy Load Everything
// Images: Native lazy loading
<img src="photo.jpg" alt="..." loading="lazy" decoding="async">
// Components: Dynamic imports
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));
// Routes: Code splitting per route
const Dashboard = () => import('./pages/Dashboard');
const Settings = () => import('./pages/Settings');
// Data: Infinite scroll / pagination
async function loadMore() {
const spinner = showSpinner();
const newItems = await fetchItems(currentPage + 1);
appendItems(newItems);
currentPage++;
hideSpinner(spinner);
}
// Intersection Observer for visibility-based loading
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadContent(entry.target.dataset.src);
observer.unobserve(entry.target);
}
});
});
document.querySelectorAll('[data-lazy]').forEach(el => observer.observe(el));
4. Use requestAnimationFrame for Animations
// ❌ Bad: setInterval/setTimeout for animations
setInterval(() => {
element.style.left = parseInt(element.style.left) + 5 + 'px';
}, 16); // Not synced with display refresh, wastes battery
// ✅ Good: requestAnimationFrame
let position = 0;
function animate() {
position += 5;
element.style.left = position + 'px';
if (position < targetPosition) {
requestAnimationFrame(animate); // Runs at optimal time (~60fps)
}
}
requestAnimationFrame(animate);
// Benefits:
// - Synced with browser's repaint cycle
// - Pauses when tab is not visible (saves CPU/battery!)
// - Automatically adjusts to display refresh rate
5. Web Workers for Heavy Computation
// main.js
const worker = new Worker('worker.js');
worker.postMessage({ type: 'PROCESS', data: largeDataset });
worker.onmessage = (e) => {
console.log('Result:', e.data.result);
};
// worker.js — runs on separate thread (doesn't block UI!)
self.onmessage = function(e) {
if (e.data.type === 'PROCESS') {
const result = heavyComputation(e.data.data);
self.postMessage({ result });
}
};
function heavyComputation(data) {
// This can take seconds without freezing the UI!
let sum = 0;
for (let i = 0; i < data.length; i++) {
sum += Math.sqrt(data[i]) * Math.log(i + 1);
}
return sum;
}
6. Memoize Expensive Computations
// Simple memoization
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key); // Return cached result
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// Usage:
const expensiveFibonacci = memoize(function(n) {
if (n <= 1) return n;
return expensiveFibonacci(n - 1) + expensiveFibonacci(n - 2);
});
expensiveFibonacci(50); // First call: slow (computes)
expensiveFibonacci(50); // Second call: instant (cached!)
// In React: useMemo / useCallback
const sortedItems = useMemo(
() => [...items].sort((a, b) => b.date - a.date),
[items] // Only recompute when items changes
);
const handleClick = useCallback(
() => doSomething(userId),
[userId] // Only recreate when userId changes
);
7. Virtual Scrolling for Long Lists
// Instead of rendering 10,000 DOM elements:
// Only render the ~20 that are visible in the viewport
function VirtualList({ items, itemHeight = 50, containerHeight }) {
const [scrollTop, setScrollTop] = useState(0);
const startIndex = Math.floor(scrollTop / itemHeight);
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight / itemHeight) + 2,
items.length
);
const visibleItems = items.slice(startIndex, endIndex).map((item, i) => ({
item,
index: startIndex + i,
}));
const totalHeight = items.length * itemHeight;
const offsetY = startIndex * itemHeight;
return (
<div
style={{ height: containerHeight, overflow: 'auto' }}
onScroll={(e) => setScrollTop(e.target.scrollTop)}
>
<div style={{ height: totalHeight, position: 'relative' }}>
<div style={{ transform: `translateY(${offsetY}px)` }}>
{visibleItems.map(({ item, index }) => (
<ListItem key={index} item={item} height={itemHeight} />
))}
</div>
</div>
</div>
);
}
// Libraries: react-window, react-virtualized, tanstack-virtual
8. Optimize Loops
const arr = /* huge array */;
// ❌ Slower patterns:
for (let i = 0; i < arr.length; i++) { } // Checks .length every iteration
arr.forEach(item => { }); // Function call overhead
arr.map(item => { }); // Creates new array even if not needed
// ✅ Faster patterns:
for (let i = 0, len = arr.length; i < len; i++) { } // Cache length
for (let i = arr.length; i--; ) { } // Reverse loop (faster comparison)
for (const item of arr) { } // Clean and fast (modern engines optimize well)
// For nested loops: break out early when possible
function findMatch(items, target) {
outer: for (let i = 0; i < items.length; i++) {
for (let j = 0; j < items[i].length; j++) {
if (items[i][j] === target) return { i, j }; // Found! Exit immediately
}
}
return null;
}
Performance Measurement Tools
// High-resolution timing
console.time('operation');
doSomething();
console.timeEnd('operation'); // "operation: 123.456ms"
// Performance API (more precise)
const start = performance.now();
heavyOperation();
const duration = performance.now() - start;
console.log(`Took ${duration.toFixed(2)}ms`);
// Performance marks (in DevTools Performance tab)
performance.mark('fetch-start');
await fetchData();
performance.mark('fetch-end');
performance.measure('fetch', 'fetch-start', 'fetch-end');
// Memory monitoring
const used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(`Memory: ${Math.round(used)}MB`);
What's your biggest performance win? Share it below!
Follow @armorbreak for more JS content.
Top comments (0)