Optimizing Scroll Performance in Frontend Applications and Practices of Debouncing and Throttling

Optimizing Scroll Performance in Frontend Applications and Practices of Debouncing and Throttling

Description

The scroll event is one of the high-frequency events in frontend development. Directly binding complex operations (such as rendering, calculations, or data requests) can easily lead to page lag or even crashes. The core of optimizing scroll performance lies in reducing the execution load when the event is triggered and controlling the execution frequency through Debounce and Throttle.


1. Problem Analysis: Why Does Scrolling Need Optimization?

  • High Frequency of Scroll Events: Browsers can trigger scroll events (scroll) dozens of times per second, far exceeding ordinary interaction events (such as clicks).
  • Risk of Main Thread Blocking: If heavy computations or DOM operations are performed on every scroll, it may block the rendering thread, causing frame rate drops.
  • Accumulation of Invalid Calculations: During continuous scrolling, intermediate states may not need processing (e.g., updating element positions); only the final state is required.

2. Basic Optimization: Reducing Scroll Event Load

Step 1: Avoid Heavy Operations During Scrolling

// Bad Example: Frequently Modifying DOM During Scrolling  
window.addEventListener('scroll', () => {  
  const rect = element.getBoundingClientRect(); // Triggers reflow  
  element.style.top = rect.top + 1 + 'px'; // Triggers reflow again  
});  

Optimization Methods:

  • Use transform or position: fixed instead of modifying top/left (avoid reflow).
  • Cache calculation results (e.g., element positions) in advance to avoid repeated calculations during scrolling.

Step 2: Use passive: true to Improve Scroll Responsiveness

// Enable passive event listener to avoid blocking scrolling  
window.addEventListener('scroll', handler, { passive: true });  

Principle: Tells the browser that the event handler will not call preventDefault(), allowing it to continue scrolling without waiting for the function execution.


3. Difference Between Debounce and Throttle

Strategy Core Idea Applicable Scenarios
Debounce Executes only the last call after continuous triggering Search box input, window resizing
Throttle Executes at a fixed frequency during continuous triggering Scrolling, dragging, animation synchronization

4. Implementation and Optimization of Debounce

Basic Debounce (Delayed Execution)

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

// Usage Example  
window.addEventListener('scroll', debounce(() => {  
  console.log('Scrolling ended');  
}, 200));  

Effect: During continuous scrolling, the function only triggers 200ms after scrolling stops.

Advanced Optimization: Immediate Execution Debounce

function debounceImmediate(func, delay, immediate = true) {  
  let timeoutId;  
  return function (...args) {  
    if (immediate && !timeoutId) {  
      func.apply(this, args); // Execute immediately on first trigger  
    }  
    clearTimeout(timeoutId);  
    timeoutId = setTimeout(() => {  
      timeoutId = null;  
      if (!immediate) func.apply(this, args);  
    }, delay);  
  };  
}  

Applicable Scenarios: Such as preventing repeated button clicks, responding immediately on the first trigger, and resetting after a delay for subsequent triggers.


5. Implementation and Optimization of Throttle

Timestamp-Based Throttle (Fixed Interval Execution)

function throttle(func, interval) {  
  let lastTime = 0;  
  return function (...args) {  
    const now = Date.now();  
    if (now - lastTime >= interval) {  
      func.apply(this, args);  
      lastTime = now;  
    }  
  };  
}  

Disadvantage: The last trigger may be ignored (e.g., if scrolling stops before the interval is reached).

Timer + Timestamp Version (Ensuring First and Last Execution)

function throttleAdvanced(func, interval) {  
  let lastTime = 0, timeoutId;  
  return function (...args) {  
    const now = Date.now();  
    const remaining = interval - (now - lastTime);  
    if (remaining <= 0) {  
      if (timeoutId) {  
        clearTimeout(timeoutId);  
        timeoutId = null;  
      }  
      func.apply(this, args);  
      lastTime = now;  
    } else if (!timeoutId) {  
      timeoutId = setTimeout(() => {  
        lastTime = Date.now();  
        timeoutId = null;  
        func.apply(this, args);  
      }, remaining);  
    }  
  };  
}  

Effect: Ensures both the first and last scroll events are processed, with intermediate executions at a fixed frequency.


6. Practical Combination: Infinite Scrolling Case

// Throttle scroll listener, debounce data request  
const checkPosition = throttle(() => {  
  if (window.innerHeight + window.scrollY >= document.body.offsetHeight - 500) {  
    debounce(loadMoreData, 300)(); // Debounce to avoid duplicate requests  
  }  
}, 100);  

window.addEventListener('scroll', checkPosition);  

Summary

  • Debounce: Focuses on the final state, suitable for final actions after continuous events (e.g., searching).
  • Throttle: Focuses on process control, suitable for continuous high-frequency events (e.g., scrolling, dragging).
  • Combined Use: First, throttle to control detection frequency; then, debounce to avoid duplicate requests, balancing performance and user experience.