[译]深入了解现代web浏览器(四)

本文是根据Mariko Kosaka在谷歌开发者网站上的系列文章https://developer.chrome.com/blog/inside-browser-part4/翻译而来,共有四篇,该篇是第四篇。对于其中一些直译出来不太好理解的句子,笔者做了加工处理和提炼。

输入来到了合成器

在上篇文章中,我们了解了渲染过程和合成器;在这片文章中,我们将来了解下合成器是如何在用户输入到来时保持交互流畅的。

从浏览器的角度看输入事件

当你听到“输入事件”时,你可能只会想到在文本框中输入或是鼠标点击;但从浏览器的角度来看,来自用户的任何动作都是输入。鼠标滚轮滚动、触摸或者鼠标悬浮都是一个输入事件。

当用户在屏幕做出触摸等动作时,浏览器进程最先接收到该动作。但是浏览器进程只关注该动作发生的位置,因为tab页中的内容是由渲染进程处理的。浏览器进程把事件类型(比如touchstart)和其坐标发送给渲染进程;渲染进程会找到对应的事件目标并执行目标上绑定的事件监听器。

通过浏览器进程路由到渲染进程的输入事件

合成器接收输入事件

在上一篇文章中,我们知道了合成器是如何通过合成光栅化的图层来达到流畅地处理滚动的。如果页面上没有绑定事件监听,合成线程可以完全独立于主线程来创建合成帧。但如果页面上绑定有事件监听呢?毕竟事件监听的回调函数只能由主线程来执行。

理解非快速滚动区域

由于运行Javascript是主线程的工作,当页面被合成时,合成线程会将页面上绑定有事件监听的区域标记为“非快速滚动区域(Non-Fast Scrollable Region)”。拥有这些信息,合成线程能够确保当有事件发生在该区域时能将输入事件发送给主线程。如果输入事件是来自该区域之外的,则合成线程会继续合成新的帧而无需等待主线程执行事件处理函数。

非快速滚动区域的输入事件

注意你编写的事件处理函数

在web开发中一种很常见的处理模式是事件委派。由于事件冒泡机制的存在,你可以在最顶层的元素绑定事件处理函数然后根据事件目标来委派处理函数。你可能见过或者写过如下的代码:

document.body.addEventListener('touchstart', event => {
    if (event.target === area) {
        event.preventDefault();
    }
});

由于你只需要为所有的元素只编写一个处理函数,从工程学上讲这种事件委派模式很有吸引力。但如果从浏览器的角度来看这段代码,现在整个页面都被标记为了“非快速滚动区域”。这意味着,即使你的应用并不关心页面上某些部分产生的输入事件,但只要发生了输入事件,合成线程还是不得不与主线程发生通信并等待。因此,合成线程的流畅性就会收到影响。

覆盖到了整个页面的非快速滚动区域

为了减轻这种情况的影响,你可以向事件处理函数中传入passive: true选项。这能提示浏览器你在事件处理函数中不会调用event.preventDefault(),就意味着你代码中不会通过event.preventDefault()语句阻止事件的默认行为(没有这个选项的话,则需要主线程执行完你的处理函数然后才能决定是否要阻止诸如滚动、失焦之类的事件默认行为,再告知合成线程要合成新的帧)。因此,在有passive: true的情况下,就可以让主线程在执行处理函数的同时,合成线程能继续合成下一帧。

document.body.addEventListener('touchstart', event => {
    if (event.target === area) {
        event.preventDefault()
    }
 }, {passive: true});

寻找事件目标

当主线程收到合成线程发送的输入事件后,首先要做的就是执行命中测试来找到事件目标。命中测试利用渲染阶段生成的绘制记录来找出发生事件的点坐标下方的内容。

主线程根据绘制记录查询x.y点处绘制的内容

最小化事件调度到主线程

在上一篇文章中,我们讨论了典型的显示器是如何一秒钟刷新60次并保持动画流畅的节奏。对于输入,典型的触屏设备每秒会传递60-120次触屏事件,而典型的鼠标每秒会传递100次事件。输入事件的保真度高于我们屏幕刷新频率的保真度。

如果像touchmove这样的连续事件被每秒120次地发送到主线程,那么与屏幕的刷新速度相比,它会触发过多的命中测试和Javascript执行。

帧的时间轴上被大量事件淹没导致页面卡顿

为了尽量减少在主线程上的调用,Chrome会合并连续的事件(例如wheel, mousewheel, mousemove, pointermove, touchmove)并延迟调度直至下一次requestAnimationFrame之前。

与上一张图片相同的时间轴,但事件被合并和延迟了

任何的离散事件例如keydown,keyup,mouseup,mousedown,touchstarttouchend都会被立即调度。

使用getCoalescedEvents获取帧内事件

对于绝大部分的web应用,合并事件足以提供良好的用户体验。然而,如果你正在构建诸如绘画或者基于touchmove坐标的路径放置的应用,你可能会因丢失中间坐标而不能绘画出平滑的线段。这种情况下,你可以使用指针事件中的getCoalescedEvents方法来获取更多关于这些合并事件的信息。

左侧是平滑触摸手势的路径,右侧是受合并事件限制的路径

window.addEventListener('pointermove', event => {
    const events = event.getCoalescedEvents();
    for (let event of events) {
        const x = event.pageX;
        const y = event.pageY;
        // draw a line using x and y coordinates.
    }
})

下一步

在本系列中,我们介绍了Web浏览器的内部工作原理。如果您从未想过为什么DevTools建议在事件处理程序中添加{passive: true},或者为什么你可能要在脚本标签中写入async属性;我希望本系列文章能够阐明为什么浏览器需要这些信息来提供更快、更流畅的Web体验。

使用Lighthouse

如果你想让自己的代码对浏览器更友好但不知道从哪里开始,Lighthouse会是一个不错的工具。它可以对任何网站执行审计并提供一份报告,告知你哪些地方做的不错而哪些地方还需要改进。通过阅读审计列表,可以让你知道浏览器关心哪些指标。

了解如何衡量性能

对于不同站点性能调整会有所不同,因此如何衡量你的站点性能并确定最适合的方案是非常重要的。Chrome的DevTools团队有一些关于衡量站点性能的教程

总结

当我开始构建网站的时候,几乎只关心书写代码和哪些能帮助我提高效率的东西。这些事当然很重要,但我们也应该思考下浏览器是如何处理我们书写的代码的。现代浏览器为给用户提供更好的web体验而持续努力。书写对浏览器“友好”的代码,这反过来会改进你的用户体验。希望你能加入我们一起追求对浏览器更为友好的世界!

posted @ 2022-07-20 17:21  爱喝可乐的咖啡  阅读(129)  评论(2编辑  收藏  举报