Skip to content

Event Handling Performance: Making Page Responses Silky Smooth

When the Page Starts to Lag

You open a website and start scrolling to view content, but the scrolling isn't smooth—the page stutters and jumps at times, and the scrollbar movement isn't fluid. Or you type in a search box, and every letter triggers a network request, noticeably slowing down the browser. These are typical manifestations of event handling performance issues.

Event handling seems simple, but without attention to performance, it can easily make pages laggy. A scroll event might trigger hundreds of times per second when users scroll quickly; a mousemove event might even trigger hundreds of times per second when the mouse moves. If complex operations are executed on every trigger, the browser becomes overwhelmed.

This chapter will take you deep into understanding event handling performance issues, mastering various optimization techniques, and keeping your pages fluid no matter how users interact.

Identifying Performance Issues

Natural Characteristics of High-Frequency Events

Certain events have high-frequency triggering characteristics. When users perform continuous operations, these events trigger multiple times within very short periods.

javascript
// Monitor event trigger frequency
let scrollCount = 0;
let lastTime = Date.now();

window.addEventListener("scroll", () => {
  scrollCount++;
  const now = Date.now();

  if (now - lastTime >= 1000) {
    console.log(`Scroll events per second: ${scrollCount}`);
    scrollCount = 0;
    lastTime = now;
  }
});

// When scrolling quickly, output might be:
// Scroll events per second: 85
// Scroll events per second: 120
// Scroll events per second: 95

Common high-frequency events include:

  • scroll: Scroll events
  • mousemove: Mouse movement events
  • touchmove: Touch movement events
  • resize: Window resize events
  • input: Input events (especially when typing quickly)

Manifestations of Performance Issues

When event handling has performance problems, you'll observe:

  1. Main thread blocking: Page freezes, user operations unresponsive
  2. Frame rate drops: Animation stutters, scrolling not smooth
  3. Memory usage increases: Many event listeners not cleaned up
  4. Too many network requests: Frequent API calls
javascript
// ❌ Performance issue example: complex calculation on every scroll
window.addEventListener("scroll", () => {
  // Expensive DOM query
  const elements = document.querySelectorAll(".expensive-selector");

  // Forced synchronous layout (very expensive operation)
  elements.forEach((element) => {
    const rect = element.getBoundingClientRect(); // Triggers layout calculation
    element.style.transform = `translateY(${rect.top}px)`; // Triggers another one
  });

  // Send network request
  fetch("/api/track-scroll");
});

// User scrolls once, this code might execute 100+ times
// Each time queries DOM, calculates layout, sends request
// Page will severely lag

Optimization Technique 1: Event Delegation

Event delegation not only reduces code volume, more importantly it significantly lowers memory usage.

javascript
// ❌ 1000 listeners
const items = document.querySelectorAll(".list-item"); // 1000 elements
items.forEach((item) => {
  item.addEventListener("click", handleClick); // 1000 listeners
  item.addEventListener("mouseenter", handleHover); // Another 1000
  item.addEventListener("mouseleave", handleHoverEnd); // Another 1000
});
// Total: 3000 event listeners, occupying lots of memory

// ✅ 3 listeners
const list = document.querySelector(".list");
list.addEventListener("click", (e) => {
  const item = e.target.closest(".list-item");
  if (item) handleClick(e);
});
list.addEventListener(
  "mouseenter",
  (e) => {
    const item = e.target.closest(".list-item");
    if (item) handleHover(e);
  },
  true
); // Capture phase, because mouseenter doesn't bubble
list.addEventListener(
  "mouseleave",
  (e) => {
    const item = e.target.closest(".list-item");
    if (item) handleHoverEnd(e);
  },
  true
);
// Total: 3 event listeners, memory usage reduced 99.9%

Performance improvement is obvious:

  • Memory usage: From several MB reduced to a few KB
  • Initialization time: From hundreds of milliseconds reduced to a few milliseconds
  • Dynamic elements: Automatically supported, no additional binding needed

Optimization Technique 2: Debouncing

The core idea of debouncing is: delay executing the handler after the event triggers. If the event triggers again during the delay, restart the timer. Only when the event stops triggering for more than the specified time does the handler actually execute.

It's like an elevator door: as long as someone continues to enter, the door restarts the timer, and only closes after a period of no one entering.

Implementing Debounce

