JavaScript Performance

45 min read Intermediate

Optimize JavaScript code for speed and efficiency with profiling, memory management, and performance best practices.

Measuring Performance

Use built-in APIs to measure and benchmark code performance.

JavaScript
// 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.

JavaScript
// 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));
Layout Thrashing

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.

JavaScript
// ===== 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.

JavaScript
// ===== 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.

JavaScript
// ===== 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).

JavaScript - Measuring Web Vitals
// 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.

JavaScript - Code Splitting Patterns
// 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.

JavaScript - Resource Hints
// 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
}
Performance Checklist
  • 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