无心间在 Google Developer 上看到的文章,这是这个系列博客的 第三部分,主要是研究渲染进程所作的事情。渲染进程涉及到 Web 性能的不少方面,这里只是概述,若是你想深刻了解,能够去 Web 基础的性能部分看看。
渲染进程负责处理标页内的全部事情。其中,主线程负责处理大部分代码。少部分的代码可能会由工做线程处理(好比 Service Worker 或者 Web Worker)。同时,合成器线程和栅格线程也在渲染进程中运行,负责高效、流畅的呈现页面。javascript
渲染进程接收到导航提交的消息后,就开始接收 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
若是解析器碰到了 <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,每一个 DOM 节点都会有样式。好比 <h1>
显示出来比 <h2>
大,而且每一个元素都有边距。这是由于浏览器具备默认样式,若是你想知道 Chrome 默认的样式,能够到这里看源代码。缓存
如今渲染进程知道了文档的结构和每一个节点的样式,但仍是不足以渲染页面。想象一下,你给你朋友打电话描述一幅画:“画里有一个大红圈和一个蓝色小方块。”,你的朋友听了你的描述,可能仍是一脸懵逼。网络
布局就是计算出元素之间的几何位置的过程。主线程会遍历 DOM 树和样式,而后构造出一颗布局树,这棵树上的节点都带有 x、y 坐标和边界框大小之类的信息。布局树和 DOM 树的结构相似,可是树上只包含页面可见元素的信息。若是元素被设置了 display: none
,那么布局树就不会包含这个元素(visibility: hidden
的元素会被包含)。一样的,若是一个内容是经过伪类(好比 p::before { content: 'Hi!' }
)添加进来的,那么这个元素会被包含在布局树中,可是 DOM 树中没有。
肯定页面如何布局是一项很是难的事情。即便是最简单的布局方式也要考虑字体大小、换行之类的事情,更别说浮动、隐藏溢出、修改文本显示方向等等事情了。在 Chrome 里,有一个专门负责布局的团队,感兴趣的话,能够看看这个分享。
有了 DOM 结构、样式、布局以后,咱们仍是不能渲染页面,咱们还要解决渲染的顺序问题。好比,有些元素可能设置了 z-index
属性,那么按照 HTML 里面的元素顺序进行渲染就会出错。
因此在这一步,主线程会遍历布局树,并建立绘制记录。绘制记录会记录绘制过程,就像是先画背景,再画文本,最后画矩形。若是你用过 canvas
,那么你可能对这个过程会很熟悉。
渲染的过程是一个流水线,每一个步骤的结果都用于下一个步骤。若是布局树变化了,那么就须要从新为受影响的部分生成绘制记录。
若是要给元素设置动画,浏览器就要在每一帧运行这些操做。大多数的显示器屏幕每秒刷新 60 次(60 fps),当每一帧都在变化的时候,人就会以为动画很流畅,可是,若是中间丢了一些帧就会显得很卡顿。
即使渲染能跟得上屏幕刷新,但动画是在主线程上进行计算,也就是说若是主线程一旦由于执行 JavaScript 代码而被阻塞了,动画也就被卡住了。
你能够将动画涉及的 JavaScript 操做分红小块,并使用 requestAnimationFrame()
调度在每一帧上执行,更多请参考。你也能够在 Web Worker 中运行 JavaScript以免阻塞主线程。
如今浏览器知道了文档结构、元素的样式、页面的几何关系以及绘制顺序,接下来就该渲染页面了。具体该怎么渲染呢?把上述信息转换成屏幕上的像素叫作栅格化。
最简单的处理方式就是把页面在当前视窗中的部分先转换成像素。若是用户滚动页面,则移动栅格化的画框,填补没有渲染的部分。Chrome 最先就是这么干的,但现代浏览器有更复杂的流程,叫作合成。
合成是将页面的各个部分进行分层,而后分别对其进行栅格化,而后经过单独的线程进行合成的技术。这样的话,当用户滚动页面的时候,由于图层都被栅格化了,因此浏览器只须要合成一个新的帧便可。动画也能够经过移动图层再合成新的帧来实现。
你能够在 DevTools 里经过 Layers 面板查看网站的分层(能够在开发者工具里找到)。
为了找出哪些元素在那个图层,主线程会遍历布局树来建立图层树。若是页面的某些部分是单独的图层(好比滑入式侧边菜单)可是没有拆分出来,你能够用 CSS 里的 will-change
属性来提示浏览器进行拆分。
分层并非越多越好,层过多可能会形成操做速度变慢,甚至还不如每帧都对页面中的小部分执行一次栅格化快,至于该怎么平衡,能够参考这里。
一旦建立了图层树,并肯定了绘制的顺序,主线程就会将信息提交给合成线程。紧接着,合成线程会栅格化每一个图层。有的状况下一个图层可能和页面同样长,所以合成线程会将它们划分红图块后发送给栅格线程。栅格线程栅格化每一个图块(图块转化为位图),并将它们存到显存中。
合成线程会根据栅格线程不一样的优先级处理图块,好比它会优先处理视窗(及附近)的图块。而且图块还具备不一样分辨率的图块,以便在用户放大、缩放时使用。
全部的图块都栅格化后,合成线程会收集这些图块的信息(绘制图块)来建立合成帧。
建立好的合成帧会经过 IPC 提交给浏览器进程。此时,能够从 UI 线程或者其余插件的渲染进程添加另外一个合成帧。这些合成帧会被发送到 GPU 进行,最终展现到屏幕上。若是发生了滚动,合成线程会建立另外一个合成帧发送给 GPU。
合成的好处就是和主线程无关。合成线程不须要等待样式计算或者 JavaScript 的执行,这也是为何只须要合成的动画流畅平滑的缘由。若是须要再次计算布局或者绘制,就须要涉及到主线程了(这就是为何要减小重排和重绘)。
后续还会有接下来的最后一篇 - 交互,公众号里有上两篇的内容,欢迎关注、转发、分享支持我。