FIELD NOTE 026JavaScriptReviewed July 2026

Optimized Scroll Event Handling

Combines debounce with requestAnimationFrame for high-performance scroll event handling

Copy the complete example or adapt its typed interface to your project. Comments are kept inside the snippet so the intent travels with the code.

pattern.js
/**
 * Optimized scroll event handler
 * @param callback Function to execute on scroll
 * @param delay Debounce delay in milliseconds
 * @returns Cleanup function
 */
function handleScroll(callback: () => void, delay = 16): () => void {
  let timeoutId: NodeJS.Timeout;
  
  const handleScroll = () => {
    cancelAnimationFrame(timeoutId);
    timeoutId = setTimeout(() => {
      requestAnimationFrame(callback);
    }, delay);
  };

  window.addEventListener('scroll', handleScroll, { passive: true });
  
  // Return cleanup function
  return () => {
    window.removeEventListener('scroll', handleScroll);
    clearTimeout(timeoutId);
  };
}

// Usage Example
const cleanupScroll = handleScroll(() => {
  console.log('Scroll position:', window.scrollY);
  // Update UI based on scroll position
}, 20);

// Cleanup when component unmounts
// cleanupScroll();

Essential for high-performance scroll-based UI updates, parallax effects, or sticky headers

Implementation note

Review the input assumptions and browser or runtime support before adopting a utility unchanged. Small helpers are easiest to maintain when their contract stays narrow.

How it works

The event handler schedules work through requestAnimationFrame and ignores additional scroll events until that frame runs. Visual updates are aligned with the browser paint cycle instead of executing for every raw event.

What to watch for

Animation-frame scheduling is not a general rate limit and pauses in background tabs. The callback must still avoid expensive synchronous layout work, especially alternating DOM reads and writes that trigger repeated reflow.

Production use

Use the pattern for scroll-linked visual state that should update once per frame. Prefer IntersectionObserver for visibility and CSS-native effects where available, and always remove listeners when the owning component is destroyed.

Before you ship

  • Confirm the input contract with tests that cover empty, invalid, boundary, and representative real-world values before sharing the helper across a codebase.
  • Keep the function focused on one responsibility and name any project-specific policy explicitly instead of hiding it inside a generic-looking utility.
  • Measure behavior in the browsers or runtime versions your product supports, especially when timing, storage, DOM state, locale, or network access is involved.
  • Document the return type, mutation behavior, failure mode, and performance characteristics beside the call site so future maintainers can evaluate changes safely.
  • Compare the example with current official platform documentation when it relies on a browser API, language feature, or runtime behavior that may change over time.
  • If the helper crosses a trust boundary, validate again on the server and avoid treating browser-side checks, TypeScript annotations, or formatted output as a security guarantee.
  • Add a small regression test whenever a real edge case is discovered; concrete examples preserve the reason behind a guard more reliably than a general comment.