How to optimize resizing or scrolling

If you listen for events like mousewheel, resize or scroll, your page could get slow. These events are generated multiple times per second, and if the event handler takes too much time, the browser won’t catch with redrawing the page.

This is what to do if you have such problem.

Throttle#

By default, the event handler is executed on each event. Sometimes, when the handler is taking a long time, it might be acceptable to delay its execution to once per 100-200 ms. This “delaying” can be achieved with the _.throttle utility method.

When is this useful? E.g. if you recalculate the page layout each time the browser resizes. If recalculating layout takes a long time, you could perform it less often.

Before and after:

// Before
window.addEventListener('resize', () => {
  calculateLayout(); // Takes 20 ms
});

// After
window.addEventListener('resize', _.throttle(() => {
  calculateLayout(); // Still takes 20 ms, but now runs once in 100 ms
}, 100));

passive: true#

passive: true a flag that’s passed into addEventListener. It tells the browser that the event handler will never call event.preventDefault(). Using this knowledge, the browser can start doing the event action (e.g. scrolling) without waiting for the JS code to finish. This makes the page smoother.

Currently, only touchstart, touchmove and mousewheel event handlers could be made passive.

When is this useful? E.g. if you redraw the parallax background on each touch or scroll event. Most likely, it won’t be a problem if the browser starts scrolling before the background finishes redrawing. If so, such event handler could be made passive.

Before and after:

// Before:
window.addEventListener('touchstart', updateBackground)

// After:
window.addEventListener('touchstart', updateBackground, {
  passive: true
})

Use alternative APIs#

For some common tasks, there are alternative APIs that help to perform the task easier and with better performance:

  • Intersection Observer. It helps if you need to detect when an element goes off the screen or intersects with another element. This API can replace the scroll and resize listeners.
  • window.matchMedia. This API can notify you when a specific media query becomes true. Helps if you need to e.g. update the page layout when the viewport reaches a specific width. It can replace the resize listeners.
  • position: sticky. This CSS property lets you stick an element to the top when it reaches an edge of the viewport. Helps if you need to e.g. make a sticky page header. It can replace the scroll listeners that switch the element from position: static to position: fixed and vice versa.
  • At the moment of July 2017, the API is not yet well-supported. However, there’s a polyfill.

    Resize Observer. It helps if you need to detect when an element gets resized. This API can replace the scroll and resize listeners.

    Thanks to Vitali Kuzmich for mentioning this approach.

Optimize the code itself#

You can also make the event handler itself run faster. It should take not more than 10 ms to run.

To analyze how much time your handler takes, use Web Performance API or the Performance tab in Chrome DevTools:

The optimization approach depends on your code. Try caching some data or skipping some computations. If you use React, you can try dropping parts of the render tree with shouldComponentUpdate.

And this is what won’t work:#

  • requestAnimationFrame. It doesn’t help with slow event handlers and, by my tests, doesn’t bring any benefits for fast ones. Seems like it’s only useful for scheduling JS animations.
  • Event delegation. It optimizes the memory usage, not the speed.

That’s it. Optimize.

2 thoughts on “How to optimize resizing or scrolling”

Leave a Reply

Your email address will not be published.