深刻了解现代浏览器之三 - 渲染

无心间在 Google Developer 上看到的文章,这是这个系列博客的 第三部分,主要是研究渲染进程所作的事情。渲染进程涉及到 Web 性能的不少方面,这里只是概述,若是你想深刻了解,能够去 Web 基础的性能部分看看。

渲染进程处理网页内容

渲染进程负责处理标页内的全部事情。其中,主线程负责处理大部分代码。少部分的代码可能会由工做线程处理(好比 Service Worker 或者 Web Worker)。同时,合成器线程和栅格线程也在渲染进程中运行,负责高效、流畅的呈现页面。javascript

渲染进程具备主线程、工做线程、合成器线程和栅格线程

解析数据

构造 DOM 树

渲染进程接收到导航提交的消息后,就开始接收 HTML 数据,主线程就开始解析文本字符串(HTML),并将其转换成 DOM(Document Object Model)。css

DOM 是页面在浏览器内部的结构,也是开发人员经过 JavaScript 与之交互的数据结构和 API。html

解析 HTML 的规则由 HTML 标准定义。同时 HTML 标准要求兼容错误的写法,若是你对这个感兴趣,能够查看 An introduction to error handling and strange cases in the parser 的 HTML 部分。html5

子资源加载

一个网站常常会使用一些外部资源,好比 CSS、图片以及 JavaScript 等。这些文件都须要从网络获取或者是从缓存中加载。主线程在解析构建 DOM 时,会发现一个加载一个,可是这样太慢,因而为了加快速度,“预加载扫描器”会同时运行。当在文档中发现有像 <img> 或者 <link> 的内容时,预加载扫描器会将请求提交给浏览器进程中的网络线程。java

主线程解析 HTML 并构建 DOM 树

JavaScript 可能阻塞解析

若是解析器碰到了 <script> 标签,就会暂停解析 HTML 文档,而后开始解析和执行 JavaScript 代码。为何呢?由于 JavaScript 可能会经过 document.write() 这样的代码修改文档,从而改变 DOM 结构(HTML 标准里有张解析模型的图很是好)。因此 HTML 解析器就必需要停下来执行 JavaScript,而后再继续解析 HTML。若是你对 JavaScript 执行的细节感兴趣,能够看看 V8 团队的分享web

提示浏览器如何加载资源

Web 开发者能够经过多种方式提示浏览器。若是你的 JavaScript 代码不使用 document.write(),就能够在 <script> 标签上添加 async 或者 defer 属性,这样浏览器就会异步加载运行 JavaScript 代码,而不会阻塞解析。若是能够的话,也可使用 JavaScript 模块。可使用 <link rel="preload"> 告诉浏览器当前导航确定须要该资源,但愿尽快下载。有关信息请参阅资源优先级canvas

样式计算

光一个 DOM 结构,咱们仍是不知道页面长啥样子,咱们还须要 CSS 来设置页面元素的样式。因此主线程会解析 CSS 来计算每一个 DOM 节点长什么样子。基于 CSS 选择器,对每一个元素应用相应的样式,这些均可以在 DevTools 中的 computed 中看到。浏览器

主线程解析 CSS 并计算样式

即使你不提供任何 CSS,每一个 DOM 节点都会有样式。好比 <h1> 显示出来比 <h2> 大,而且每一个元素都有边距。这是由于浏览器具备默认样式,若是你想知道 Chrome 默认的样式,能够到这里看源代码缓存

布局

如今渲染进程知道了文档的结构和每一个节点的样式,但仍是不足以渲染页面。想象一下,你给你朋友打电话描述一幅画:“画里有一个大红圈和一个蓝色小方块。”,你的朋友听了你的描述,可能仍是一脸懵逼。网络

布局就是计算出元素之间的几何位置的过程。主线程会遍历 DOM 树和样式,而后构造出一颗布局树,这棵树上的节点都带有 x、y 坐标和边界框大小之类的信息。布局树和 DOM 树的结构相似,可是树上只包含页面可见元素的信息。若是元素被设置了 display: none,那么布局树就不会包含这个元素(visibility: hidden 的元素会被包含)。一样的,若是一个内容是经过伪类(好比 p::before { content: 'Hi!' })添加进来的,那么这个元素会被包含在布局树中,可是 DOM 树中没有。

主线程遍历具备样式的 DOM 树生成布局树

因为换行而更改的布局

肯定页面如何布局是一项很是难的事情。即便是最简单的布局方式也要考虑字体大小、换行之类的事情,更别说浮动、隐藏溢出、修改文本显示方向等等事情了。在 Chrome 里,有一个专门负责布局的团队,感兴趣的话,能够看看这个分享

绘制

先画圆仍是先画方?

