Making touch scrolling fast by default

We know that scrolling responsiveness is critical to the user's engagement with
a website on mobile, yet touch event listeners often cause serious scrolling
performance problems. Chrome has been addressing this by allowing touch event
listeners to be
passive
(passing the {passive: true} option to addEventListener()) and shipping the
pointer
events API.
These are great features to drive new content into models that don't block
scrolling, but developers sometimes find them hard to understand and adopt.

We believe the web should be fast by default without developers needing to
understand arcane details of browser behavior. In Chrome 56 we are defaulting
touch listeners to passive by
default in cases where
that most often matches the developer's intention. We believe that by doing this
we can greatly improve the user's experience whilst having minimal negative
impact on sites.

In rare cases this change can result in unintended scrolling. This is usually
easily addressed by applying a
touch-action:
none style to the element where scrolling shouldn't occur. Read on for
details, how to know if you are impacted, and what you can do about it.

Background: Cancelable Events slow your page down

If you call
preventDefault()
in the touchstart or first touchmove events then you will prevent scrolling.
The problem is that most often listeners will not call preventDefault(), but
the browser needs to wait for the event to finish to be sure of that.
Developer-defined "passive event listeners" solve this. When you add a touch
event with a {passive: true} object as the third parameter in your event
handler then you are telling the browser that the touchstart listener will not
call preventDefault() and the browser can safely perform the scroll without
blocking on the listener. For example:

window.addEventListener("touchstart", func, {passive: true} );

The Intervention

Our main motivation is to reduce the time it takes to update the display after
the user touches the screen. To understand the usage of touchstart and touchmove
we added metrics to determine how frequently scroll blocking behavior occurred.

We looked at the percentage of cancelable touch events that were sent to a root
target (window, document, or body) and determined that about 80% of these
listeners are conceptually passive but were not registered as such. Given the
scale of this problem we noticed a great opportunity to improve scrolling
without any developer action by making these events automatically "passive".

This drove us to define our intervention as: if the target of a touchstart or
touchmove listener is the window, document or body we default
passive to true. This means that code like:

window.addEventListener("touchstart", func);

becomes equivalent to:

window.addEventListener("touchstart", func, {passive: true} );

Now calls to preventDefault() inside the listener will be ignored.

The graph below shows the time taken by the top 1% of scrolls from the time a
user touches the screen to scroll to the time the display is updated. This data
is for all websites in Chrome for Android. Before the intervention was enabled
1% of scrolls took just over 400ms. That has now been reduced to just over 250ms
in Chrome 56 Beta; a reduction of about 38%. In the future we hope to make
passive true the default for alltouchstart and touchmove listeners,
reducing this to below 50ms.

Breakage and Guidance

In the vast majority of cases, no breakage will be observed. But when breakage
does occur, the most common symptom is that scrolling happens when you don't
want it. In rare cases, developers may also notice unexpected click events
(when preventDefault() was missing from a touchend listener).

In Chrome 56 and later, DevTools will log a warning when you call
preventDefault() in an event where the intervention is active.

touch-passive.html:19 Unable to preventDefault inside passive event listener due to target being treated as passive. See https://www.chromestatus.com/features/5093566007214080

Your application can determine whether it may be hitting this in the wild by
checking if calling preventDefault had any effect via the
defaultPrevented
property.

We've found that a large majority of impacted pages are fixed relatively easily
by applying the
touch-action
CSS property whenever possible. If you wish to prevent all browser scrolling and
zooming within an element apply touch-action: none to it. If you have a
horizontal carousel consider applying touch-action: pan-y pinch-zoom to it so
that the user can still scroll vertically and zoom as normal. Applying
touch-action correctly is already necessary on browsers such as desktop Edge
that support Pointer Events and not Touch Events. For mobile Safari and older
mobile browsers that don't support touch-action your touch listeners must
continue calling preventDefault even when it will be ignored by Chrome.

In more complex cases it may be necessary to also rely on one of the following:

If your touchstart listener calls preventDefault(), ensure
preventDefault() is also called from associated touchend listeners to continue
suppressing the generation of click events and other default tap behavior.

Last (and discouraged) pass {passive: false} to addEventListener() to
override the default behavior. Be aware you will have to feature detect if the
User Agent supports
EventListenerOptions.

Conclusion

In Chrome 56 scrolling starts substantially faster on many websites. This is the
only impact that most developers will notice as a result of this change. In some
cases developers may notice unintended scrolling.

Although it's still necessary to do so for mobile Safari, websites should not
rely on calling preventDefault() inside of touchstart and touchmove
listeners as this is no longer guaranteed to be honored in Chrome. Developers
should apply the touch-action CSS property on elements where scrolling and
zooming should be disabled to notify the browser before any touch events occur.
To suppress the default behavior of a tap (such as the generation of a click
event), call preventDefault() inside of a touchend listener.