原文链接:https://developers.google.com/web/updates/2017/01/scrolling-intervention
我们都知道,对于移动端的网页而言,滚动是十分重要的交互,然而 touch 系列事件触发(滚动后)经常会引发严重的性能问题。为了解决这问题,Chrome (通过允许往addEventListener()
中传入{passive: true}
)让touch系列事件的事件监听器变为“被动”(译者注:其实就是touch之后,不再是执行完事件函数后再滚动),同时 pointer
事件相关的API(也支持相关参数)。这些都是很有用的特性,能使处理( touch 系列)事件的过程中,不会妨碍页面的正常滚动,然而开发者们有时候会觉得它们难以理解,更不会去使用。
我们相信,即使开发者们完全参透浏览器(各种)复杂的细节,也不会使网站性能提高多少。因而在 Chrome 56中,我们将touch系列事件的监听器默认设为“被动”,大多数情况下这正是前端所需要的。我们相信这会极大地提高用户体验,也不会网站造成多大的影响。
在极个别例子下,这会导致意外的滚动。可以通过在(意外滚动发生的)元素上添加CSStouch-action:
none(来阻止滚动发生)。继续阅读,你可以了解到更多相关技术的细节。
背景: 取消默认时事件会降低页面性能
如果你在touchstart
或touchmove
事件处理函数中调用preventDefault(),这将会阻止(页面)滚动。然而,问题是大多说情况下是不会在事件处理函数中调用preventDefault()
,但浏览器需要等到事件处理函数执行完之后才能确定这点。因而开发者可以定义“被动的事件监听器”去解决这问题。当你注册 touch 系列事件的监听器时,加入{passive: true}
对象作为第三个参数后,浏览器就认为你不会在事件处理函数中调用 preventDefault()
,它就可以安全地让页面滚动,不再等待事件处理函数执行完(再判断)。例子如下:
1 | `window.addEventListener("touchstart", func, {passive: true} );` |
优化
我们主要的目的是为了降低用户触摸屏幕后,内容(滚动)更新的响应时间。为了解 touchstart 和 touchmove 的使用(情况),我们添加了对这两个事件阻止滚动(发生)频率的监控。
我们看到,其中大约80%表现上都是“被动(译者注:也就是并未调用 preventDefault() )”的,但(开发者)却基本没将该事件的监听器注册为“被动”。鉴于此问题的严重性,我们意识到可以通过默认将这些事件(的监听器)设置为“被动”来提高滚动的性能,而且基本不需要任何开发者修改代码。
因而我们做出以下优化:只要 touchstart 或者 touchmove 事件的事件监听器是注册在window
, document
或body
上的时候,我们会将passive
默认为true
,也就是说代码是这样的:
1 | `window.addEventListener("touchstart", func);` |
等同于:
1 | `window.addEventListener("touchstart", func, {passive: true} );` |
现在,在事件处理函数中,调用preventDefault()
会变为无效。
下图展示了用户触发滚动后到真正滚动期间,耗时最长的前百分之一案例中所耗费的时间。这些数据是由安卓上的 Chrome 访问任意网页后采集的。在优化( passive )之前,平均耗时是超过400ms,优化后降低到250ms,时间降低了38%。在未来,我们希望默认为所有touchstart
和touchmove
的事件监听器的passive
设置为true
,并优化到(滚动响应)低于50ms。
问题与修复
大部分情况下,(我们的优化)不会导致任何 BUG 。但是如果 BUG 真的出现了,最常见问题是当你不希望页面发生滚动时却发生了。极个别的例子是,开发者发现(如果不在touchend
事件处理函数中调用preventDefault()
) click 事件会被触发了。
在 Chrome 56版本及其之后的版本,如果触发了优化,而你又在事件处理函数中调用了preventDefault()
,在 DevTools 中会打印一条警告(作为提示)。
1 | `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` |
你可以直接在应用中,通过defaultPrevented
属性检查preventDefault
的调用是否生效。
我们发现大量受影响的页面,通过使用touch-action这个CSS属性就能轻松修复(问题)。如果你希望某个元素(无论进行任何 touch 操作都)不会使浏览器发生滚动或缩放,可以往该元素的CSS中加入touch-action: none
。如果想某个元素只可以水平滚动或缩放,可以使用touch-action: pan-y pinch-zoom
。在浏览器中正确地使用 touch-action 是已经是非常重要了,如 PC 端中 Edge 支持 Pointer 事件,不支持 Touch 事件。可惜的是,移动端 Safari 和一些旧的浏览器不支持 touch-action,因此你仍然需要在事件处理函数中调用preventDefault
,尽管 Chrome 会忽略它。
在更复杂的例子中,你可能需要参考下面的其中一条(来解决问题):
如果你的
touchstart
事件的监听器中,调用了preventDefault()
,为阻止触发click
事件和浏览器的默认行为,请确保preventDefault()
在相关的touchend
事件的监听器中也被调用。如果需要往
addEventListener()
中传入{passive: false}
来覆盖浏览器默认行为,请确保该浏览器支持EventListenerOptions属性。
小结
开发者通常只会察觉到,通过优化后的 Chrome 56中访问大多数网页时,滚动响应会更快。而在个别的例子中,开发者可能会发现一些意外的滚动。
虽然仍需要为移动端的 Safari 调用preventDefault()
,然而 Chrome 已经不再推荐网站依靠在touchstart
和 touchmove
事件处理函数中调用preventDefault()
(来阻止浏览器默认行为)。开发者在需要时,应该在 touch 系列事件发生前,使用touch-action
这一 CSS 属性去阻止某元素滚动或缩放。只有在为了阻止之后的默认行为(如将要触发的click事件)时,才应该在touchend
的事件处理函数中调用preventDefault()
。
除非另有声明,此文章采用Creative Commons Attribution 3.0 License许可,示例代码采用Apache 2.0 License许可。如需了解更多,请查阅我们的网站政策。