Chrome架构4 - 交互处理

本系列文章从内部剖析了 Chrome 浏览器的进程、线程架构设计,各部原理等基础知识,梳理了浏览器是怎样将你的代码变成网站的,解开某个特定技术为什么可以提高性能的疑问。本文是这个系列的最后一篇,讲述了 Chome 浏览器怎样处理响应来自外部的事件并流畅交互的。

本系列文章共 4 篇,原文发布在 Google 开发者博客,作者 Mariko Kosaka 。文章中涉及非中文局域网友好链接请读者自行解决。

之前我们了解了 CPU,GPU,内存和多进程架构从在地址栏键入到文档开始加载前的导航过程加载渲染过程与合成器。在这篇文章中,我们将看看触发输入时如何实现流畅交互。

浏览器视角的输入事件

当看到“输入事件”时,您可能会想到键入文本框或鼠标单击。但从浏览器的角度来看,输入意味着来自用户的任何操作、手势。鼠标滚轮滚动是输入事件,触摸或鼠标移动也是输入事件。

当发生诸如屏幕触摸的事件时,浏览器进程是首先接收到手势事件的进程。但是浏览器进程只知道该手势发生的位置,因为选项卡内部的内容由渲染器进程处理。因此,浏览器进程将事件类型(如touchstart)及其坐标发送到渲染器进程。渲染器进程通过查找事件目标节点并运行对应的事件监听器来适当地处理这个事件。

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

合成器(Compositor)接收输入事件



上一篇文章中,我们研究了合成器如何通过合成栅格化图层来流畅地处理页面滚动。如果没有输入事件侦听器附加到页面,则合成器线程可以创建完全独立于主线程的新合成帧。但是如果一些事件监听器被附加到页面或节点上呢?如果需要处理事件,合成器线程将如何找出?

了解非快速可滚动区域

由于 JavaScript 运行在主线程,因此当合成页面时,合成器线程将标记页面设有事件监听器的区域为“非快速可滚动区域”。通过获取此信息,合成器线程可以确保在该区域中发生事件时将输入事件发送到主线程。如果输入事件来自该区域之外,则合成器线程在不等待主线程的情况下合成新帧。

描述非快速可滚动区域的输入图描述非快速可滚动区域的输入图

在编写事件处理程序时要注意

Web 开发中的常见事件处理模式是监听器。由于事件冒泡机制,您可以在最顶层的元素上附加一个事件处理程序,并根据事件目标分别处理任务。您可能看过或编写过类似一下代码。

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

由于您只需要为所有元素编写一个事件处理程序,因此该事件代理模式的编码效力很有吸引力。但是,如果从浏览器的角度来看这段代码,现在整个页面都被标记为非快速可滚动区域。这意味着即使您的应用程序不关心页面某些部分的输入,合成器线程也必须与主线程通信并在每次输入事件进入时等待执行。因此,合成器的快速平滑滚动将失效。

覆盖整个页面的非快速可滚动区域的输入的图覆盖整个页面的非快速可滚动区域的输入的图

为了避免这种性能损失,您可以在事件监听器中传递 passive: true 选项。这提示浏览器您仍然希望在主线程中监听事件,但是合成器也可以继续并合成新帧。

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

检查事件是否可取消

想象一下您在页面中有一个框,您希望这个框只能水平滚动。

一个网页,其中部分页面固定为水平滚动一个网页,其中部分页面固定为水平滚动

在监听事件时使用 passive: true 选项意味着页面滚动可以是流畅的,但是在您想要 preventDefault 限制滚动方向时可能已经开始垂直滚动。此时您可以使用event.cancelable 方法对此进行检查。

document.body.addEventListener('pointermove', event => {
if (event.cancelable) {
event.preventDefault(); // block the native scroll
/**
* do what you want the application to do here
*/
}
}, {passive: true});

或者,您可以使用CSS touch-action 来消除事件处理程序。

#area { 
touch-action: pan-x;
}

查找事件目标

当合成器线程向主线程发送输入事件时,首先要执行命中测试以查找事件发生在哪个目标节点上。命中测试使用在渲染过程中生成的绘制记录数据来查找事件发生的点坐标下的内容。

主线程查看绘制记录,询问在xy点上绘制的目标节点主线程查看绘制记录,询问在xy点上绘制的目标节点

将分发到主线程的事件最小化

在上一篇文章中,我们了解到通常显示器每秒刷新屏幕60次,以及我们要保证刷新频率以获得流畅的动画效果。对于输入,典型的触摸屏设备每秒发送60-120次触摸事件,而通常鼠标每秒发送100次事件。输入事件具有比屏幕刷新更高的频率。

如果事件 touchmove 连续发送到主线程120次每秒,那么与屏幕刷新速度相比,它可能会触发过多的命中测试和 JavaScript 代码执行。

时间线事件过多导致页面卡顿时间线事件过多导致页面卡顿

为了尽量减少对主线程过度调用,Chrome 聚合了连续事件(如 wheelmousewheelmousemovepointermovetouchmove)和延迟调度运算直到下一个 requestAnimationFrame 到来。

与之前相同的时间线,但事件合并并延迟与之前相同的时间线,但事件合并并延迟

任何离散不连续的事件,如 keydownkeyupmouseupmousedowntouchstart,和 touchend 等会立即触发。

使用 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 平台功能,可以在您构建项目时为您提特性开关。启用功能策略可确保应用程序的某些行为,并防止您犯错误。例如,如果要确保应用程序永远不会阻塞解析,可以在同步脚本策略上运行应用程序。当 sync-script: 'none' 启用时,解析是将不会阻塞执行 JavaScrip。这可以防止您的任何代码阻塞解析器,并且浏览器不需要担心解析器被阻塞。