窥探现代浏览器架构(四)

前言

本文是笔者对Mario Kosaka写的inside look at modern web browser系列文章的翻译。这里的翻译不是指直译,而是结合我的的理解将做者想表达的意思表达出来,并且会尽可能补充一些相关的内容来帮助你们更好地理解。javascript

到达合成线程的输入

这篇文章是探究Chrome内部工做原理的四集系列文章中的最后一篇了。在上一篇文章中,咱们探讨了一下浏览器渲染页面的过程以及学习了一些关于合成线程的知识,在本篇文章中,咱们要看一下当用户在网页上输入内容的时候,合成线程(compositor)作了些什么来保证流畅的用户体验的。java

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

当你听到“输入事件”(input events)的时候,你可能只会想到用户在文本框中输入内容或者对页面进行了点击操做,但是从浏览器的角度来看的话,输入其实表明着来自于用户的任何手势动做(gesture)。因此用户滚动页面触碰屏幕以及移动鼠标等操做均可以看做来自于用户的输入事件。git

当用户作了一些诸如触碰屏幕的手势动做时,浏览器进程(browser process)是第一个能够接收到这个事件的地方。但是浏览器进程只能知道用户的手势动做发生在什么地方而不知道如何处理,这是由于标签内(tab)的内容是由页面的渲染进程(render process)负责的。所以浏览器进程会将事件的类型(如touchstart)以及坐标(coordinates)发送给渲染进程。为了能够正确地处理这个事件,渲染进程会找到事件的目标对象(target)而后运行这个事件绑定的监听函数(listener)。github


<p align="center">点击事件从浏览器进程路由到渲染进程</p>web

合成线程接收到输入事件

在上一篇文章中,咱们查看了合成线程是如何经过合并页面已经光栅化好的层来保障流畅滚动体验(scroll smoothly)的。若是当前页面不存在任何用户事件的监听器(event listener),合成线程彻底不须要主线程的参与就能建立一个新的合成帧来响应事件。但是若是页面有一些事件监听器(event listeners)呢?合成线程是如何判断出这个事件是否须要路由给主线程处理的呢?chrome

了解非快速滚动区域 - non-fast scrollable region

由于页面的JavaScript脚本是在主线程(main thread)中运行的,因此当一个页面被合成的时候,合成线程会将页面那些注册了事件监听器的区域标记为“非快速滚动区域”(Non-fast Scrollable Region)。因为知道了这些信息,当用户事件发生在这些区域时,合成线程会将输入事件发送给主线程来处理。若是输入事件不是发生在非快速滚动区域,合成线程就无须主线程的参与来合成一个新的帧。segmentfault


<p align="center">非快速滚动区域有用户事件发生时的示意图</p>浏览器

当你写事件监听器的时候留点心眼

Web开发的一个常见的模式是事件委托(event delegation)。因为事件会冒泡,你能够给顶层的元素绑定一个事件监听函数来做为其全部子元素的事件委托者,这样子节点的事件就能够统一被顶层的元素处理了。所以你可能看过或者写过相似于下面的代码:架构

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

只用一个事件监听器就能够服务到全部的元素,乍一看这种写法仍是挺实惠的。但是,若是你从浏览器的角度去看一下这段代码,你会发现上面给body元素绑定了事件监听器后实际上是将整个页面都标记为一个非快速滚动区域,这就意味着即便你页面的某些区域压根就不在意是否是有用户输入,当用户输入事件发生时,合成线程每次都会告知主线程而且会等待主线程处理完它才干活。所以这种状况下合成线程就丧失提供流畅用户体验的能力了(smooth scrolling ability)。async


<p align="center">当整个页面都是非快速滚动区域时页面的事件处理示意图</p>

为了减轻这种状况的发生,您能够为事件监听器传递passive:true选项。 这个选项会告诉浏览器您仍要在主线程中侦听事件,但是合成线程也能够继续合成新的帧。

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

