[译] 深入了解现代网络浏览器(第4部分) - 浏览器的交互实现

393 阅读7分钟

作者:Mariko Kosaka

原文链接

用户输入时的 compositor

这是深入了解现代网络浏览器四部曲的最后一篇,让我们一起探查渲染一个网站时,他是怎么处理我们的代码的。在前一个章节,我们知道了rendering 进程和compositor, 在本章节,我们将聚焦 compositor 如何在用户输入时实现顺畅的互动

从浏览器的角度输入事件

当您听到“输入事件”时,您可能只会想到键入文本框或单击鼠标,但是从浏览器的角度来看,输入表示用户的任何手势。 鼠标滚轮滚动是输入事件,触摸或鼠标悬停也是输入事件。

当发生用户手势(如屏幕上的触摸)时,浏览器进程将最先接收到手势的。 但是,由于 tab 内部的内容由 renderer process 处理,浏览器进程仅知道该手势在何处(x、y)发生。 因此,浏览器进程将事件类型(如 touchstart )及其坐标发送到 renderer process。 renderer process 通过找到事件的目标并运行他的事件侦听器来处理这个事件。

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

Compositor 接受到 input 事件

在上一篇文章中,我们研究了 Compositor 如何通过合成栅格化图层来平滑处理滚动。 如果该页面上没有任何输入事件侦听器,则 Compositor 线程可以创建一个完全独立于主线程的 Compositor Frame。 但是,如果将某些事件侦听器附加到页面上怎么办? Compositor 线程将如何找出是否事件需要被处理呢?

图2:视口悬停在页面层上

了解非快速滚动区域

由于运行 JavaScript 是主线程的工作,因此在合成页面时,Compositor 线程会将页面上具有事件处理程序的区域标记为“非快速可滚动区域”。 通过获取此信息,如果事件发生在该区域中,则 Compositor 线程可以确保将输入事件发送到主线程。 如果输入事件来自该区域之外,则 Compositor 线程将在不等待主线程的情况下进行新的 frame 合成。

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

当你在代码中处理 event handlers 时应该注意的事

Web开发中常见的事件处理模式是事件委托。 由于事件冒泡,因此您可以在最顶层的元素上附加一个事件处理程序,并根据事件目标委派任务。 您可能已经看过或编写了如下代码。

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

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

图4:描述的覆盖整个页面的非快速滚动区域的输入图

为了减轻这种情况的发生,您可以在事件监听器中传递passive:true选项。 这向浏览器暗示您仍要在主线程中侦听事件,但是合成器也可以继续合成新的 frame。

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

检查事件是否可取消

假设您在页面中有一个Box,希望将滚动方向限制为仅水平滚动。

在指针事件中使用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;
}

找到事件的根源

当 compositor 线程向主线程发送 input 事件时,要运行的第一件事是命中测试以找到事件目标。 命中测试使用在 rendering process 中生成的绘制记录数据来找出 event 触发的点坐标。

最小化事件分配到主线程

在上一篇文章中,我们讨论了典型的显示如何每秒刷新屏幕60次,以及如何保持节奏以实现平滑动画。 对于 input,触摸屏设备最典型的是发送 60-120次/s touch-screen 事件,而对于鼠标 100次/s。 input 事件的保真度高于我们的屏幕刷新能力。

如果以 120次/s 的速度将 touchmove 之类的连续性事件发送到主线程,它可能会触发大量的命中测试和JavaScript执行去比较需要多久去刷新我们的屏幕。

图7:事件泛滥到 Frame 时间轴上,导致页面混乱

为了最大程度地减少对主线程的过多调用,Chrome 会合并连续事件(例如 wheel,mousewheel,mousemove,pointermove,touchmove),并将分发延迟到下一个 requestAnimationFrame 之前。

使用getCoalescedEvents获取帧内事件

对于大多数Web应用程序,合并事件应该足以提供良好的用户体验。 但是,如果要构建诸如绘制应用程序之类的内容并基于 touchmove 坐标放置路径,则可能会丢失中间的坐标以绘制平滑线。 在这种情况下,可以在指针事件中使用 getCoalescedEvents 方法来获取有关那些合并事件的信息。

图9:左侧的平滑触摸手势路径,右侧的合并受限路径

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}或为什么要在script标签中编写async属性,那么我希望本系列文章能阐明为什么浏览器需要这些信息来提供更快,更流畅的信息 web 体验。

使用 Lighthouse

如果您想使代码对浏览器友好,却又不知道从哪里开始,那么 Lighthouse 是可以对任何网站进行审核的工具,并为您提供有关正确操作和需要改进的报告。通读审核列表还可以使您了解浏览器关心的是哪种类型。

了解如何衡量 performance

对于不同的站点,性能调整可能会有所不同,因此至关重要的是,您必须衡量站点的性能并确定最适合您的站点的性能。Chrome DevTools团队几乎没有有关如何衡量网站性能的教程 。

向您的网站添加 Feature Policy

如果您想采取进一步的措施,Feature Policy 是一个新的Web平台功能,可以在您构建项目时成为您的护栏。 启用 Feature Policy 可确保您的应用程序具有某些行为,并防止您犯错。 例如,如果要确保您的应用程序永远不会阻止解析,则可以在同步脚本策略上运行您的应用程序。 启用 sync-script:'none'时,将阻止执行阻止解析器的 JavaScript 。 这样可以防止您的任何代码阻止解析器,并且浏览器无需担心暂停解析器。

总结

当我开始建立网站时,我几乎只关心如何编写代码以及什么可以帮助我提高生产力。 这些事情很重要,但是我们还应该考虑浏览器如何使用我们编写的代码。 现代浏览器已经并且正在继续投资于为用户提供更好的Web体验的方式。 通过组织我们的代码对浏览器友好,从而改善了您的用户体验。 希望您加入我的行列,以求对浏览器友好!