输入来到合成器

  1. 这是本系列文章的第四部分,在上一篇中,我们研究了渲染进程以及学习了合成。在本文中,我们将会探寻合成器在接收输入时如何实现平滑的响应

从浏览器的角度看待输入

  1. 当你听到输入事件的时候,你可能第一时间想到的是键盘的输入或者鼠标的点击,但是从浏览器的角度来看,输入意味着用户的任何操作,鼠标滚轮的滑动,触摸,鼠标的移动都是输入事件。 当发生触摸事件的时候,浏览器进程第一个接收到输入。然而浏览器进程只知道事件发生的几何位置,至于是什么内容,取决于渲染进程。因此浏览器进程将事件发生的位置以及类型发送给渲染进程。渲染进程通过合适的方式处理事件,找出其发起者,执行监听者的代码。<br />![image.png](https://cdn.nlark.com/yuque/0/2020/png/314703/1608692905095-57c2c2ab-e3ed-466f-a6d9-6f2b66b83d75.png#align=left&display=inline&height=278&margin=%5Bobject%20Object%5D&name=image.png&originHeight=278&originWidth=554&size=50438&status=done&style=none&width=554)<br />图1:用户输入从浏览器进程发送到渲染进程

合成器接收输入

  1. 在之前的内容中,我们了解合成器通过合成光栅化图层来实现平滑的滚动。如果一个页面没有事件监听者触发,那么合成器线程将会在主线程之外创建一个完整的合成帧,但是如果触发了事件监听者那么将会发生什么?合成器线程怎么样找到需要处理的事件。

composit (1).mp4 (270.55KB)图2:视口悬停在页面层上

理解非快速滚动区域

由于JS都在主线程上运行,因此在合成页面的时候,合成器会将包含事件处理的区域标记为非快速滚动区域。通过这个特性,如果时间发生在非快速滚动区域,合成器线程就会将事件发送给主线程。如果事件不是来自该区域,那么合成器线程将会独立于主线程进行新的一帧的合成。
image.png
图3:非快速滚动区域输入图

添加事件处理器的注意点

web开发中常见的就是事件委托。由于事件冒泡,你可以在最顶层的元素上添加一个事件处理程序,并且根据事件触发的目标来委派任务。您可能已经看到过如下代码:

  1. document.body.addEventListener('touchstart', event => {
  2. if (event.target === area) {
  3. event.preventDefault();
  4. }
  5. });

由于你只需要为所有元素只编写一个事件处理程序,因此事件委托模式的人体工程学很具有吸引力。但是如果从浏览器的角度来看待,那么现在整个页面都被标记为不可滚动区域。这意味着尽管您的程序对某些事件不关心,但合成器线程还是需要和主线程通信,并且在每次输入事件发生的时候等待它。因此,合成器的平滑滚动将会失效。
image.png
图4:事件委托下非快速滚动区域的示意图
为了避免这种情况,你可以通过传递 passive:true 给事件监听器。这个选项告诉浏览器你会在主线程中监听事件,但不影响后续的渲染,合成器可以合成新的帧。

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

检查事件是否可以取消

想象一下,你在页面中有一个框,你希望将滚动选项设置为水平。
传递 passive:true 可以使得页面滚动平滑,但是在你使用preventDefault 限制滚动方向时,垂直方向的滚动或许已经开始了。你可以使用event.cancelable再次检查一下。
image.png
图5:网页的部分元素可以水平滚动

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

另外,你可以用CSS规格touch-action来完全清除事件处理程序。

  1. #area {
  2. touch-action: pan-x;
  3. }

寻找事件触发者

当合成器线程发送input事件给主线程时,第一件事是找到发生事件的元素。命中测试通过使用在渲染过程中生成的绘画数据来找到事件发生坐标下面是什么元素。
image.png
图6:主线程在绘制记录中查找在xy点上绘制了什么

降低主进程上事件的触发

在上一篇文章中,我们知道了屏幕每秒刷新60次,以及如何保持节奏以实现平滑滚动。对于输入,常规的设备每秒发送60-120次触摸事件,而鼠标会发送100次时间。输入事件的保真度高于我们的屏幕刷新能力。
如果类似的连续事件比如 touchmove 每秒发送给主线程120次,那么与屏幕的刷新速度对比,它可能出发大量的点击事件和JS代码的执行。
image.png
图7:事件的不断触发,导致页面渲染混乱起来

为了尽量减少对主进程的调用,Chrome合并了连续事件并且延迟处罚,直至下一次requestAnimationFrame之前。
image.png
图8:对比以前相同的时间表,事件进行了合并并延迟触发

使用getCoalescedEvents 获取帧内事件

对于大多数Web应用程序,合并事件应该足以提供良好的用户体验。但是,如果要基于touchmove的坐标绘制应用程序或者展示路径 ,则可能会丢失中间的坐标。在这种情况下,可以在指针事件中使用getCoalescedEvents方法来获取有关那些合并事件的信息。
image.png
图9:左侧的平滑触摸手势路径对比右侧使用合并事件的路径

  1. window.addEventListener('pointermove', event => {
  2. const events = event.getCoalescedEvents();
  3. for (let event of events) {
  4. const x = event.pageX;
  5. const y = event.pageY;
  6. // draw a line using x and y coordinates.
  7. }
  8. });

下一步

在这本系列文章中,我们介绍了web浏览器的内部工作原理。如果你从来没有想过为什么开发者工具建议在事件处理程序上添加 { passive: true },或者为什么在script标签上添加async标识,那么我希望本系列文章能阐明为什么浏览器需要这些信息来加速内容的呈现。

使用Lighthouse

如果你想优化你的代码,并且不知道该如何做的时候,Lighthouse可以帮助你测试你的站点,并且提供一份报告,告诉你该如何改善你的代码。通过一系列的测试它可以告诉你很多建议,以便使浏览器更好的呈现页面。

学习如何衡量性能

由于站点的不同,性能调整方式也可能不同。因此怎么样衡量你的网站的性能和什么样的措施适合你的网站很重要,Chrome开发者工具有很多方案优化你的网站性能

最后

当我开发一个站点的时候,我几乎只关心怎么样更快的写代码。尽管这些很重要,但是我们也应该思考浏览器是如何处理我们的代码。现代浏览器一直都在致力于如何提升用户的体验。给浏览器提供更好的代码,作为回报,它将会改善你的性能。希望你加入我的队伍让浏览器变得更好。
image.png