在之前 以用户为中心的性能指标 一文中,我们了解到在 RAIL 性能评估模型中,滚动时的卡顿是非常影响用户体验的。 在下面的演示 video中,可以明显地看到滚动是存在 jank 的。
浏览器判定滚动流程
究其原因,由于页面注册的事件监听函数,尤其是移动端的 touch 类型事件,是可以通过 preventDefault()
阻止浏览器的默认行为的,例如滚动,页面缩放等等。所以对于浏览器来说,需要等待 touchstart
touchmove
事件处理函数的执行结果,才能决定是否要继续滚动。如下图所示:
而在大部分场景下,开发者的注册的事件监听函数是不会调用 preventDefault()
的,浏览器的等待就显得没有必要了。
或者说,如果开发者能显式的告知浏览器接下来不会取消滚动,浏览器就可以放心的执行滚动,也就不存在延迟了。
passive event listener
在注册事件监听函数时,赋予第三个参数更多功能:
target.addEventListener(type, listener[, options]);
可以通过这个参数对象告知浏览器这是一个“被动”的函数,不会主动阻止浏览器默认行为。
所以不仅仅针对滚动场景下,而且这个参数将来也不仅仅只可以传 {passive: true}
。
这里有两点需要注意,首先是类型检测。当然也可以借助 Modernizr、DetectIt 等特性检测库。下面是一种简单的检测方法:
var supportsPassive = false;
try {
var opts = Object.defineProperty({}, 'passive', {
get: function() {
supportsPassive = true;
}
});
window.addEventListener("testPassive", null, opts);
window.removeEventListener("testPassive", null, opts);
} catch (e) {}
其次,如果检测通过,当然可以直接使用,但如果未通过:
Because older browsers will interpret any object in the 3rd argument as a true value for the capture argument
所以在这种情况下我们要传一个 false
,完整的兼容方案如下:
elem.addEventListener('touchstart', fn,
detectIt.passiveEvents ? {passive:true} : false);
在现有项目中应用
从浏览器支持度上看,iOS 10.3+ 就已经支持了。 所以很多框架和类库也针对这一特性做了优化。当然你也可以引入 polyfill。
在 Polymer 中,在使用内置的手势库时,也有这样的设置:
Polymer.setPassiveTouchGestures(true);
Chrome 的激进做法
Chrome 发现在 window
document
body
上注册的 touch 事件,80% 的情况都不会阻止滚动,而很多开发者又不会调用 passive
。
因此想到了在这种情况下主动介入。
因此以下两种写法在 Chrome 56+ 中是等价的:
window.addEventListener("touchstart", func);
window.addEventListener("touchstart", func, {passive: true});
虽说在这 80% 的情况下确实不需要开发者操心就能提升滚动性能。
但是 Chrome 的这种非向前兼容的介入会给部分已有代码带来问题,因为试图阻止滚动的 preventDefault()
将不再起作用。
在这种 Chrome 介入的情况下,控制台会给出如下提示:
Unable to preventDefault inside passive event listener due to target being treated as passive.
See https://www.chromestatus.com/features/5093566007214080
毕竟还有 20% 的场景确实需要阻止浏览器滚动,我们应该怎么做呢?
阻止滚动的场景
现在我们了解了优化滚动的方式,那么有没有反倒希望阻止浏览器滚动行为的场景呢?
或者说在 Chrome 介入的情况下,除了 preventDefault()
,我们还能有什么方法停止滚动呢?
比如在开发者更熟悉的 IScroll 中,由于需要自己处理滚动,通常会在 touchmove
事件处理函数中调用 preventDefault()
禁止浏览器的滚动。
如果定义在 document
上,在 Chrome 中运行时就失效了。在 ISSUE 中搜索 passive 关键词,会出来很多结果
首先想到的一种改动方法,告知 Chrome 这是一个非 passive 的处理函数:
document.addEventListener('touchmove', function (e) { e.preventDefault(); }, isPassive() ? {
capture: false,
passive: false
} : false);
显然,这种方法又回到了老路上。实际上,我们通过 CSS 属性就能提前告知浏览器禁止滚动,那就是 touch-action
,禁止滚动和缩放:
#wrapper {
touch-action: none;
}
在只需要禁止某个方向上的滚动场景下,touch-action: pan-y pinch-zoom
。但是如果要兼容老的浏览器,preventDefault()
还是不能删除的,尽管 Chrome 会忽略。
针对 IScroll 的情况,开发者还给出了另外一种做法。