Measuring Performance
Use built-in APIs to measure and benchmark code performance.
// console.time / console.timeEnd
console.time("loop");
for (let i = 0; i < 1000000; i++) { }
console.timeEnd("loop"); // loop: 3.5ms
// Performance API (more precise)
const start = performance.now();
// ... code to measure
const end = performance.now();
console.log(`Took ${end - start}ms`);
// Performance marks and measures
performance.mark("start");
// ... code to measure
performance.mark("end");
performance.measure("My Operation", "start", "end");
const measures = performance.getEntriesByName("My Operation");
console.log(measures[0].duration);
// Clear performance entries
performance.clearMarks();
performance.clearMeasures();
// Benchmarking function
function benchmark(name, fn, iterations = 1000) {
const times = [];
// Warm up
for (let i = 0; i < 10; i++) fn();
// Measure
for (let i = 0; i < iterations; i++) {
const start = performance.now();
fn();
times.push(performance.now() - start);
}
const avg = times.reduce((a, b) => a + b) / times.length;
const min = Math.min(...times);
const max = Math.max(...times);
console.log(`${name}: avg=${avg.toFixed(3)}ms, min=${min.toFixed(3)}ms, max=${max.toFixed(3)}ms`);
}
// Compare two approaches
benchmark("Array.push", () => {
const arr = [];
for (let i = 0; i < 1000; i++) arr.push(i);
});
benchmark("Array literal", () => {
const arr = Array.from({ length: 1000 }, (_, i) => i);
});
DOM Performance
Optimize DOM operations which are often the main performance bottleneck.
// BAD: Multiple DOM updates cause reflows
for (const item of items) {
const div = document.createElement("div");
div.textContent = item.name;
container.appendChild(div); // Reflow each time!
}
// GOOD: Use DocumentFragment
const fragment = document.createDocumentFragment();
for (const item of items) {
const div = document.createElement("div");
div.textContent = item.name;
fragment.appendChild(div);
}
container.appendChild(fragment); // Single reflow
// GOOD: Build HTML string
const html = items.map(item =>
`${item.name}`
).join("");
container.innerHTML = html; // Single reflow
// BAD: Reading layout properties causes forced reflow
for (const el of elements) {
el.style.width = el.offsetWidth + 10 + "px"; // Read then write
}
// GOOD: Batch reads, then batch writes
const widths = elements.map(el => el.offsetWidth);
elements.forEach((el, i) => {
el.style.width = widths[i] + 10 + "px";
});
// ===== requestAnimationFrame =====
// Use for visual updates
function animate() {
// Update positions
element.style.transform = `translateX(${x}px)`;
if (shouldContinue) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
// ===== Debouncing =====
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
// Use for scroll, resize, input events
window.addEventListener("resize", debounce(() => {
console.log("Resized!");
}, 250));
// ===== Throttling =====
function throttle(fn, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Use for scroll handlers that need regular updates
window.addEventListener("scroll", throttle(() => {
console.log("Scrolling!");
}, 100));
Reading layout properties (offsetWidth, getBoundingClientRect) then immediately writing styles forces the browser to recalculate layout. Batch reads and writes separately.
Memory Management
Avoid memory leaks and optimize memory usage.
// ===== Common Memory Leaks =====
// 1. Event listeners not removed
function setup() {
const button = document.getElementById("btn");
button.addEventListener("click", handleClick);
}
function cleanup() {
const button = document.getElementById("btn");
button.removeEventListener("click", handleClick);
}
// 2. Closures holding references
function createHandler() {
const largeData = new Array(1000000);
return function() {
// largeData is kept in memory even if not used!
console.log("Handler called");
};
}
// Fix: Only capture what's needed
function createHandler() {
const largeData = new Array(1000000);
const neededValue = process(largeData);
return function() {
console.log(neededValue);
};
}
// 3. Timers not cleared
const intervalId = setInterval(() => {
console.log("Running...");
}, 1000);
// Remember to clear when done
clearInterval(intervalId);
// 4. Detached DOM nodes
let detachedNode = document.createElement("div");
document.body.appendChild(detachedNode);
document.body.removeChild(detachedNode);
// detachedNode still holds reference - set to null
detachedNode = null;
// ===== WeakMap / WeakSet =====
// Allow garbage collection when key is no longer referenced
// Cache that doesn't prevent GC
const cache = new WeakMap();
function processNode(node) {
if (cache.has(node)) {
return cache.get(node);
}
const result = expensiveOperation(node);
cache.set(node, result);
return result;
}
// When node is removed from DOM, cache entry is cleaned up
// ===== Object Pooling =====
// Reuse objects instead of creating new ones
class ParticlePool {
constructor(size) {
this.pool = Array.from({ length: size }, () => ({
x: 0, y: 0, active: false
}));
}
acquire() {
const particle = this.pool.find(p => !p.active);
if (particle) {
particle.active = true;
return particle;
}
return null;
}
release(particle) {
particle.active = false;
particle.x = 0;
particle.y = 0;
}
}
Algorithm Optimization
Choose efficient algorithms and data structures.
// ===== Use Map for frequent lookups =====
// BAD: O(n) lookup
const users = [{ id: 1, name: "John" }, { id: 2, name: "Jane" }];
const user = users.find(u => u.id === 1);
// GOOD: O(1) lookup
const userMap = new Map(users.map(u => [u.id, u]));
const user = userMap.get(1);
// ===== Use Set for unique values =====
// BAD: O(n) for includes
const seen = [];
for (const item of items) {
if (!seen.includes(item)) {
seen.push(item);
}
}
// GOOD: O(1) for has
const seen = new Set();
for (const item of items) {
if (!seen.has(item)) {
seen.add(item);
}
}
// Or simply: const unique = [...new Set(items)];
// ===== Avoid nested loops when possible =====
// BAD: O(n²)
for (const a of arrayA) {
for (const b of arrayB) {
if (a.id === b.id) { /* match */ }
}
}
// GOOD: O(n)
const bMap = new Map(arrayB.map(b => [b.id, b]));
for (const a of arrayA) {
const b = bMap.get(a.id);
if (b) { /* match */ }
}
// ===== Early exit =====
function findUser(users, id) {
for (const user of users) {
if (user.id === id) {
return user; // Exit as soon as found
}
}
return null;
}
// ===== Memoization =====
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const fibonacci = memoize(function(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
fibonacci(40); // Fast with memoization
Lazy Loading
Load resources only when needed.
// ===== Dynamic Imports =====
// Load module only when needed
button.addEventListener("click", async () => {
const { openModal } = await import("./modal.js");
openModal();
});
// Route-based code splitting
async function loadPage(route) {
switch (route) {
case "/dashboard":
return import("./pages/dashboard.js");
case "/settings":
return import("./pages/settings.js");
default:
return import("./pages/home.js");
}
}
// ===== Intersection Observer for lazy loading =====
const lazyImages = document.querySelectorAll("img[data-src]");
const imageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute("data-src");
imageObserver.unobserve(img);
}
});
}, {
rootMargin: "50px" // Start loading before visible
});
lazyImages.forEach(img => imageObserver.observe(img));
// ===== Virtual Scrolling =====
// For large lists, only render visible items
class VirtualList {
constructor(container, items, itemHeight) {
this.container = container;
this.items = items;
this.itemHeight = itemHeight;
this.container.style.height = items.length * itemHeight + "px";
this.container.style.position = "relative";
this.render();
window.addEventListener("scroll", () => this.render());
}
render() {
const scrollTop = window.scrollY;
const containerTop = this.container.offsetTop;
const viewportHeight = window.innerHeight;
const startIndex = Math.floor((scrollTop - containerTop) / this.itemHeight);
const endIndex = Math.ceil((scrollTop - containerTop + viewportHeight) / this.itemHeight);
const visibleItems = this.items.slice(
Math.max(0, startIndex - 5),
Math.min(this.items.length, endIndex + 5)
);
// Only render visible items
this.container.innerHTML = visibleItems.map((item, i) => {
const index = startIndex + i;
return `
${item.name}
`;
}).join("");
}
}
Core Web Vitals
Core Web Vitals are Google's metrics for user experience. They measure loading (LCP), interactivity (INP), and visual stability (CLS).
// Using the web-vitals library
import { onLCP, onINP, onCLS, onFCP, onTTFB } from 'web-vitals';
function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating, // 'good', 'needs-improvement', 'poor'
delta: metric.delta, // Change since last report
id: metric.id,
navigationType: metric.navigationType
});
// Use sendBeacon for reliability
navigator.sendBeacon('/analytics', body);
}
// Largest Contentful Paint (LCP) - loading performance
// Good: < 2.5s, Poor: > 4s
onLCP(sendToAnalytics);
// Interaction to Next Paint (INP) - responsiveness
// Good: < 200ms, Poor: > 500ms
onINP(sendToAnalytics);
// Cumulative Layout Shift (CLS) - visual stability
// Good: < 0.1, Poor: > 0.25
onCLS(sendToAnalytics);
// First Contentful Paint (FCP)
onFCP(sendToAnalytics);
// Time to First Byte (TTFB)
onTTFB(sendToAnalytics);
// Manual Performance Observer
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log(`${entry.name}: ${entry.startTime.toFixed(0)}ms`);
}
});
observer.observe({ type: 'largest-contentful-paint', buffered: true });
observer.observe({ type: 'layout-shift', buffered: true });
observer.observe({ type: 'first-input', buffered: true });
Code Splitting
Code splitting breaks your bundle into smaller chunks that load on demand, reducing initial load time and improving performance.
// Route-based splitting
const routes = {
'/': () => import('./pages/Home.js'),
'/about': () => import('./pages/About.js'),
'/dashboard': () => import('./pages/Dashboard.js')
};
async function navigate(path) {
const loader = routes[path];
if (loader) {
const module = await loader();
module.default.render();
}
}
// Component-based splitting
async function loadEditor() {
const { Editor } = await import('./components/Editor.js');
return new Editor();
}
// Prefetch on hover/focus
document.querySelectorAll('a').forEach(link => {
link.addEventListener('mouseenter', () => {
const path = link.getAttribute('href');
if (routes[path]) {
routes[path](); // Start loading
}
}, { once: true });
});
// Webpack magic comments
const Dashboard = React.lazy(() =>
import(
/* webpackChunkName: "dashboard" */
/* webpackPrefetch: true */
'./Dashboard'
)
);
// Conditional loading
const loadPolyfills = async () => {
const polyfills = [];
if (!('IntersectionObserver' in window)) {
polyfills.push(import('intersection-observer'));
}
if (!('ResizeObserver' in window)) {
polyfills.push(import('resize-observer-polyfill'));
}
await Promise.all(polyfills);
};
// Load on demand
button.addEventListener('click', async () => {
const { Chart } = await import('chart.js');
new Chart(canvas, config);
});
Resource Hints & Preloading
Resource hints tell the browser about resources it will need soon, allowing it to fetch them early and improve perceived performance.
// Preload - high priority, needed soon
// <link rel="preload" href="/fonts/main.woff2" as="font" crossorigin>
// <link rel="preload" href="/hero.jpg" as="image">
// Prefetch - low priority, might need later
// <link rel="prefetch" href="/next-page.js">
// Preconnect - establish connection early
// <link rel="preconnect" href="https://api.example.com">
// <link rel="dns-prefetch" href="https://cdn.example.com">
// Dynamic resource hints
function addResourceHint(type, url, options = {}) {
const link = document.createElement('link');
link.rel = type;
link.href = url;
if (options.as) link.as = options.as;
if (options.crossOrigin) link.crossOrigin = options.crossOrigin;
document.head.appendChild(link);
}
// Preload critical resources
addResourceHint('preload', '/api/user', { as: 'fetch', crossOrigin: 'anonymous' });
// Prefetch likely next pages
addResourceHint('prefetch', '/next-page.html');
// Preconnect to API server
addResourceHint('preconnect', 'https://api.example.com');
// Fetch priority API
// <img src="hero.jpg" fetchpriority="high">
// <img src="footer.jpg" fetchpriority="low">
// <script src="critical.js" fetchpriority="high"></script>
// Loading attribute for lazy loading
// <img src="below-fold.jpg" loading="lazy">
// <iframe src="widget.html" loading="lazy"></iframe>
// Native lazy loading with fallback
const img = document.createElement('img');
if ('loading' in img) {
img.loading = 'lazy';
img.src = 'image.jpg';
} else {
// Use IntersectionObserver fallback
}
- Measure: Use Lighthouse, WebPageTest, and real user monitoring
- Reduce: Minimize bundle size, compress images, remove unused code
- Defer: Load non-critical resources lazily
- Cache: Use browser caching and service workers
- Optimize: Use efficient algorithms and avoid layout thrashing
Summary
Measure First
performance.now(), Chrome DevTools
DOM Batching
DocumentFragment, batch reads/writes
Event Handling
Debounce, throttle, delegation
Memory
Clear listeners, WeakMap, pooling
Data Structures
Map/Set for O(1) lookups
Lazy Loading
Dynamic imports, IntersectionObserver