此文是一道面试题,又不仅仅是一道面试题,不过这道题共分了三篇来说,嗯。。可想而知
接上文,上文我们讲了网络通信的部分,详细请看「一道面试题」输入URL到渲染全面梳理上-网络通信篇, 那么该说说页面渲染的流程了,也就是当输入一个URL拿到了页面后,浏览器怎么解析,怎么呈现
首先要了解这块内容,需要对下面这些知识点有一个简单认知
之前写的一篇帖子 「硬核JS」一次搞懂JS运行机制 - 传送门 里有介绍到,下面我们还是重新来一遍吧,花不了多长时间,全当复习一遍,大家也可以自行去看下来了解了解,当然如果你都清楚的话可以直接去看渲染过程
我们都知道,CPU
是计算机的核心,承担所有的计算任务
官方说法,进程
是CPU
资源分配的最小单位
字面意思就是进行中的程序,我将它理解为一个可以独立运行且拥有自己的资源空间的任务程序
进程
包括运行中的程序和程序所使用到的内存和系统资源
CPU
可以有很多进程,我们的电脑每打开一个软件就会产生一个或多个 进程
,为什么电脑运行的软件多就会卡,是因为 CPU
给每个 进程
分配资源空间,但是一个 CPU
一共就那么多资源,分出去越多,越卡,每个进程
之间是相互独立的, CPU
在运行一个 进程
时,其他的进程处于非运行状态,CPU
使用时间片轮转调度算法[1]来实现同时运行多个进程
线程
是 CPU
调度的最小单位
线程
是建立在 进程
的基础上的一次程序运行单位,通俗点解释 线程
就是程序中的一个执行流,一个 进程
可以有多个 线程
一个 进程
中只有一个执行流称作 单线程
,即程序执行时,所走的程序路径按照连续顺序排下来,前面的必须处理好,后面的才会执行
一个 进程
中有多个执行流称作 多线程
,即在一个程序中可以同时运行多个不同的 线程
来执行不同的任务, 也就是说允许单个程序创建多个并行执行的 线程
来完成各自的任务
进程是操作系统分配资源的最小单位,线程是程序执行的最小单位
一个 进程
由一个或多个 线程
组成,线程
可以理解为是一个进程中代码的不同执行路线
进程
之间相互独立,但同一进程下的各个 线程
间共享程序的内存空间 (包括代码段、数据集、堆等) 及一些进程级的资源 (如打开文件和信号)
调度和切换:线程上下文切换比进程上下文切换要快得多
「多进程:」 多进程指的是在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态。多进程带来的好处是明显的,比如大家可以在网易云听歌的同时打开编辑器敲代码,编辑器和网易云的进程之间不会相互干扰
「多线程:」 多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务,也就是说允许单个程序创建多个并行执行的线程来完成各自的任务
JS的单线程,与它的用途有关,作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM,这决定了它只能是单线程,否则会带来很复杂的同步问题
比如,假定 JavaScript
同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
还有人说 js
还有 Worker
线程,对的,为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许 JavaScript
脚本创建多个线程,但是子线程是完 全受主线程控制的,而且不得操作DOM
所以,这个标准并没有改变JavaScript是单线程的本质
了解了进程和线程之后,接下来看看浏览器解析,浏览器之间也是有些许差距的,不过大致是差不多的,下文我们皆用市场占有比例最大的Chrome为例
作为前端,免不了和浏览器打交道,浏览器是多进程的,拿 Chrome
来说,我们每打开一个Tab页就会产生一个进程,我们使用 Chrome
打开很多标签页不关,电脑会越来越卡,不说其他,首先就很耗CPU
我们假设浏览器是单进程,那么某个Tab页崩溃了,就影响了整个浏览器,体验有多差
同理如果插件崩溃了也会影响整个浏览器
当然多进程还有其它的诸多优势,不过多阐述
浏览器进程有很多,每个进程又有很多线程,都会占用内存
这也意味着内存等资源消耗会很大,有点拿空间换时间的意思
到此可不只是为了让我们理解为何Chrome运行时间长了电脑会卡,哈哈,第一个重点来了
页面的渲染,JS的执行,事件的循环,都在渲染进程内执行,所以我们要重点了解渲染进程
渲染进程是多线程的,我们来看渲染进程的一些常用较为主要的线程
setInterval
与 setTimeout
所在线程setTimeout
中低于4ms的时间间隔算为4ms了解了上面这些基础后,接下来我们开始进入今天的正题,输入URL拿到资源之后,如何渲染,又经历了哪些过程?
我们都知道,浏览器之间的渲染流程是有些细微差别的,我们这里介绍的一些知识点是基于Chrome的,也就是Webkit,毕竟它是主流,先来看一下Webkit的渲染流程图
大家如果第一次看到这张图,可能会有点迷茫,不知从哪看起,别急,先大致过一眼,我们下面会慢慢介绍,一步步的分析,如果大家完整的阅读完此文,不妨回过头来再看一遍这张图,会清晰很多
浏览器渲染,那么浏览器肯定就拿到页面内容了,肯定要先解析HTML的
话不多说,我们直接来看HTML解析的图 ( 网图侵删 )
如果是第一次看到这张图可能看不懂,没关系,慢慢道来
先来看图中解析DOM的这几个大过程
Bytes(字节) -> Characters(字符) -> Tokens(词) -> Nodes(节点) -> DOM(DOM树)
首先,发起请求拿到页面 HTML 内容,这个内容它是0/1这样的原始 字节流
接着,浏览器拿到这些 HTML 的原始字节,根据文件的指定编码 (例如 UTF-8) 将它们转换成各个 字符
现在字节流变成了 字符流
,也就是一大串字符串
为了把 字符流
解析成正确的 DOM
结构,浏览器还要继续努力
接着进行 词法解析
,把字符流初步解析成我们可理解的 词
,学名叫 token
「嗯?什么是词 (Token)?」
词
是编译原理中的最小单元,如标签开始、属性、标签结束、注释、CDATA节点
Token
会标识出当前 Token
的种类,有点绕,怎么说方便理解呢,举个例子
<div class="haha">haha</div>
如上,这是一个标签它有一个class属性 (废话),但是浏览器拿到的只是字符串,它不知道这都是什么标签有啥属性要做什么,那么得给它一点一点拆开读,就是词法解析,怎么解析,就像下面这样
1. <div # 哦,看到了<div,这是一个div标签的开始
2. class="haha" # 这是一个class属性
3. > # 哦,到这儿是一个完整的div开始标签
4. haha # 嗯,这是一个文本
5. </div> # 奥,看到了</div>,整个div标签结束了
词法解析
是编译原理中的概念,上面是极度简化版本 (防大佬死磕),只是为了方便大家理解
现在理解了吗,Tokens
这个阶段中会标识出当前 Token
是 开始标签
或是 结束标签
亦或是 文本
等信息
那么我们收回思路,接着上面的步骤,经历 词法解析
我们把字符流解析成了 词 (Token)
接着在每个 Token
被生成后,会立刻消耗这个 Token
创建出节点对象,就是 节点 (Nodes) 阶段
把开始结束标签配对、属性赋值好、父子关系这些都连接好了,最终就构成了 DOM
树
后面这两小步也可称为 语法解析
,到此 DOM Tree
就解析完了
另外多嘴一句,DOM树(DOM Tree) | 文档对象模型
,这些东西说的都是 DOM树
有 HTML
解析,那肯定有 CSS
解析,比如我们构建 DOM
的时候遇到了 link
标记,该标记引用一个外部 CSS
样式表,那么浏览器会认为它需要这个外部样式资源,就会立即发出对该资源的请求,并返回样式内容,也是字节流
与处理 HTML
时一样,将收到的 CSS
规则转换成某种浏览器能够理解和处理的东西,基本步骤重复 HTML
过程,不过是构建 CSS 而不是 HTML
CSS
字节转换成字符,接着词法解析与法解析,最后构成 CSS对象模型(CSSOM)
的树结构
我们都知道,节点样式是可以继承的,所以在构建的过程中浏览器得递归 DOM
树来确定元素到底是什么样式,为了 CSSOM
的完整性,只有等构建完毕才能进入到下一个阶段,所以就算 DOM
已经构建完了,也得等 CSSOM
,然后才能进入下一个阶段
所以 CSS
的加载速度与构建 CSSOM
的速度会影响首屏渲染速度,这就是我们常说的 CSS
资源的加载会阻塞渲染
怎么优化?DOM树要小,CSS尽量用 id
和 class
少直接用标签😄
这个解析 JS
的步骤是不固定的,因为在构建DOM 树的过程中,当 HTML
解析器遇到一个 script
标记时,即遇到了js,立即阻塞DOM树的构建,就会将控制权移交给 JavaScript
引擎,等到 JavaScript
引擎运行完毕,浏览器才会从中断的地方恢复DOM树的构建
为什么上面也说了,「JS会对DOM节点进行操作,浏览器无法预测未来的DOM节点的具体内容,为了防止无效操作,节省资源,只能阻塞DOM树的构建」
例如,若不阻塞DOM树的构建,若 JS 删除了某个DOM节点A,那么浏览器为构建此节点A花费的资源就是无效的
若在 HTML 头部加载 JS 文件,由于 JS 阻塞,会推迟页面的首绘,所以为了加快页面渲染,一般将 JS 文件放到HTML 底部进行加载,或是对 JS 文件执行 async
或 defer
加载
async
是异步执行,异步下载完毕后就会执行,不确保执行顺序,一定在 onload
前,但不确定在 DOMContentLoaded
事件的前或后defer
是延迟执行,在浏览器看起来的效果像是将脚本放在了 body
后面一样(虽然按规范应该是在 DOMContentLoaded
事件前,但实际上不同浏览器的优化效果不一样,也有可能在它后面)渲染树 ( Render Tree ) 由 DOM树
、CSSOM树
合并而成,但并不是必须等 DOM树
及 CSSOM树
加载完成后才开始合并构建 渲染树
,三者的构建并无先后条件,也并非完全独立,而是会有交叉,并行构建,因此会形成一边加载,一边解析,一边渲染的工作现象
CSSOM 树
和 DOM 树
合并成渲染树,渲染树
只包含渲染网页所需的节点,然后用于计算每个可见元素的布局,并输出给绘制流程,将像素渲染到屏幕上
如上图 ( 网图侵删 ) ,为了构建渲染树,我们看看浏览器都做了什么
span
标签有 display: none
属性,也会被忽略渲染树
同时包含了屏幕上的所有可见内容及其样式信息,有了渲染树,再接着就要进入布局 ( layout ) 阶段了,到目前为止,我们计算了哪些节点应该是可见的以及它们的计算样式,但我们还没有计算它们在设备 视口[2] 内的确切位置和大小,这就是 布局
( Layout ) 阶段,也称为 自动重排
或 回流
( Reflow )
此阶段一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树
简单举个例子,我们看下面这段代码
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>hahaha</title>
</head>
<body>
<div style="width: 50%">
<div style="width: 50%">Hello world</div>
</div>
</body>
</html>
上面代码网页的正文包含两个嵌套 div:第一个父 div 将节点的显示尺寸设置为视口宽度的 50%,父 div 包含的第二个 div 将其宽度设置为其父项的 50%,即视口宽度的 25% (网图侵删)
布局流程的输出是一个 盒模型
,它会精确地捕获每个元素在视口内的确切位置和尺寸,当然,所有相对测量值都转换为屏幕上的绝对像素
我们先往下看,稍后还会给大家介绍
经历了以上种种步骤,终于来到了 绘制
,这一步听名字就能想到其作用了
经由前几步我们知道了哪些节点可见、它们的计算样式以及几何信息,我们将这些信息传递给最后一个阶段将渲染树中的每个节点转换成屏幕上的实际像素,也就是俗称的 绘制
或 栅格化
绘制
过程中有一种绘制叫 重绘
,也就是下我们要说的
元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就可以了,这叫做 重绘
( Repaint )
上面我们已经说过了 回流
,当然也叫 重排
,要知道,回流
一定伴随着 重绘
,重绘
却可以单独出现,对比来看,显然回流的成本开销要高于重绘,而且一个节点的回流往往还会导致子节点以及同级节点的回流,所以优化方案中一般都包括,尽量避免 回流
resize
render树
的直接变化,当获取一些属性时,浏览器为了获得正确的值也会触发回流,这样使得浏览器优化无效getComputedStyle()
或者IE的 currentStyle
上面我们说到,回流开销太大了,那么我们肯定是要优化的,接着看,其实就是尽量避免上面那些操作
style
,或者将样式定义为 class
并一次性更新documentFragment
,在它上面应用所有DOM操作,最后再把它添加到 window.document
display:none
( 会触发一次 reflow
),然后做修改后,再把它显示出来总之,说来说去,回流重绘,特别是回流,特别耗费资源,尽量避免就好,关于一些CSS属性会引起的回流重绘,可以去这个网站查查看 https://csstriggers.com/
终于来到了最后一个点 合成
,我们先来总结一下上面的步骤,到目前我们经历渲染过程如下
HTML
文档,形成 DOM 树CSS
,产生 CSSOM树<head></head> 或 display:none
Reflow
) 或 布局(Layout
)Repaint
)composite
),浏览器会将各层信息发送给GPU,GPU将各层合成,显示在屏幕上关于合成这一步骤,准备细聊一下子,让大家对其有个基本概念,因为刚开始忽略了它
首先,我们需要简单了解一些基本概念
浏览器在渲染图形的时候,有一个绘图上下文,绘图上下文又分成两种类型
网页也有三种渲染方式
当然,这些我们也不需要深入理解,知道它们的存在即可
Webkit 在不需要硬件加速内容的时候(包括但不限于 CSS3 3D变形
、CSS3 3D变换
、 WebGL
和 视频
),它就可以使用 软件渲染技术
来完成页面绘制
上面我们看到了软件渲染技术,它是什么呢?我们接着看
对于每个渲染对象,需要三个阶段绘制自己
硬件加速技术是指使用 GPU 的硬件能力来帮助渲染网页 ( GPU的作用主要是用来绘制3D图形并且性能很 nice )
浏览器渲染的图层一般包含两大类:普通图层
以及 复合图层
普通文档流大家就可以理解为一个复合图层,我们叫它默认复合层
,因为里面不管添加多少元素,其实都是在同一个复合图层中,absolute
布局、 fixed
也一样,虽然可以脱离普通文档流,但它仍然属于 默认复合层
复合图层,可以独立于普通文档流中,改动后可以避免整个页面重绘,提升性能,但也不要大量使用复合图层,否则由于资源消耗过度,页面反而会变的更卡,因小失大
GPU中,各个复合图层是单独绘制的,所以也互不影响,通过 硬件加速
的方式,会声明一个 新的复合图层
,它会单独分配资源,当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响 默认复合层
里的回流重绘
复合图层或者说硬件加速,其实就是仅触发合成 composite
,那么也就必须符合以下三个条件
寻思一下,可以做到这种情况得还真的不多 ( Chrome )
transform
opacity
属性 / 过渡动画 (需要动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之前的状态)will-chang
属性 (这个比较偏僻),一般配合 opacity
与 translate
使用,除了上述可以引发硬件加速的属性外,其它属性并不会变成复合层,作用是提前告诉浏览器要变化,这样浏览器会开始做一些优化工作 (最好用完后就释放)<canvas> <webgl>
等元素flash
插件等等通俗一点,假如我们给一个元素加了 transform
属性吧,那么该元素就不会影响也不会依赖文档流,也不会造成重绘,就变成了一个复合图层,也就可以说我们对它使用了传说中的 硬件加速技术
到了这里,大家可能有些迷惑,我们不是常说 absolute
是脱离文档流吗,为什么上面复合图层或者说硬件加速中没有 absolute
呢
其实,absolute
虽然可以脱离普通文档流,但是无法脱离默认复合层,就像它的 left
属性可以使用百分比的值,依赖于它的 offset parent
所以,就算 absolute
中信息改变时不会改变普通文档流中的 渲染树
,但浏览器最终绘制时,是整个复合层绘制的,所以 absolute
中信息改变,仍会影响整个复合层的绘制,浏览器还是会重绘它,如果复合层中内容多,absolute
带来的绘制信息变化过大,资源消耗也非常严重
而我们上面说的硬件加速,那直接就是在另一个复合层了,所以它的信息改变不会影响默认复合层,当然内部肯定会影响属于自己的复合层,仅仅是引发最后的合成渲染
浏览器对上文介绍的关键渲染步骤进行了很多优化,针对每一次变化产生尽量少的操作,还有优化判断重新绘制或布局的方式等等,据上文所述,总结下页面渲染这块的优化实践,不分先后,大家也可一块来补充
class
名方式操作样式或动画table
布局了transform
和 opacity
,不会发生重排和重绘上面讲的有些随意,最后再来波官方点的总结吧
发起一个请求,我们拿到了页面,下载完的网页将被交给浏览器内核(渲染进程)进行处理
JS引擎
对 JS代码进行解释执行,此时由 JS引擎
和 GUI渲染线程
的互斥,GUI渲染线程
就会被挂起,渲染过程停止,如果 JS 代码的运行中对DOM树进行了修改,那么DOM的构建需要从新开始JS引擎
执行后才继续构建DOM页面渲染篇到此就结束了,又是上万字,好像也没讲太多东西,大家还是只能以庞观的方式去了解,私下想深入的话还是要多看些相关资料,此文也是我看了很多资料输出的,看完本文,再去看资料或深入应该也会容易了解一些吧,这几篇帖子的核心都脱离不了那道经典面试题,那么看到了这里基本的一些知识点都已经给大家阐述过了,可以自己尝试总结一番了,一定要自己总结再看下文总结篇,这样大家也算没白浪费时间
下一文「一道面试题」输入URL到渲染全面梳理下-总结篇,待续哦。。
对您有帮助的话,动动小手,点个在看鼓励下吧,当然,个人理解,如有不正,欢迎指出,不胜感激
❝参考 ( 参考了很多帖子,贴了三个认为比较好的,推荐大家看一看 )
[1]时间片轮转调度算法: https://baike.baidu.com/item/时间片轮转调度
[2]视口: https://developers.google.com/web/fundamentals/design-and-ux/responsive?hl=zh-cn#set-the-viewport
[3]从输入URL到页面加载的过程?如何由一道题完善自己的前端知识体系: https://segmentfault.com/a/1190000013662126
[4]从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理: https://segmentfault.com/a/1190000012925872
[5]一篇文章说清浏览器解析和CSS(GPU)动画优化: https://segmentfault.com/a/1190000008015671