查找事件的目标对象(event target)

当合成线程向主线程发送输入事件时,主线程要作的第一件事是经过命中测试(hit test)去找到事件的目标对象(target)。具体的命中测试流程是遍历在渲染流水线中生成的绘画记录(paint records)来找到输入事件出现的x, y坐标上面描绘的对象是哪一个。


<p align="center">主线程经过遍历绘画记录来肯定在x,y坐标上的是哪一个对象</p>

最小化发送给主线程的事件数

上一篇文章中咱们有说过显示器的刷新频率一般是一秒钟60次以及咱们能够经过让JavaScript代码的执行频率和屏幕刷新频率保持一致来实现页面的平滑动画效果(smooth animation)。对于用户输入来讲,触摸屏通常一秒钟会触发60到120次点击事件,而鼠标通常则会每秒触发100次事件,所以输入事件的触发频率其实远远高于咱们屏幕的刷新频率。

若是每秒将诸如touchmove这种连续被触发的事件发送到主线程120次,由于屏幕的刷新速度相对来讲比较慢,它可能会触发过量的点击测试以及JavaScript代码的执行。


<p align="center">事件淹没了屏幕刷新的时间轴,致使页面很卡顿</p>

为了最大程度地减小对主线程的过多调用,Chrome会合并连续事件(例如wheelmousewheelmousemovepointermovetouchmove),并将调度延迟到下一个requestAnimationFrame以前。


<p align="center">和以前相同的事件轴,但是此次事件被合并并延迟调度了</p>

任何诸如keydownkeyupmouseupmousedowntouchstarttouchend等相对不怎么频繁发生的事件都会被当即派送给主线程。

使用getCoalesecedEvents来获取帧内(intra-frame)事件

对于大多数web应用来讲,合并事件应该已经足够用来提供很好的用户体验了,然而,若是你正在构建的是一个根据用户的touchmove坐标来进行绘图的应用的话,合并事件可能会使页面画的线不够顺畅和连续。在这种状况下,你可使用鼠标事件的getCoalescedEvents来获取被合成的事件的详细信息。


<p align="center">左边是顺畅的触摸手势,右边是事件合成后不那么连续的手势</p>

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.
    }
});

下一步

这本系列的文章中,咱们以Chrome浏览器为例子探讨了浏览器的内部工做原理。若是你以前历来没有想过为何DevTools推荐你在事件监听器中使用passive:true选项或者在script标签中写async属性的话,我但愿这个系列的文章能够给你一些关于浏览器为何须要这些信息来提供更快更流畅的用户体验的缘由。

学习如何衡量性能

不一样网站的性能调整可能会有所不一样,你要本身衡量本身网站的性能并肯定最适合提高你的网站性能的方案。 你能够查看Chrome DevTools团队的一些教程来学习如何才能衡量本身网站的性能

为你的站点添加Feature Policy

若是你想更进一步,你能够了解一下Feature Policy这个新的Web平台功能,这个功能能够在你构建项目的时候提供一些保护让您的应用程序具备某些行为并防止你犯下错误。例如,若是你想确保你的应用代码不会阻塞页面的解析(parsing),你能够在同步脚本策略(synchronius scripts policy)中运行你的应用。具体作法是将sync-script设置为'none',这样那些会阻塞页面解析的JavaScript代码会被禁止执行。这样作的好处是避免你的代码阻塞页面的解析,并且浏览器无须担忧解析器(parser)暂停。

总结

以上就是全部和浏览器架构和运行原理相关的内容了,咱们之后在开发web应用的时候,不该该只考虑代码的优雅性,还要多多从浏览器是如何解析运行咱们的代码的方面进行思考,从而为用户提供更好的用户体验。

持续关注个人技术动态

我是进击的大葱,关注我和我一块儿进步成独当一面的全栈工程师!

文章首发于:窥探现代浏览器架构(四)

关注个人我的公众号获取个人最新技术推送!

相关文章
相关标签/搜索