A few months ago, I profiled a React dashboard that took 8 seconds to render a table with 2,000 rows. The fix took 15 minutes — it was a re-render issue caused by creating new object references on every render cycle. The actual computation was fast; the framework was just doing 50x more work than necessary because of a subtle JavaScript pattern.
Performance issues in JavaScript are rarely about the language being slow. They’re about developers unknowingly triggering expensive operations — unnecessary DOM updates, memory leaks from forgotten event listeners, layout thrashing from interleaved reads and writes. Here are the patterns I look for when a JavaScript application feels sluggish.
DOM Manipulation: The Biggest Performance Trap
The DOM is the single most expensive thing you interact with in browser JavaScript. Every DOM read or write can trigger style recalculation, layout, and paint operations. The key is to minimize interactions and batch them when possible.
Batch DOM Writes
// Slow: triggers layout recalculation on every iteration
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
container.appendChild(div); // triggers reflow each time
}
// Fast: build everything, then append once
const fragment = document.createDocumentFragment();
for (let i = 0; i < 1000; i++) {
const div = document.createElement('div');
div.textContent = `Item ${i}`;
fragment.appendChild(div);
}
container.appendChild(fragment); // single reflow
Using DocumentFragment reduces 1,000 reflows to 1. On a mid-range mobile device, this difference can be 200ms vs 5ms.
Avoid Layout Thrashing
Layout thrashing happens when you alternate between reading and writing DOM properties:
// Slow: forces layout recalculation on every read
items.forEach(item => {
const height = item.offsetHeight; // read → forces layout
item.style.height = height * 2 + 'px'; // write → invalidates layout
});
// Fast: batch reads, then batch writes
const heights = items.map(item => item.offsetHeight); // all reads
items.forEach((item, i) => {
item.style.height = heights[i] * 2 + 'px'; // all writes
});
Every time you read a layout property (offsetHeight, getBoundingClientRect(), scrollTop) after modifying the DOM, the browser must recalculate layout synchronously. Batching reads and writes separately avoids this forced synchronous layout.
Event Handling: Debounce and Throttle
The Problem with Scroll and Resize Handlers
// Dangerous: fires 60+ times per second during scroll
window.addEventListener('scroll', () => {
updateHeader();
calculateVisibility();
trackScrollDepth();
});
Scroll and resize events fire at the screen’s refresh rate — typically 60 times per second. If your handler takes more than 16ms, you’ll drop frames and the page will feel janky.
Debounce: Wait Until They Stop
Use debounce when you only care about the final value — like search input:
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
const handleSearch = debounce((query) => {
fetchResults(query);
}, 300);
searchInput.addEventListener('input', (e) => handleSearch(e.target.value));
Throttle: Limit Frequency
Use throttle when you need periodic updates during continuous action — like scroll position:
function throttle(fn, limit) {
let inThrottle;
return (...args) => {
if (!inThrottle) {
fn(...args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
window.addEventListener('scroll', throttle(updateHeader, 100));
With throttle at 100ms, the handler fires at most 10 times per second instead of 60+.
Memory Leaks: The Silent Performance Killer
Memory leaks don’t crash your app immediately — they slowly degrade performance until the browser tab uses 2GB of RAM and starts freezing.
The Three Most Common Leaks
1. Forgotten Event Listeners
// Leak: listener is never removed
function setupWidget(element) {
const handler = () => updateWidget(element);
window.addEventListener('resize', handler);
// element gets removed from DOM, but handler still references it
}
// Fix: clean up when done
function setupWidget(element) {
const handler = () => updateWidget(element);
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}
2. Closures Holding References
// Leak: the closure holds a reference to the entire dataset
function processData(hugeDataset) {
return function getFirstItem() {
return hugeDataset[0]; // keeps hugeDataset in memory forever
};
}
// Fix: extract what you need
function processData(hugeDataset) {
const firstItem = hugeDataset[0];
return function getFirstItem() {
return firstItem; // only keeps the single item
};
}
3. Detached DOM Nodes
// Leak: removed elements still referenced in JavaScript
const cache = new Map();
function removeElement(id) {
const el = document.getElementById(id);
cache.set(id, el); // prevents garbage collection
el.remove();
}
// Fix: use WeakMap or clear references
const cache = new WeakMap();
String Operations at Scale
String manipulation is fast for small inputs but can become a bottleneck with large datasets:
// Slow: creates a new string on every iteration (O(n²))
let result = '';
for (let i = 0; i < 100000; i++) {
result += `item ${i}\n`;
}
// Fast: join an array (O(n))
const parts = [];
for (let i = 0; i < 100000; i++) {
parts.push(`item ${i}`);
}
const result = parts.join('\n');
The array approach is 10–100x faster for large iterations because strings are immutable — every += allocates a new string and copies all previous data.
Testing regex patterns? Complex regex can cause catastrophic backtracking that freezes your browser. Test patterns in our Regex Tester before putting them in production code.
Lazy Loading and Code Splitting
Dynamic Imports
Don’t load code until it’s needed:
// Bad: all tools loaded upfront
import { JSONFormatter } from './tools/json';
import { RegexTester } from './tools/regex';
import { ImageConverter } from './tools/image';
// Good: load on demand
const tools = {
json: () => import('./tools/json'),
regex: () => import('./tools/regex'),
image: () => import('./tools/image'),
};
async function loadTool(name) {
const module = await tools[name]();
return module.default;
}
Intersection Observer for Scroll-Based Loading
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadContent(entry.target);
observer.unobserve(entry.target);
}
});
}, { rootMargin: '200px' });
document.querySelectorAll('.lazy-section').forEach(el => observer.observe(el));
The rootMargin: '200px' starts loading content 200px before it enters the viewport, so users never see a loading state.
Profiling: Finding the Real Bottleneck
Before optimizing anything, measure. Premature optimization based on intuition wastes time.
Chrome DevTools Performance Tab
- Open DevTools → Performance tab
- Click Record, interact with your page, then Stop
- Look for:
- Long Tasks (yellow bars over 50ms) — anything blocking the main thread
- Layout Shift indicators — unexpected reflows
- JavaScript execution time — which functions take the most time
console.time for Quick Measurements
console.time('render');
renderDashboard(data);
console.timeEnd('render');
// Output: render: 142.3ms
Performance.now() for Precise Benchmarks
const start = performance.now();
processLargeDataset(data);
const duration = performance.now() - start;
console.log(`Processing took ${duration.toFixed(2)}ms`);
Quick Wins Checklist
These optimizations have the highest impact-to-effort ratio:
- Use
DocumentFragmentfor batch DOM insertions - Debounce search inputs (300ms delay)
- Throttle scroll/resize handlers (100ms limit)
- Remove event listeners when components unmount
- Use
loading="lazy"on below-fold images - Dynamic import heavy modules
- Avoid
innerHTMLfor repeated updates (usetextContent) - Use
requestAnimationFramefor visual updates - Cache DOM queries outside loops
- Profile before optimizing — measure, don’t guess
Further Reading
Optimize your JavaScript with our browser-based tools: JS Beautifier for formatting, Regex Tester for pattern validation, and CSS Minifier for production-ready stylesheets.