渲染进程的内部工作

这是我们了解浏览器如何工作4篇博客的第3篇。之前,我们介绍了 多进程架构 和 导航流程。在本文中,我们将研究渲染进程内部发生了什么。

渲染器进程涉及web优化的许多方面。由于渲染进程内部发生了很多事情,因此本文只是概述。如果你想更加深入,“web基础知识的性能优化部分”有更多的资源。

渲染进程处理web内容

渲染进程负责处理tab选项卡内发生的所有事情。在一个渲染进程中,主进程处理你发送给用户的大多数代码。如果有时候你使用web worker 或者 service worker,你的部分 javascript 由woker线程处理。排版和光栅线程也在渲染进程内部运行,以便高效,流畅的渲染页面。

渲染进程的核心工作是将html、css和javascript转换为可以与用户交互的web页面。

renderer.png

解析

DOM构造

当渲染进程接收到导航提交的信息,并且开始接收html数据,主线程开始解析文本字符(html),并且将其转换为文档对象模型(DOM)。

DOM是一个浏览器对页面的内部表示,也是web开发者可以通过javascript与之交互的数据结构和API。

解析一个HTML文档到DOM是通过HTML标准来定义的。你可能注意到将HTML放到浏览器从来没有抛出过错误。例如,缺少闭合 </p> 是一个合法的HTML。错误的标记例如 Hi! I'm Chrome! (b标签在i标签之前关闭),它被看成是 Hi! I'm Chrome!。这是因为HTML规范定义了如何优雅的处理这些错误。如果你关心这些事情是如何完成的,你可以阅读HTML规范中“错误处理和解析中奇怪的例子”一节。

子资源加载

一个网站通常会使用额外的资源,例如图片、css和javascript。这些文件需要从网络或者缓存中加载。主进程可以 在解析构建DOM的时候一个接一个的请求他们,但为了加快速度,“预加载扫描”会同时运行。如果在HTML里面有一些像img和link的内容,预加载会查看HTML解析生成的token,并且在浏览器进程中发送网络请求。

dom.png

javascript会阻塞解析

当HTML解析发现一个script标签,它停止解析HTML文档,并且去加载,解析,并且执行javascript代码。为什么?因为javascript可以改变文档的形状,使用document.write()可以改变整个的DOM结构(解析模型概述在HTML定义里有一个好的绘图)。这是HTML解析为什么在恢复HTML文档解析之前等待javascript执行。如果你像深入了解javascript执行发生了什么,V8团队关于此的讨论在这。

提示浏览器如何加载资源

web开发者有很多种方式可以发送提示给浏览器按序加载资源。如果你的javascript没有使用document.write(),你可以添加 async 和 defer 属性给 <script> 标签。浏览器可以异步地加载和执行 javascript 代码,并且不会阻塞html解析。如果适合的话你可能也会使用 javascript module。<link rel="preload">是一种通知浏览器在当前导航需要尽可能早的下载资源的方式。你可以阅读更多的内容在这里 Resource Prioritization – Getting the Browser to Help You.

样式计算

只有一个DOM是不足以知道页面长什么样子的,因为我们可以在css中设置页面样式。主线程解析CSS并且为每个DOM节点准确地计算出样式。这是基于CSS选择器为每个元素应用对应样式的信息。你可以在 DevTools 中的 computed 部分看到这些信息。

computedstyle.png

即使你没有提供任何的CSS,每个DOM节点也会又一个computed样式。<h1>标签比<h2>标签展示出来大,并且为每个元素都定义了外间距。这是因为浏览器有一个默认样式表。如果你想知道chrome有哪些默认的css,请查看这个chrome默认css

布局

现在渲染进程知道文档的结构和每个节点的样式,但是这些还不足以渲染一个页面。想象一下你在电话里面为你的朋友描述一副画。“这里有一个大红色的圆和一个小的蓝色正方形”这些信息不能够让你的朋友准确的知道画到底是什么样子。

tellgame.png

布局是一个寻找元素坐标的过程。主线程遍历DOM和计算样式,创建包含x,y坐标和边界框大小的布局树。布局树和DOM树有着相似的结构,但是它仅仅包含页面上可见元素的关联信息。如果应用了 display:none,那么这个元素就不是布局树的一部分(然而,一个visibility:hidden的元素在布局树中)。类似地,一个包含内容的伪类就像p::before{content: "Hi!"}被应用,它会包含在布局树中,即使它不在DOM中。

layout.png
确定页面布局是一个有挑战的任务。即使最简单的页面,从上到下块布局,也必须去考虑字体多大,哪里需要换行,因为这些都会影响段落的大小和形状;而且也会影响下一行的段落的内容。

CSS可以使元素浮动到一边,屏蔽溢出项,改变书写的方向。你可以想象,这个布局阶段有一个艰巨的任务。在Chrome中,一个工程师团队都在为布局工作。如果你想了解更多他们的工作细节,请点击 演讲视频 观看有趣的记录。

绘制

有了DOM,样式,布局还是不足以渲染一个页面。假设你想模仿一幅画。你知道大小,形状,元素的位置,但是你仍然需要判断绘制的顺序。

