在之前 以用户为中心的性能指标 一文中,我们了解到在 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 的情况,开发者还给出了另外一种做法

参考资料