javascript
/**
 * Debounce function
 * @param {Function} func - Function to execute
 * @param {number} delay - Delay time (milliseconds)
 * @returns {Function} Debounced function
 */
function debounce(func, delay) {
  let timeoutId = null;

  return function (...args) {
    // Clear previous timer
    if (timeoutId) {
      clearTimeout(timeoutId);
    }

    // Set new timer
    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

Practical Application: Search Suggestions

javascript
const searchInput = document.getElementById("search");
const suggestions = document.getElementById("suggestions");

// ❌ No debouncing: request on every input
searchInput.addEventListener("input", async (e) => {
  const query = e.target.value;
  const results = await fetch(`/api/search?q=${query}`).then((r) => r.json());
  displaySuggestions(results);
});
// Typing "javascript" sends 10 requests:
// j, ja, jav, java, javas, javasc, javascr, javascri, javascrip, javascript

// ✅ Using debounce: request after stopping input
const debouncedSearch = debounce(async (query) => {
  const results = await fetch(`/api/search?q=${query}`).then((r) => r.json());
  displaySuggestions(results);
}, 300); // 300ms delay

searchInput.addEventListener("input", (e) => {
  debouncedSearch(e.target.value);
});
// Typing "javascript" only sends 1 request (300ms after input ends)

Performance improvement:

  • Network requests: Reduced from 10 to 1
  • Server load: Reduced 90%
  • User experience: Faster response, less flickering

Cancellable Debounce

Sometimes you need to execute or cancel the debounced function early:

javascript
function debounce(func, delay) {
  let timeoutId = null;

  const debounced = function (...args) {
    if (timeoutId) {
      clearTimeout(timeoutId);
    }

    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };

  // Execute immediately
  debounced.immediate = function (...args) {
    if (timeoutId) {
      clearTimeout(timeoutId);
    }
    func.apply(this, args);
  };

  // Cancel execution
  debounced.cancel = function () {
    if (timeoutId) {
      clearTimeout(timeoutId);
      timeoutId = null;
    }
  };

  return debounced;
}

// Usage
const search = debounce(performSearch, 300);

searchInput.addEventListener("input", (e) => {
  search(e.target.value);
});

// Execute search immediately
searchButton.addEventListener("click", () => {
  search.immediate(searchInput.value);
});

// Cancel pending search when user leaves page
window.addEventListener("beforeunload", () => {
  search.cancel();
});

Optimization Technique 3: Throttling

The core idea of throttling is: limit the frequency of function execution. No matter how frequently the event triggers, the handler executes at fixed time intervals.

It's like a faucet's flow limiter: no matter how wide you open it, the water flow speed is fixed.

Implementing Throttle

javascript
/**
 * Throttle function
 * @param {Function} func - Function to execute
 * @param {number} limit - Time interval (milliseconds)
 * @returns {Function} Throttled function
 */
function throttle(func, limit) {
  let inThrottle = false;
  let lastResult;

  return function (...args) {
    if (!inThrottle) {
      inThrottle = true;
      lastResult = func.apply(this, args);

      setTimeout(() => {
        inThrottle = false;
      }, limit);
    }

    return lastResult;
  };
}

Practical Application: Infinite Scroll Loading

javascript
let page = 1;
let loading = false;

// ❌ No throttling: triggers frantically
window.addEventListener("scroll", () => {
  if (loading) return;

  const scrollTop = window.scrollY;
  const windowHeight = window.innerHeight;
  const documentHeight = document.documentElement.scrollHeight;

  if (scrollTop + windowHeight >= documentHeight - 100) {
    loading = true;
    loadMoreContent(++page).then(() => {
      loading = false;
    });
  }
});
// When scrolling to bottom, check function might be called 50+ times

// ✅ Using throttle: execute at most once per 200ms
const throttledScroll = throttle(() => {
  if (loading) return;

  const scrollTop = window.scrollY;
  const windowHeight = window.innerHeight;
  const documentHeight = document.documentElement.scrollHeight;

  if (scrollTop + windowHeight >= documentHeight - 100) {
    loading = true;
    loadMoreContent(++page).then(() => {
      loading = false;
    });
  }
}, 200);

window.addEventListener("scroll", throttledScroll);
// Check at most once per 200ms, significantly reducing CPU usage

Improved Throttle: Execute Immediately First + Tail Execution

javascript
function throttle(func, limit) {
  let inThrottle = false;
  let lastArgs = null;
  let lastThis = null;

  return function (...args) {
    lastArgs = args;
    lastThis = this;

    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;

      setTimeout(() => {
        inThrottle = false;

        // If there were new calls during throttling, execute the last one
        if (lastArgs) {
          func.apply(lastThis, lastArgs);
          lastArgs = null;
          lastThis = null;
        }
      }, limit);
    }
  };
}

Practical Application: Scroll Progress Bar

javascript
const progressBar = document.getElementById("reading-progress");

const updateProgress = throttle(() => {
  const scrollTop = window.scrollY;
  const documentHeight = document.documentElement.scrollHeight;
  const windowHeight = window.innerHeight;

  const scrollPercentage = (scrollTop / (documentHeight - windowHeight)) * 100;
  progressBar.style.width = `${scrollPercentage}%`;
}, 100); // Update at most once per 100ms

window.addEventListener("scroll", updateProgress);

// Performance comparison:
// Without throttle: 100+ DOM updates per second, causing frequent repaints
// With throttle: At most 10 updates per second, fluid and excellent performance

Debounce vs Throttle: When to Use Which?

Debounce Use Cases

Characteristic: Execute after user "stops" operation

  • Search suggestions: Send request after user stops typing
  • Window resize: Recalculate layout after user stops resizing window
  • Form validation: Validate after user stops typing
  • Auto-save: Save after user stops editing
javascript
// Search suggestions: debounce
const search = debounce((query) => {
  fetch(`/api/search?q=${query}`);
}, 300);

// Auto-save: debounce
const autoSave = debounce((content) => {
  localStorage.setItem("draft", content);
}, 1000);

// Window resize: debounce
const handleResize = debounce(() => {
  recalculateLayout();
}, 250);

Throttle Use Cases

Characteristic: Execute at fixed frequency during continuous operation

  • Scroll events: Periodically update position during scroll
  • Mouse movement: Periodically update position during drag
  • Animation frames: Execute at fixed frame rate
  • Real-time statistics: Periodically update data
javascript
// Scroll progress: throttle
const updateScrollProgress = throttle(() => {
  const progress = calculateScrollProgress();
  updateProgressBar(progress);
}, 100);

// Mouse movement: throttle
const handleMouseMove = throttle((e) => {
  updateCursorPosition(e.clientX, e.clientY);
}, 16); // Approximately 60fps

// Chart update: throttle
const updateChart = throttle((data) => {
  renderChart(data);
}, 500);

Comparison Example

javascript
// Scenario: User types search keyword "hello"

// Debounce:
// h -> cancel
// he -> cancel
// hel -> cancel
// hell -> cancel
// hello -> (300ms later) execute search
// Summary: Only executes once

// Throttle (100ms):
// h -> execute search "h"
// he -> skip (100ms not reached)
// hel -> execute search "hel" (100ms reached)
// hell -> skip
// hello -> execute search "hello" (100ms reached)
// Summary: Executes 3 times, at most once per 100ms

Optimization Technique 4: Passive Event Listeners

Certain events (especially touch and scroll events) have default behaviors that can be prevented by preventDefault(). When triggering events, browsers must wait for all listeners to complete before knowing whether to execute default behavior. This causes scroll delay.

Passive listeners tell the browser: "I promise not to call preventDefault()", so the browser can immediately execute default behavior without waiting.

javascript
// ❌ Non-passive listener (default)
document.addEventListener("touchstart", (e) => {
  // Browser must wait for this function to complete
  // before knowing whether to execute default scroll behavior
  handleTouch(e);
});
// May cause scroll delay

// ✅ Passive listener
document.addEventListener(
  "touchstart",
  (e) => {
    // Browser knows we won't call preventDefault()
    // Can immediately execute scroll, no need to wait
    handleTouch(e);
  },
  { passive: true }
);
// Smoother scrolling

Practical Applications

javascript
// Scroll event listener (for analytics)
let scrollDistance = 0;

window.addEventListener(
  "scroll",
  () => {
    scrollDistance += Math.abs(window.scrollY - lastScrollY);
    lastScrollY = window.scrollY;
  },
  { passive: true }
); // Tell browser we won't prevent scrolling

// Touch event listener (for gesture recognition)
let touchStartY = 0;

document.addEventListener(
  "touchstart",
  (e) => {
    touchStartY = e.touches[0].clientY;
  },
  { passive: true }
);

document.addEventListener(
  "touchmove",
  (e) => {
    const touchY = e.touches[0].clientY;
    const deltaY = touchY - touchStartY;

    // If you really need to prevent default behavior, can't use passive
    // Here we're just recording, not preventing, so can use passive
    trackSwipe(deltaY);
  },
  { passive: true }
);

When Not to Use passive

If you need to call preventDefault(), you can't use passive: true.

javascript
// ❌ This will error
document.addEventListener(
  "touchstart",
  (e) => {
    e.preventDefault(); // Error! passive listener can't call preventDefault
  },
  { passive: true }
);

// ✅ If need to prevent default behavior, don't use passive
document.addEventListener("touchstart", (e) => {
  if (shouldPreventDefault(e)) {
    e.preventDefault(); // Can call
  }
}); // Don't set passive

// ✅ Or dynamically set based on condition
const needsPrevent = checkIfNeedsPrevent();

document.addEventListener("touchstart", handleTouch, {
  passive: !needsPrevent,
});

Optimization Technique 5: Remove Event Listeners Timely

Unremoved event listeners cause memory leaks, especially when elements are removed but listeners still exist.

Common Memory Leak Scenarios

javascript
// ❌ Memory leak example
function createModal() {
  const modal = document.createElement("div");
  modal.className = "modal";
  document.body.appendChild(modal);

  // Add event listeners
  modal.addEventListener("click", handleModalClick);
  window.addEventListener("resize", handleResize);

  // Close modal
  function closeModal() {
    document.body.removeChild(modal);
    // Problem: Event listeners not removed!
    // modal's click listener still exists (though element removed)
    // window's resize listener still exists
  }

  return { closeModal };
}

// Each call leaks memory
const modal1 = createModal(); // +2 listeners
modal1.closeModal(); // Listeners not cleaned up

const modal2 = createModal(); // +2 listeners
modal2.closeModal(); // Listeners not cleaned up

// Memory now has 4 zombie listeners

Correct Cleanup Method

javascript
// ✅ Correct approach
function createModal() {
  const modal = document.createElement("div");
  modal.className = "modal";
  document.body.appendChild(modal);

  // Use named functions for easy removal
  function handleModalClick(e) {
    // Handle click
  }

  function handleResize() {
    // Handle window resize
  }

  // Add listeners
  modal.addEventListener("click", handleModalClick);
  window.addEventListener("resize", handleResize);

  function closeModal() {
    // Remove listeners
    modal.removeEventListener("click", handleModalClick);
    window.removeEventListener("resize", handleResize);

    // Remove element
    document.body.removeChild(modal);
  }

  return { closeModal };
}

Use AbortController for Batch Removal

AbortController provides an elegant way to batch remove event listeners.

javascript
class Component {
  constructor(element) {
    this.element = element;
    this.abortController = new AbortController();
    this.signal = this.abortController.signal;

    this.init();
  }

  init() {
    // All listeners use the same signal
    this.element.addEventListener("click", this.handleClick, {
      signal: this.signal,
    });

    window.addEventListener("resize", this.handleResize, {
      signal: this.signal,
    });

    document.addEventListener("keydown", this.handleKeydown, {
      signal: this.signal,
    });
  }

  handleClick = (e) => {
    console.log("Click");
  };

  handleResize = () => {
    console.log("Resize");
  };

  handleKeydown = (e) => {
    console.log("Keydown");
  };

  destroy() {
    // Remove all listeners at once
    this.abortController.abort();
    this.element.remove();
  }
}

// Usage
const component = new Component(document.getElementById("my-component"));

// Automatically clean up all listeners when destroying
component.destroy();

Optimization Technique 6: Avoid Forced Synchronous Layouts

Reading layout information (like offsetHeight, getBoundingClientRect()) in event handlers and immediately modifying styles forces the browser to recalculate layout, which is a very expensive operation.

javascript
// ❌ Forced synchronous layout (very slow)
elements.forEach((element) => {
  const height = element.offsetHeight; // Read layout
  element.style.height = height + 10 + "px"; // Modify style, triggers layout
  // Next iteration reads layout again, browser forced to calculate again
});
// Each element causes a complete layout calculation (Layout Thrashing)

// ✅ Batch reads, then batch writes
const heights = [];

// First step: batch reads
elements.forEach((element) => {
  heights.push(element.offsetHeight);
});

// Second step: batch writes
elements.forEach((element, i) => {
  element.style.height = heights[i] + 10 + "px";
});
// Only triggers one layout calculation

Practical Application: Animation Optimization

javascript
// ❌ Mixed read/write in scroll event
window.addEventListener("scroll", () => {
  elements.forEach((element) => {
    const rect = element.getBoundingClientRect(); // Read
    element.style.transform = `translateY(${rect.top * 0.5}px)`; // Write
  });
});
// Every scroll causes multiple forced synchronous layouts

// ✅ Optimize with requestAnimationFrame
let ticking = false;

window.addEventListener("scroll", () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      updateParallax();
      ticking = false;
    });
    ticking = true;
  }
});