有了 DOM 结构、样式、布局以后,咱们仍是不能渲染页面,咱们还要解决渲染的顺序问题。好比,有些元素可能设置了 z-index 属性,那么按照 HTML 里面的元素顺序进行渲染就会出错。

没考虑 z-index 而致使渲染错误

因此在这一步,主线程会遍历布局树,并建立绘制记录。绘制记录会记录绘制过程,就像是先画背景,再画文本,最后画矩形。若是你用过 canvas,那么你可能对这个过程会很熟悉。

主线程遍历布局树生成绘制记录

更新渲染管道的成本很高

渲染的过程是一个流水线,每一个步骤的结果都用于下一个步骤。若是布局树变化了,那么就须要从新为受影响的部分生成绘制记录。

DOM 树、布局树、绘制记录的生成顺序

若是要给元素设置动画,浏览器就要在每一帧运行这些操做。大多数的显示器屏幕每秒刷新 60 次(60 fps),当每一帧都在变化的时候,人就会以为动画很流畅,可是,若是中间丢了一些帧就会显得很卡顿。

时间轴上的动画帧

即使渲染能跟得上屏幕刷新,但动画是在主线程上进行计算,也就是说若是主线程一旦由于执行 JavaScript 代码而被阻塞了,动画也就被卡住了。

动画被 JavaScript 阻塞了

你能够将动画涉及的 JavaScript 操做分红小块,并使用 requestAnimationFrame() 调度在每一帧上执行,更多请参考。你也能够在 Web Worker 中运行 JavaScript以免阻塞主线程。

合成

如何绘制页面?

简单栅格化的处理动画

如今浏览器知道了文档结构、元素的样式、页面的几何关系以及绘制顺序,接下来就该渲染页面了。具体该怎么渲染呢?把上述信息转换成屏幕上的像素叫作栅格化。

最简单的处理方式就是把页面在当前视窗中的部分先转换成像素。若是用户滚动页面,则移动栅格化的画框,填补没有渲染的部分。Chrome 最先就是这么干的,但现代浏览器有更复杂的流程,叫作合成。

合成过程的动画

合成是将页面的各个部分进行分层,而后分别对其进行栅格化,而后经过单独的线程进行合成的技术。这样的话,当用户滚动页面的时候,由于图层都被栅格化了,因此浏览器只须要合成一个新的帧便可。动画也能够经过移动图层再合成新的帧来实现。

你能够在 DevTools 里经过 Layers 面板查看网站的分层(能够在开发者工具里找到)。

分层

为了找出哪些元素在那个图层,主线程会遍历布局树来建立图层树。若是页面的某些部分是单独的图层(好比滑入式侧边菜单)可是没有拆分出来,你能够用 CSS 里的 will-change 属性来提示浏览器进行拆分。

主线程遍历布局树生成图层树

分层并非越多越好,层过多可能会形成操做速度变慢,甚至还不如每帧都对页面中的小部分执行一次栅格化快,至于该怎么平衡,能够参考这里

主线程的栅格化和合成

一旦建立了图层树,并肯定了绘制的顺序,主线程就会将信息提交给合成线程。紧接着,合成线程会栅格化每一个图层。有的状况下一个图层可能和页面同样长,所以合成线程会将它们划分红图块后发送给栅格线程。栅格线程栅格化每一个图块(图块转化为位图),并将它们存到显存中。

栅格线程建立图块的位图发送到 GPU

合成线程会根据栅格线程不一样的优先级处理图块,好比它会优先处理视窗(及附近)的图块。而且图块还具备不一样分辨率的图块,以便在用户放大、缩放时使用。

全部的图块都栅格化后,合成线程会收集这些图块的信息(绘制图块)来建立合成帧。

  • 绘制图块:包含图块在内存中的地址、页面中的位置等相关信息
  • 合成帧:多个绘制图块的集合,绘成了页面的一帧

建立好的合成帧会经过 IPC 提交给浏览器进程。此时,能够从 UI 线程或者其余插件的渲染进程添加另外一个合成帧。这些合成帧会被发送到 GPU 进行,最终展现到屏幕上。若是发生了滚动,合成线程会建立另外一个合成帧发送给 GPU。

合成线程将合成帧发送到浏览器进程,而后发送到 GPU

合成的好处就是和主线程无关。合成线程不须要等待样式计算或者 JavaScript 的执行,这也是为何只须要合成的动画流畅平滑的缘由。若是须要再次计算布局或者绘制,就须要涉及到主线程了(这就是为何要减小重排和重绘)。

后续还会有接下来的最后一篇 - 交互,公众号里有上两篇的内容,欢迎关注、转发、分享支持我。

公众号

相关文章
相关标签/搜索