Browsers, input events, and frame throttling

If there’s one thing I’ve learned about web performance, it’s that you have to approach it with a sense of open-mindedness and humility. Otherwise, prepare to be humbled.

Just as soon as you think you’ve got it all figured out, poof! Browsers change their implementation. Or poof! The spec changes. Or poof! You just flat-out turn out to be wrong. So you have to constantly test and revalidate your assumptions.

In a recent post, I suggested that pointermove events fire more frequently than requestAnimationFrame, and so it’s a good idea to throttle them to rAF. I also rattled off some other events that may fire faster than rAF, such as scroll, wheel, touchmove, and mousemove.

Do these events actually fire faster than rAF, though? It’s an important detail! If browsers already align/throttle these events to rAF, then there’s little point in recreating that same behavior in userland. (Thankfully an extra rAF won’t add an extra frame delay, though, assuming browsers fire the rAF-aligned events right before rAF. Thanks Jake Archibald for this tip!)

TL;DR: it varies across browsers and events. I’d still recommend the rAF-throttling technique described in my previous post.

Step one: check the spec

The first question to ask is: what does the spec say?

After reading the specs for pointermove, mousemove, touchmove, scroll, and wheel, I found that the only mention of animation frame timing was in pointermove and scroll. The spec for pointermove says:

A user agent MUST fire a pointer event named pointermove when a pointer changes button state. […] These events may be coalesced or aligned to animation frame callbacks based on UA decision.

(Emphasis mine.) So browsers are not required to coalesce or align pointermove events to animation frames, but they may do so. (Presumably, this is the point of getCoalescedEvents().)

As for scroll, it’s mentioned in the event loop spec, where it says “for each fully active Document […], run the scroll steps for that Document” as part of the steps before running rAF callbacks. So on the main document at least, scroll is definitely supposed to fire before rAF.

For contrast, here’s touchmove:

A user agent must dispatch this event type to indicate when the user moves a touch point along the touch surface. […] Note that the rate at which the user agent sends touchmove events is implementation-defined, and may depend on hardware capabilities and other implementation details.

(Emphasis mine.) So this time, nothing about animation frames, and also some language about “implementation-defined.” Similarly, here’s mousemove:

The frequency rate of events while the pointing device is moved is implementation-, device-, and platform-specific, but multiple consecutive mousemove events SHOULD be fired for sustained pointer-device movement, rather than a single event for each instance of mouse movement.

(Emphasis mine.) So we’re starting to get a pretty clear picture (or a hazy one, depending on your perspective). It seems that, aside from scroll, the specs don’t have much to say about whether events should be coalesced with rAF or not.

Step two: test it

However, this doesn’t mean browsers don’t do it! After all, it’s clearly in browsers’ interests to coalesce these events to animation frames. Assuming that most web developers do the simplest possible thing and handle the events directly, then any browser that aligns with rAF will avoid some unintentional jank from noisy input events.

Do browsers actually do this, though? Thankfully Jake has written a nice demo which makes it easy to test this. I’ve also extended his demo to test scroll events. And because I apparently have way too much free time on my hands (or I just hate uncertainty when it comes to browser stuff), I went ahead and compiled the data for various browsers and OSes:

pointermove mousemove touchmove wheel scroll
Chrome 76 (Windows 10) Y* Y* N/A Y* Y
Firefox 68 (Windows 10) Y Y N/A N Y
Edge 18 (Windows 10) N N N/A N Y
Chrome 76 (macOS 10.14.6) Y* Y* N/A Y* Y
Firefox 68 (macOS 10.14.6) Y Y N/A N Y
Safari 12.1.2 (macOS 10.14.6) N/A N N/A N N
Safari Technology Preview 13.1 (macOS 10.14.6) N N N/A N N
Chrome 76 (Ubuntu 16.04) Y* Y* N/A Y* Y
Firefox 68 (Ubuntu 16.04) Y Y N/A N Y
GNOME Web 3.28.5 (Ubuntu 16.04) N/A N N/A N N
Chrome 76 (Android 6) Y N/A Y N/A Y
Firefox 68 (Android 6) N/A N/A Y N/A Y
Safari (iOS 12.4) N/A N/A Y N/A N

Abbreviations:

  • Y: Yes, events are coalesced and aligned to rAF.
  • N: No, events fire independently of and faster than rAF.
  • N/A: Event doesn’t apply to this device/browser.
  • *: Except when Dev Tools are opened, apparently.

Conclusion

As you can see from the data, there is a lot of variance in terms of which events and browsers align to rAF. Although for the most part, it seems consistent within browser engines (e.g. GNOME Web is a WebKit-based browser, and it patterns with macOS Safari). Note though that I only tested a regular mouse or trackpad, not exotic input devices such as a Wacom stylus, Surface Pen, etc.

Given this data, I would take the cautious approach and still do the manual rAF-throttling as described in my previous blog post. It has the upside of being guaranteed to work roughly the same across all browsers, at the cost of some extra bookkeeping. [1]

Depending on your supported browser matrix, though, and depending on when you’re reading this (maybe at a point in the future when all browser input events are rAF-aligned!), then you may just handle the input directly and trust the browser to align it to rAF. [2]

Thanks to Ben Kelly and Jake Archibald for feedback on a draft of this blog post. Thanks also to Jake for clueing me in to this rAF-throttling business in the first place.

Footnotes

1. Interestingly, in the case of pointermove at least, the browser behavior can be feature-detected by checking getCoalescedEvents (i.e. Firefox and Chrome have it, Edge and Safari Technology Preview don’t). So you can use PointerEvent.prototype.getCoalescedEvents as a feature check. But there’s little point in feature-detecting, since manual rAF-throttling doesn’t add an extra frame delay in browsers that already rAF-align.

2. Jake also pointed me to an interesting detail: “Although these events are synced to rendering, they’ll flush if another non-synced event happens.” So for instance, keyboard events will interfere with pointermove and cause them to no longer sync to rAF, which you can reproduce in Jake’s demo by typing on the keyboard and moving the mouse at the same time. Another good reason to just rAF-throttle and be sure!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.