窥探现代浏览器架构-四

窥探现代浏览器架构-四

前言

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

到达合成线程的输入

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

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

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

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

点击事件从浏览器进程路由到渲染进程

合成线程接收到输入事件

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

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

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

非快速滚动区域有用户事件发生时的示意图

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

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

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

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

当整个页面都是非快速滚动区域时页面的事件处理示意图

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

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

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

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

主线程通过遍历绘画记录来确定在x,y坐标上的是哪个对象

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

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

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

事件淹没了屏幕刷新的时间轴,导致页面很卡顿

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

和之前相同的事件轴,可是这次事件被合并并延迟调度了

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

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

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

左边是顺畅的触摸手势,右边是事件合成后不那么连续的手势

1
2
3
4
5
6
7
8
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应用的时候,不应该只考虑代码的优雅性,还要多多从浏览器是如何解析运行我们的代码的方面进行思考,从而为用户提供更好的用户体验。

个人技术动态

欢迎关注公众号进击的大葱一起学习成长

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×