R·ex / Zeng


音游狗、安全狗、攻城狮、业余设计师、段子手、苦学日语的少年。

由于浏览器的优化导致的 Bug 们

按理说,随着时间的发展,浏览器会做各种各样的优化来提升网页浏览的性能,但不是所有优化都能起到积极的效果,有的优化甚至属于“智障”级别。下面我就分享三个我遇到或听说的、由于浏览器自身的优化导致的一些 Bug。

Passive Event Listeners

我之所以知道了这个东西,是因为昨天邻居向我抱怨的一个问题,他的移动端页面使用了一个日期选择器,但在上下滑动选择日期的时候,Console 里面会报一堆这样的错误:

[Intervention] Unable to preventDefault inside passive event listener due to target being treated as passive. See https://www.chromestatus.com/features/5093566007214080

“Passive”这个词,一开始我还以为是“由代码触发的”监听器,直到我去网上搜了一圈才知道,这是一个开始于 Chrome 51 的特性,旨在提升页面滑动的流畅度。大概的理解是(摘自 SegmentFault,有修改):

当你触摸滑动页面时,页面应该跟随手指一起滚动。如果你绑定了一个触摸事件(大概执行 200 毫秒),浏览器就犯迷糊了:如果你在事件绑定函数中调用了 preventDefault,那么页面就不应该滚动;但是浏览器一开始不知道你有没有调用,只能先执行你的函数,等 200 毫秒后,浏览器才知道,“哦,原来你没有阻止默认行为,好的我马上滚”。此时,页面开始滚。

经统计,80% 左右的触摸事件都没有被 preventDefault 阻止,这些等待的时间都被浪费掉了。使用 Passive Event Listeners,开发者能够提前告诉浏览器:“我不调用 preventDefault 函数来阻止事件默认行为”,那么浏览器就无需等待事件执行完成,而是直接执行默认行为(滚动)。

一开始在 Chrome 浏览器中,如果发现耗时超过 100 毫秒的非 Passive 的监听器,会在 DevTools 里面警告你:需要把 addEventListener 的第三个参数设为 {passive: true};但新版 Chrome 有一个实验性的功能:Passive Event Listener Override(chrome://flags/#passive-listener-default),目的是将 touchstarttouchmovemousewheel 等事件当作 Passive 事件,而且这个功能是默认开启的,因此在新版 Chrome 中,如果你没有显性的使用 dom.addEventListener(type, fn, { passive: false }) 的话,你在这些事件中的 preventDefault 不光没有用,还会报个错。

虽然解决方法很简单,但很讨厌的一点就是,这个特性并不是在某次重大更新中发布的,除非你每天盯着 Chrome Release Note,否则很容易被忽视掉;而且它打破了之前的规则,导致你的网站在某一天突然收到客户的 Bug 反馈。

Firefox Quantum 的并行引擎

这个坑主要是我的原因。在上古时期,我需要在输入框内容改变的时候,实时获取其值(change 无法满足实时的条件),当时还没有 input 事件,因此我的方法是这样的:

input.addEventListener('keydown', function () {
    setTimeout(function () {
        console.log(input.value);
    }, 0);
});

虽然 keydown 触发的时候 DOM 还没更新,此时获取 input.value 会得到旧值,但经过 setTimeout 包了一层,于是用来获取的代码被放到了下一个事件循环中,此时 input.value 已经改变了。这个写法在 2017 年 11 月前的最新浏览器中都是没问题的。

然而 2017 年 11 月发布的 Firefox Quantum 使用了 Servo 作为浏览器引擎,后者利用了 CPU 多核的优势,让 DOM 的更新脱离了 JavaScript 的事件循环。这会提高渲染的速度,但同时会导致我的代码失效,因为此时无法保证下一个事件循环中 input.value 究竟有没有变化。

解决方法很简单,使用 HTML5 的 input 事件即可。不过我总觉得,如果大家不知道 DOM 的更新已经脱离了事件循环,以后可能会出现其它类似的问题……

Webkit CSS3 合成层的灾难

这是我偶然从一篇文章里面看到的。文章链接在本文最下方,由于七牛回收了测试域名,文章中的图片已经失效了。

人们常说移动端要想动画性能流畅,应该使用硬件加速,但人们还常说“优化没有银弹”,一味的使用硬件加速反而会降低性能。

Webkit 中的硬件加速原理是:把需要渲染的元素放到特定的合成层,这些层里面的内容是静态的,因此 Webkit 可以将其作为纹理上传到 GPU,以提高渲染速度。这个技术对于前端来说,通常是用于避免当某个元素的某些属性改变时重绘页面,因为如果它自成一层,就可以把其它元素搞成纹理,再整体渲染;但如果合成层的数量过多,那么需要绘制的纹理就太多,这相当于没做什么优化。

一般来说,有经验的开发者会注意到这一点,因此不会使用太多的 transform: translateZ(0) 来启用硬件加速。但其实 Webkit 的“层创建标准”还真不少,而且满足任一标准即可创建层(未来标准可能会改变):

  • 3D 或透视变换(perspective transform)CSS 属性
  • 使用加速视频解码的 <video> 元素
  • 拥有 3D (WebGL) 上下文或加速的 2D 上下文的 <canvas> 元素
  • 混合插件(如 Flash)
  • 对自己的 opacity 做 CSS 动画,或使用一个动画变换的元素
  • 拥有加速 CSS 过滤器的元素
  • 元素有一个包含合成层的后代节点(换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)
  • 元素有一个 z-index 较低且包含一个合成层的兄弟元素(换句话说就是该元素在合成层上面渲染)

注意最后一句,这意味着,如果你没有给“可能自成一层”的元素设置一个比较大的 z-inedx,浏览器有可能会给“所有跟它父元素相同的相对或绝对定位的元素”都创建一个合成层来渲染……

这特么不是智障是什么?!

所以如果怀疑页面滚动出了问题,或想做一些性能优化,可以先看看是不是这儿的问题,调试的方法是在 Chrome 中开启“Show composited layer borders”,控制台和 Flags 里面都可以开。如果发现页面中的黄色边框太多,那说明你的合成层太多了,如果有些元素本来没必要开启合成层,但却被黄色边框框起来了,那你可能需要检查一下 CSS 代码了。


参考文章

版权声明:除文章开头有特殊声明的情况外,所有文章均可在遵从 CC BY 4.0 协议的情况下转载。

这是我们共同度过的

第 1114 天