function updateParallax() {
  const scrollY = window.scrollY;

  elements.forEach((element) => {
    // All reads use the same scrollY
    const translateY = scrollY * 0.5;
    element.style.transform = `translateY(${translateY}px)`;
  });
}

Performance Testing and Monitoring

Using Performance API

javascript
// Measure event handler execution time
function measureEventPerformance(eventName, handler) {
  return function (event) {
    const startTime = performance.now();

    handler.call(this, event);

    const endTime = performance.now();
    const duration = endTime - startTime;

    if (duration > 16) {
      // Exceeds one frame time (60fps)
      console.warn(`${eventName} handler took ${duration.toFixed(2)}ms`);
    }
  };
}

// Usage
element.addEventListener(
  "click",
  measureEventPerformance("click", (e) => {
    // Your handling logic
    heavyComputation();
  })
);

Using Chrome DevTools

  1. Performance panel: Record user interactions, view event handler execution times
  2. Memory panel: Check for memory leaks, view event listener count
  3. Rendering panel: Enable "Paint flashing" and "FPS meter" to view repaints and frame rate

Real Optimization Cases

Case 1: Optimize Table Sorting

javascript
// ❌ Unoptimized version
table.addEventListener("click", (e) => {
  if (e.target.tagName === "TH") {
    const column = e.target.dataset.column;

    // Direct DOM manipulation, re-render entire table each time
    const rows = Array.from(table.querySelectorAll("tbody tr"));
    rows.sort((a, b) => {
      const aVal = a.querySelector(`td:nth-child(${column})`).textContent;
      const bVal = b.querySelector(`td:nth-child(${column})`).textContent;
      return aVal.localeCompare(bVal);
    });

    const tbody = table.querySelector("tbody");
    tbody.innerHTML = "";
    rows.forEach((row) => tbody.appendChild(row));
  }
});

