JavaScript  

Chapter 23: JavaScript Performance and Optimization

Previous chapter: Chapter 22: Unit Testing Fundamentals (Jest/Mocha)

Writing code that works is one thing; writing code that is fast and doesn't freeze the user interface is another. This chapter focuses on professional techniques to minimize costly operations, manage event handling efficiently, and reduce unnecessary browser rendering.

1. Debouncing and Throttling (Managing Events)

These two techniques are essential for managing event handlers that fire rapidly (like scroll , resize , or input events), which can quickly overwhelm the browser and cause "jank."

A. Debouncing

Debouncing ensures that a function is only called after a specified period of time has passed since the last time the event fired. It’s perfect for auto-saving forms or search suggestions, where you only want to process the final input.

  • Logic: Reset the timer every time the event fires. The function executes only when the timer finally completes.

  
    function debounce(func, delay) {
    let timeoutId;
    return function(...args) {
        clearTimeout(timeoutId); // Cancel the previous call
        timeoutId = setTimeout(() => {
            func.apply(this, args);
        }, delay);
    };
}

// Example: Only log the input value 500ms after the user stops typing
const handleSearch = debounce((text) => {
    console.log('Sending API request for:', text);
}, 500);

document.getElementById('search-input').addEventListener('input', (e) => {
    handleSearch(e.target.value);
});
  

B. Throttling

Throttling ensures that a function is called, at most, once every specified time interval. It’s ideal for handling progress during scrolling or resize events, where continuous updates are needed, but not every single event.

  • Logic: Check the last time the function ran. If the cool-down period hasn't passed, ignore the call.

2. Memoization (Caching Results)

Memoization is a technique that caches the return values of expensive function calls based on their arguments. If the function is called again with the same arguments, the cached result is returned instead of re-executing the heavy computation.

  
    function memoize(func) {
    const cache = {}; // The cache stores previous results
    return function(n) {
        if (n in cache) {
            console.log('Fetching from cache...');
            return cache[n];
        }
        console.log('Calculating fresh result...');
        const result = func(n); // Execute the original function
        cache[n] = result; // Store the new result
        return result;
    };
}

// Example: A slow factorial calculation
const slowFactorial = (n) => {
    if (n === 0) return 1;
    return n * slowFactorial(n - 1); // Simulate expensive work
};

const fastFactorial = memoize(slowFactorial);

fastFactorial(10); // Calculation runs
fastFactorial(10); // Result immediately returned from cache
  

3. Minimizing Reflow and Repaint

When you manipulate the DOM (Chapter 9), the browser may perform two expensive operations:

  • Reflow (or Layout): Recalculating the dimensions and position of elements on the page. This happens when content, element size, or window size changes.

  • Repaint: Redrawing the pixels onto the screen (e.g., changing colors, visibility).

To optimize performance:

  1. Batch DOM Changes: Make multiple style changes simultaneously by manipulating the element's style property once, or better yet, by adding/removing a CSS class (Chapter 9).

  2. Avoid Reading Layout: Do not read layout properties (like offsetHeight , scrollWidth ) immediately after writing to them, as this forces a synchronous Reflow.

  3. Use CSS Animations: Prefer CSS properties that don't trigger layout changes (like transform and opacity ) for animations.