应该有很多前端开发人员都思考过这么一个问题:从输出 URL 到页面加载实现,两头都做产生了什么?
这个问题波及的面十分广,每个波及的点又很深刻。从触屏 / 键盘如何到 CPU?CPU 如何到零碎内核?如何从操作系统 GUI 到浏览器?浏览器如何向网卡发送数据?数据如何从本机网卡发送到服务器?服务器接收数据后如何解决?服务器返回数据后浏览器如何解决?浏览器如何将页面展示进去?等等等等,每一个过程都蕴含了大量且深刻的常识体系,很难一以贯通。
但作为前端开发人员,浏览器是咱们的次要工具之一,浏览器是如何将页面展示进去的则是咱们更关注的局部。因而本文就从一些根本流程来简要形容这个过程。
从下面这个图中能够发现,尽管应用的 Javascript 是单线程语言,但浏览器自身是多过程的。
然而这并不是从一而终的状态,而是浏览器从晚期的单过程构造逐步倒退倒退而来。古代浏览器各过程依据负责的性能不同,分为浏览器过程、渲染器过程、网络过程、GPU 过程、缓存过程、插件过程等等。为了更好的了解浏览器页面的出现过程,咱们以最支流的 Chrome 为例,简要的阐明一下各个过程的大抵职能:
- 浏览器过程: 负责管制界面展现、用户交互、子过程治理等性能。
- 渲染器过程: 负责将 HTML\CSS\JS 转化为用户能够与之交互的网页。渲染引擎如 webkit、blink 和 JS 引擎 V8 都是在该过程之中。
- GPU 过程: GPU 过程本来是为了实现 3D CSS 成果,然而随后页面、Chrome 的 UI 都采纳 GPU 来绘制,是 GPU 成为了重要需要,于是减少了 GPU 过程。
- 网络过程:负责页面的网络资源加载。
- 插件过程:负责插件的运行,因为插件可能解体,须要插件过程其余过程隔离。留神,插件并不是咱们罕用的浏览器拓展,plugin 和 extension 是不同的。
- 缓存过程:负责解决页面资源缓存和清理。
咱们本次须要重点关注的是 渲染器过程。
回到问题,当咱们在浏览器地址栏输出地址时,浏览器过程的 UI 线程会捕获输出内容,如果拜访的是网址,那么 UI 线程会启动一个网络线程来构建申请(这里咱们临时不思考缓存,缓存又是另外一个故事了),它申请 DNS 进行域名解析而后连贯服务器获取数据。如果咱们输出的是关键词,浏览器则应用默认配置的搜索引擎来搜寻。在获取到数据并通过平安校验后,网络线程会告诉 UI 线程数据筹备结束,而后 UI 线程创立一个渲染器过程来进行页面的渲染,并将数据通过 IPC 管道传递给渲染器过程。
至此,咱们的配角 渲染器过程 退场!
解析 HTML
渲染器过程接管到的是一个 HTML,须要把 HTML 解析成 DOM 数据结构。因为间接的 HTML 字节流是无奈被渲染引擎所了解的,必须转化成能够了解的内部结构。这个内部结构就是 DOM,DOM 提供了对 HTML 文档的结构化表述。在渲染引擎中,DOM 有三个层面的作用:
- 从页面角度:DOM 是生成页面的根底数据结构。
- 从 js 角度:DOM 提供了 js 操作的接口。通过这套接口,js 能够对 DOM 接口进行拜访,从而使开发者领有扭转文档构造、款式、内容的能力。
- 从平安角度:DOM 是 HTML 通过解析的外部数据结构,它将 web 页面和 js 链接起来,并过滤了一些不平安的内容。
渲染器过程外部应用 HTML Parser 将 HTML 解析成 DOM 构造。须要留神的是,HTML 解析器不会期待整个 HTML 文档加载结束再去解析,而是加载多少了多少 HTML,就解析多少。
那么 HTML 字节流是如何转换成 DOM 的呢?
其实和 V8 解析 js 相似,也是做词法剖析,通过分词器将字节流胜利成一个个 token,包含 Tag token 和文本 token。HTML 解析器保护了一个 token 栈构造,token 会依照对应程序入栈出栈,而后将 token 解析成 DOM 节点,并将 DOM 节点增加进 DOM 树中。
后面提到生成 DOM 能够过滤一些不平安内容。这次要是渲染引擎中的一个名为 XSSAuditor 安全检查模块实现的。它会监测词法平安,在分词器解析出 token 之后,查看这些模块是否援用了内部脚本,是否合乎 CSP 标准,是否存在跨站点申请等。如果呈现不符合规范的内容。XSSAuditor 会对该脚本或下载工作进行拦挡。
DOM 树在构建过程中会创立 document 对象,而后以 document 为根节点的 DOM 树一直批改向其中增加新的元素。
解析 CSS
后面曾经将 HTML 解析成 DOM 树了,然而光领有 DOM 树还不足以让咱们晓得页面的样貌。因为咱们必定会为页面设置一些款式。因而主过程还会解析页面中的 CSS 从而确定每个 DOM 节点的计算款式(computed style)。
CSS 的款式起源次要有三个:
- 通过 link 援用的内部 CSS 文件
- 应用 <style> 标签内的 CSS
- 元素的 style 属性内嵌的 CSS
同样,浏览器无奈间接了解这些纯文本的 CSS 款式。所以渲染引擎在承受到 CSS 文本时,会通过 CSS parser 执行解析转换操作。解析过程和 HTML 是局部相似的。最终将 CSS 文本转换成浏览器能够了解的构造 styleSheets,这个构造具备查问和批改的能力,为后续的款式操作提供根底。
而后将 styleSheet 中的属性值进行标准化操作,比方咱们在写款式时常罕用到 font-size:1em、color:bule、font-weight:bold 等转换成规范的计算值。
最初依据层叠款式的继承规定和层叠规定,计算出的每个 DOM 节点的款式,被保留在 ComputedStyle 构造内。
渲染树 Render Tree VS 布局树 LayoutTree
到目前为止,咱们曾经在渲染器过程的主线程中走完了前两步。咱们曾经有了节点,又晓得了节点的款式,是不是就能够开始渲染了 ?
不,进度条通知咱们事件远没有那么简略。
然而在进行下一步之前,咱们还须要厘清些概念。这其中 Layout Tree 咱们是常听的,那 Render Tree 又是啥?它和 Layout Tree 一样吗?
Layout Tree 不等于 Render Tree。
从这篇开发者文档 [https://developers.google.com…] 中的配图能够看到,Render tree 是将 dom 和 cssom 联合的产物。也就是主线程解析 CSS 并把计算后的款式增加到 dom 节点上,进而失去了一个渲染树。
The main thread parses CSS and determines the computed style for each DOM node. This is information about what kind of style is applied to each element based on CSS selectors.
———《Inside look at modern web browser(part 3)》
如图所示,咱们只是晓得了节点是否可见和它们的可见款式,然而还不晓得节点的准确地位和大小。也就是须要进行布局。
主线程从 render tree 的根节点开始遍历,依照肯定规定解决后,将失去一个盒模型。它会准确的捕捉每个元素在视口内的确切地位和尺寸,所有的绝对测量值都会转换为屏幕上的相对元素。在得悉了那些节点可见,计算款式和几何信息后,渲染引擎就能够把 render tree 上的每个节点都转换成屏幕上的像素,这一步称为 绘制 或者 栅格化
也就是说,Layout Tree 是 Render Tree 在进行布局计算后的后果,在 Render tree 的根底上,减少了节点的几何信息。
The main thread going over DOM tree with computed styles and producing layout tree ———《Inside look at modern web browser(part 3)》
图层树 Layer tree
真好,咱们又走完一步,当初咱们有了节点还有节点的准确地位和款式,可不可以渲染了?
道歉,还是不行。
这里咱们要先理解一个概念,栅格化或者说光栅化(Restering)。简略来说栅格化就是将这些节点信息转化为屏幕上的像素点。
那么栅格化跟咱们渲染有什么关系呢?因为浏览器应用的正是这个技术将元素绘制在屏幕上。
Chrome 以前是在可视区域内将元素栅格化,随着用户滚动页面,一直调整栅格化的区域,持续栅格化并将内容填充到缺失局部效的形式。这样的问题是用户疾速滚动页面的时候,会呈现卡顿感。
当初的 chrome 栅格化是采纳一种合成(composting)的技术,把页面中某些局部分到一些层中,别离栅格化它们,而后在栅格化线程中合成。这样在页面滚动时,原材料曾经有了(曾经栅格化好的那些层),只须要将视口内的层合成为一个新的帧就好了。
那这又跟 Layer Tree 又有啥关系呢?
后面说过目前 Chrome 应用的是将多个图层合成为一帧的技术。Layer Tree 的作用就是,分层。
为了找到那些元素应该在哪些层里,主线程遍历 layout tree 来创立 layer tree(Chrome devtools 里称为 ‘update layer tree’)
渲染引擎并不会为每个节点创立一个图层,如果一个节点没有图层,那么它属于父节点的图层。想要创立新图层,节点须要满足肯定条件。
- 领有层叠上下文属性的元素会被提成为新的一层
页面是二维立体,然而层叠上下文会让 HTML 元素具备三维概念。这些元素依照本身属性的优先级散布在垂直于这个二维立体的 Z 轴上。
明确定位属性的元素、定义通明属性的元素、应用 CSS 滤镜的元素等等,都领有层叠上下文属性。具体参考 MDN[https://developer.mozilla.org…]。
- 须要裁减的中央也会被创立为图层
当咱们在一个 100100 的 div 中书写大量文字时,文字所显示的区域必定会超出 100100。这时候就产生了剪裁,渲染引擎会裁剪文字内容的一部分用于显示在 div 区域。呈现这种裁剪状况的时候,渲染引擎会为文字局部独自创立一个层。如果呈现滚动条,滚动条也会被晋升为独自的层。只有满足上述 2 条件任意之一,即会被晋升为独自一层。
绘制 Paint
经验了上述步骤,终于咱们到了绘制这一步了。
绘制是其实一个大的过程,包含生成绘制记录 Paint Records,合成器分图块,栅格线程光栅化(调用 GPU 生成位图),合成器帧提交等过程。
通过分层咱们晓得了一些非凡元素的层级关系。然而,咱们还不晓得同一层内的元素的层级关系,谁该笼罩谁。主线程依据后面的 Layer Tree 为每一层创立绘制记录表 Paint Records,决定谁先画谁后画。后画下来的必定笼罩后面的,也就决定了同一层内的元素层级。绘制记录表也了解成一个相似单向链表的模式,遍历链表即可取得绘制程序。
在查看文档的过程中,咱们会发现不同文档对学生成 Layer Tree 还是先失去 Paint Records 有不同说法。
我了解的应该是先分层,而后对每一层创立 Paint Records。如果是先遍历整个 Layout tree 失去绘制记录再分层的话,会多了很多额定的工作,比方把绘制记录的某些绘制步骤挑出来和某些层绑定在一起。而且从 Chrome devtool 里的 profermance 能够看到,先创立了 layer tree,而后开始 paint。
有了 图层 和绘制记录表 之后,将信息提交给合成器线程进行合成。因为一个图层可能十分十分大,超过了视口的面积,那么一次性将这么大的图层全绘制进去是没有必要的。所以还须要将图层宰割成一个个 图块 Tile,优先绘制这些图块。图块大小通常是 256256 或 512512,而后将图块信息传递给 栅格化线程池。
栅格化线程池中都是栅格化线程,这些线程执行栅格工作 Raster Task 将图块生成位图,优先生成视口 viewport 左近的位图。通常栅格化过程应用 GPU 来减速,所以又称为疾速栅格化、GPU 栅格化。
当所有的图块栅格化结束,合成器线程收集 Draw Quads 的图块信息。Draw Quads 记录了图块在内存中的地位和在页面哪个地位绘制图块。
当初万事俱备,在主线程内将 Draw Quads 这些信息合成合成器帧(Compositer Frame)并通过 IPC 管道发送给浏览器过程。浏览器过程再将合成器帧发送给 GPU。
GPU 执行渲染,页面呈现!!!
功败垂成!!!然而这不是完结,咱们还要思考到重排重绘。
从线程角度看重排重绘
作为前端常常听到说重排比重绘的开销大,那咱们从线程角度该如何了解呢?
重排(回流)
如果通过 js 或 CSS 批改元素的几何地位属性,如宽度、高度等,那么浏览器会触发从新布局。也就是从新生成 layout tree 及当前的所有流程,并全都再走一遍。这种开销是比拟大的。
重绘
如果只是扭转元素背景色彩,则不必批改 layout tree 和 layer tree,也不必批改进入绘制以及之后的流程。因为省略了布局和分层阶段,开销会小一些,效率较高。
间接合成
如果更改一个即不要布局也不须要绘制的属性,则渲染引擎将跳过布局和绘制阶段,只执行后续的合成操作,咱们把这个过程称之为合成。
js 执行,重排,重绘都是运行在主线程的,都有可能因为大量的计算导致页面卡顿。而除了主线程外,还有合成器线程和栅格线程,如果能不应用主线程间接进行合成的话,就能使页面更加晦涩。
css 3 transform 就是这样的一个属性,它实现动画成果能够避开重排和重绘,间接在非主线程上执行动画合成的操作。因为不占用主线程,并且也没有布局和绘制的阶段,所以效率是最高的。
另外,除了应用 transform 属性外,还能够应用 requestAnimationFrame 办法。requestAnimationFrame 传入的 callback 会在下一帧的重绘前调用,从而尽可能的进步动画帧率。能够参考这篇文档[https://zhuanlan.zhihu.com/p/…]。
延展浏览
浏览器的演进
依照目前倒退状况来,将来 Chrome 整体架构会朝向古代操作系统所采纳的“面向服务的架构”方向倒退,以达到简略、稳固、高速、平安的指标。
现有的各种模块将重形成独立的服务(Service),比方把 UI、数据库、文件、设施、网络等模块重构为相似操作系统底层的根底服务,并在各自独立的过程中运行。同时通过应用定义好的接口以及 IPC 来通信、拜访,让零碎更内聚、松耦合、易于保护和扩大。
同时 Chrome 还提供灵便的弹性架构,在弱小性能设施上以多过程的形式运行根底服务,在资源受限的设施上(如下图),则会将很多服务整合到一个过程中,从而节俭内存占用。谷歌开发者文档[https://developers.google.com…]
目前 Chrome 正在逐渐构建 Chrome 根底服务(Chrome Foundation Service)这将是一个漫长的迭代过程,让咱们一起刮目相待。
参考文献
Render-tree Construction,Layout,and Paint:
[https://developers.google.com…]
Inside look at modern web browser(par1 – part4):
[https://developers.google.com…]
Chrome 浏览器架构:
[https://xie.infoq.cn/article/…]
Chrome 架构:仅仅关上了 1 个页面,为什么有 4 个过程:
[https://blog.poetries.top/bro…]
requestAnimationFrame 回调机会:
[https://zhuanlan.zhihu.com/p/…]
层叠上下文:
[https://developer.mozilla.org…]
Process Models:
[https://www.chromium.org/deve…]
举荐浏览
深入浅出聊聊 Rust WebAssembly(一)
go-zero:开箱即用的微服务框架