// ✅ Optimized version
// 1. Use event delegation (already using)
// 2. Use debounce to avoid rapid clicks
// 3. Use DocumentFragment to reduce reflow
// 4. Cache data to avoid repeated queries

let sortedData = null;

const sortTable = debounce((column) => {
  if (!sortedData) {
    // First sort, extract data
    const rows = Array.from(table.querySelectorAll("tbody tr"));
    sortedData = rows.map((row) => {
      const cells = Array.from(row.querySelectorAll("td"));
      return {
        element: row,
        data: cells.map((cell) => cell.textContent),
      };
    });
  }

  // Sort data
  sortedData.sort((a, b) => {
    return a.data[column].localeCompare(b.data[column]);
  });

  // Use DocumentFragment for batch update
  const fragment = document.createDocumentFragment();
  sortedData.forEach((item) => fragment.appendChild(item.element));

  const tbody = table.querySelector("tbody");
  tbody.innerHTML = "";
  tbody.appendChild(fragment);
}, 100);

table.addEventListener("click", (e) => {
  if (e.target.tagName === "TH") {
    sortTable(parseInt(e.target.dataset.column));
  }
});

Case 2: Optimize Infinite Scroll

javascript
// ✅ Comprehensively optimized infinite scroll
class InfiniteScroll {
  constructor(options) {
    this.container = options.container;
    this.threshold = options.threshold || 200;
    this.onLoad = options.onLoad;
    this.loading = false;
    this.page = 1;
    this.hasMore = true;

    // Use Intersection Observer instead of scroll event
    this.setupObserver();
  }