drawgame.png

例如,会为一些元素设置z-index,在下面的例子中,按照HTML的顺序绘制将会出现错误的渲染结果。

zindex.png
图8:页面元素按照HTML标记的顺序出现,由于未考虑 z-index导致生成错误的渲染图

在当前绘制步骤,主线程遍历布局树去生成绘制记录。绘制记录是对绘制过程的记录,例如“先背景,后文字,最后矩形”。如果你使用 javascript 在<canvas>元素上绘制,这个过程对你就比较熟悉。
paint.png
图9:主线程遍历布局树并且生成绘制记录

更新渲染流非常昂贵

在渲染流中要掌握的最重要的一点是,在每一步中,使用上一步操作的结果去生成新数据。例如,如果布局树中的某些发生改变,需要为受影响的文档部分重新生成绘制顺序。

如果为某些元素设置动画,浏览器必须在每个frame中间去运行这些操作。我们大多数的显示器都是每秒钟刷新60次屏幕;当你在每一帧中在屏幕上移动物体时,动画对于人眼的显示都是平滑的。然而,如果错过了它们之间的帧,则页面将显示为“混乱”。
pagejank1.png
图11:时间轴上的动画帧

即使渲染操作域屏幕刷新保持一致,这些计算仍然运行在主线程上,这也意味着当你的应用运行javascript时会阻塞。
pagejank2.png
图12:时间轴上动画帧,但一个帧被javascript阻止

你可以将Javascript操作分成小块,并使用requestAnimationFrame()安排在每个帧上运行。对于这个话题想要了解更多,你可以看 js优化执行。你也可以让你js运行在web woker上去避免阻塞主线程。
raf.png
图13:在带有动画帧的时间轴上运行javascript小块

合成

你如何去画一个页面?

现在浏览器知道了文档的结构,每个元素的样式,在页面里面的坐标和绘制顺序,如何绘制一个页面?将这些信息转化为屏幕上的像素叫作光栅化(rasterizing)。

处理此问题的一个幼稚的方法就是在视口内部栅格化。如果用户滚动页面,则移动栅格化框架,并通过栅格化更多内容去填充缺失的部分。这是Chrome第一次发布时处理栅格化的方式。然而,现代浏览器运行着更为复杂的过程,叫作合成。

什么是合成?

合成是将一个页面的各个部分分成多个层,分别对其栅格化的技术,在一个叫作合成线程的独立线程中合称为一个页面。如果发生滚动,由于图层已经被栅格化,所以就不得不合成一个新的帧。可以使用相同的方式,通过移动图层生成一个新的帧来实现动画。

你可以在开发者工具的“Layers panel”中查看你的网站如何被划分为多个图层。

划分为图层

为了找出哪些元素应该在哪些图层,主线程遍历布局树为了生成图层树(在开发者工具中这部分在performance面板中被叫作“更新图层树”)。如果页面的某些部分应该是独立的层(例如滑入式侧边栏)没有生成,你可以在css中使用 will-change 属性去提示浏览器。
layer.png
你可能试图为每个元素提供图层,但是与页面的每帧进行栅格化相比,过多数量的图层合成会使得操作速度变慢,因此衡量应用程序的渲染性能特别重要。想了解更多这个话题,可以查看 Stick to Compositor-Only Properties and Manage Layer Count

栅格化和合成脱离主线程

一旦图层树生成,并且绘制顺序确定,主线程会提交这些信息给合成线程。合成器线程会栅格化每个图层。一个图层可以和整个页面的长度一样大,因此合成器会讲他们划分为图块,并且将每个图块发送给栅格线程。栅格线程栅格化每个图块,并且将他们存储在GPU中。

raster.png
合成器线程会优先处理不同的栅格线程,因此可以首先对视口栅格化。一个图层还具有不同分辨率的平铺,用于处理放大缩小的操作。

一旦图块被栅格化,合成器线程将手机信息叫作“draw quads”生成一个“compositor frame”。

draw quads包含例如图块在内存中位置以及考虑页面合成的情况下在页面中合成图块的信息。
compositor framedraw quads集合去展示一个页面的框架

然后合成器框架通过IPC提交信息给浏览器进程。此时,可以从更改浏览器UI的UI线程或者另外一个用于扩展的渲染进程添加另一个合成器框架。这些合成框架都被发送到GPU用于显示到屏幕上。如果滚动事件发生,合成器进程创建另一个合成器框架发送给GPU。

composit.png

合成的好处是不涉及主线程就可以完成。合成器线程不需要等到样式计算和Js执行。这是为什么合成动画被认为是最佳的平滑优化的原因。如果布局和绘制需要重新计算,则必须涉及主线程。

总结

在这篇文章中,我们深入了解了从解析到合成的渲染流。希望你现在可以获取到更多的关于网站优化的知识。

在下面也是本系列最后一篇文章中,我们将更加深入了解合成器线程,并且知道当用户输入例如mouse move和click的时候发生了什么。


silence
386 声望26 粉丝

我就是我,是不想绽放的烟火!