  setupObserver() {
    // Intersection Observer performance far exceeds scroll event
    this.observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting && !this.loading && this.hasMore) {
            this.loadMore();
          }
        });
      },
      {
        root: this.container,
        rootMargin: `${this.threshold}px`,
      }
    );

    // Observe a bottom sentinel element
    this.sentinel = document.createElement("div");
    this.sentinel.className = "scroll-sentinel";
    this.container.appendChild(this.sentinel);
    this.observer.observe(this.sentinel);
  }

  async loadMore() {
    this.loading = true;

    try {
      const data = await this.onLoad(this.page);

      if (data.length === 0) {
        this.hasMore = false;
        this.observer.unobserve(this.sentinel);
        return;
      }

      // Use DocumentFragment for batch addition
      const fragment = document.createDocumentFragment();
      data.forEach((item) => {
        const element = this.createItemElement(item);
        fragment.appendChild(element);
      });

      this.container.insertBefore(fragment, this.sentinel);
      this.page++;
    } finally {
      this.loading = false;
    }
  }

  createItemElement(item) {
    const div = document.createElement("div");
    div.className = "item";
    div.textContent = item.title;
    return div;
  }

  destroy() {
    this.observer.disconnect();
    this.sentinel.remove();
  }
}

// Usage
const scrollLoader = new InfiniteScroll({
  container: document.getElementById("content"),
  threshold: 200,
  onLoad: async (page) => {
    const response = await fetch(`/api/items?page=${page}`);
    return response.json();
  },
});

Best Practices Summary

1. Choose Appropriate Optimization Techniques

  • Many similar elements: Use event delegation
  • Execute after user stops operation: Use debounce
  • Execute periodically during continuous operation: Use throttle
  • Touch and scroll events: Use passive listeners
  • No longer needed listeners: Remove timely

2. Performance Priorities

javascript
// Priority from high to low:

// 1. Avoid unnecessary listeners (most important)
// Use event delegation, one listener replaces hundreds or thousands

// 2. Use appropriate APIs
// Intersection Observer > Scroll Event
// ResizeObserver > Resize Event

// 3. Optimize handler performance
// Use throttle/debounce
// Avoid forced synchronous layouts
// Use requestAnimationFrame

// 4. Clean up timely
// Remove unneeded listeners
// Use AbortController for batch management

3. Testing and Monitoring

javascript
// Add performance monitoring in development environment
if (process.env.NODE_ENV === "development") {
  let eventCounts = {};

  const originalAddEventListener = EventTarget.prototype.addEventListener;
  EventTarget.prototype.addEventListener = function (type, listener, options) {
    eventCounts[type] = (eventCounts[type] || 0) + 1;
    console.log(`Event listeners: ${JSON.stringify(eventCounts)}`);

    return originalAddEventListener.call(this, type, listener, options);
  };
}

4. Code Checklist

  • [ ] Used event delegation to reduce listener count?
  • [ ] High-frequency events (scroll, mousemove) use throttle or debounce?
  • [ ] Operations like search, save use debounce?
  • [ ] Touch and scroll events use passive listeners?
  • [ ] Removed all listeners when component destroyed?
  • [ ] Avoided forced synchronous layouts in event handlers?
  • [ ] Used modern APIs (Intersection Observer, ResizeObserver)?

Summary

Event handling performance optimization is a crucial part of front-end development. Through this chapter, you should have mastered:

  1. Identifying Performance Issues: Understand characteristics of high-frequency events and performance bottlenecks
  2. Event Delegation: Replace hundreds or thousands with one listener, significantly reduce memory usage
  3. Debouncing Technique: Execute after user stops operation, reduce unnecessary computation and network requests
  4. Throttling Technique: Limit function execution frequency, maintain fluid user experience
  5. Passive Listeners: Tell browser won't prevent default behavior, improve scroll performance
  6. Timely Cleanup: Remove unneeded listeners, avoid memory leaks
  7. Avoid Forced Synchronous Layouts: Separate read/write operations, reduce layout calculations

Performance optimization is not premature optimization, but conscious design. When writing event handling code, always keep performance in mind, choose appropriate techniques, and your application can maintain silky smooth response speed.

This completes our comprehensive learning of JavaScript event system core content. From basic event concepts, to event handlers, listeners, event objects, bubbling and capturing, to event delegation, custom events, and performance optimization. Mastering this knowledge enables you to build high-performance, excellent user experience interactive web applications.