浏览器渲染原理与阻塞
浏览器多进程架构
- 主进程/浏览器进程:负责界面显示、用户交互、子进程之间的调度和存储
- 渲染进程:主要负责将
HTML
、CSS
、JS
转换成用户可以访问的网页。GUI
线程(重要)- 负责浏览器界面的渲染,
html
解析、CSS
解析、dom
树、render
树、分层树的构建 - 回流和重绘
- `GUI`和`JS`线程二者互斥,一个执行另外一个就会别挂起
- 负责浏览器界面的渲染,
JS
引擎线程(重要)- 负责处理
JS
脚本程序、JS
垃圾回收、任务队列
- 负责处理
- 事件触发线程
- 负责维护一个事件队列
- 定时器触发线程
- 负责处理计时器。
JS
线程执行代码是解析代码后执行,JS
阻塞可能会影响定时器的准确性,所以需要一个单独的线程处理计时器工作- 在浏览器中
setTimeout
的最低触发时间间隔为4ms
,即使定时器的时间写的是0
也不可能做到真的0
- 异步
http
请求线程- 在
XMLHttpRequest
连接后,浏览器新开一个线程 - 检测到状态变更时,回调函数会放在异步队列等待
JS
线程执行
- 在
- 网络进程:负责网络请求
GPU
进程:绘制界面的特殊效果- 插件进程:负责插件,防止插件崩溃影响页面
浏览器何将HTML渲染到页面上
-
关键渲染路径
五个步骤:1.构建
DOM
(文档对象模型)、2.构建CSSOM
(CSS
对象模型)、3.构建render
树、4.布局、5.绘制 -
每个步骤做的事情
-
构建
DOM
-
转化成字符串,根据文件编码如
UTF-8
将字节转换字符串 -
分词得到
Token
,将如<html>
,<body>
以及尖括号内的其他字符串 -
Token
转换为Node
,将token
转换成对象,包含了属性和方法 -
构造
DomTree
,将上一步的各个节点的关系构造得到一个树形的DomTree
,最终输出文档对象模型DOM
注意:
- 写在
html
标签中的行内样式是在html parser
解析的 html
解析并不等整个HTML
文档加载完才开始的,而是网络进程一边加载,HTML
解析器一边解析。
-
-
构建
CSSOM
内联
CSS
和外部CSS
样式表在此处解析。具体步骤和上面类似:字节→字符串→
Token
→Node
→输出CSSOM
细节:CSS Parse和Dom Parse是并行关系吗?
-
style
标签中的样式,会被html parser
解析,不存在这个说法。(实际中较少用) link
外链的CSS
文件,-
遇到
link
标签,网络进程加载CSS
的时候,不会阻塞html
的解析,这里是并行的。 - 当网络进程加载完样式文件后,
CSS parser
会和html parser
抢占主线程(同一时间只能进行一个操作)
- 加载:同步(非异步)
JS
文件的加载会阻塞html
的解析,而CSS
文件的加载不会阻塞html
解析。 - 解析:
CSS
和JS
的解析都会阻塞dom/html
解析
-
-
生成渲染树
-
从
dom Tree
开始遍历每一个可见节点不可见节点:
display:none
、脚本标签script
、元标签head
-
对于
dom Tree
上的每个可见节点,在CSSom
中找到对应的规则 -
最终在
Render Tree
上挂在带有样式的可见节点。Render Tree
上包含页面所有可见内容和样式信息。
-
-
布局Layout
-
计算每个节点在屏幕上的确切位置和大小
-
所有相对测量值都会转换成屏幕上的绝对像素
-
-
绘制Paint
最终浏览器呈现出
HTML
渲染完毕的结果。
-
JavaScript的加载与执行是否会阻塞页面渲染
谷歌浏览器做了很多优化,其中一个优化就是预解析操作。当渲染引擎收到字节流之后,会开启预解析线程,分析
HTML
文件中包含的JS
,CSS
文件,解析到相关文件后,预解析线程会提前下载这些文件。会同时发起这些文件的下载请求。这两个文件的下载过程是重叠的,所以总的下载时间等于最长的文件下载时间。不管CSS
和JS
文件谁先到达,都等到CSS
文件下载完成并生成CSSOM
,然后执行JavaScript
脚本。
JS
脚本分为不同情况,
- 写在
script
标签中的内部脚本,内联JS
- 通过
src
加载的外部文件,外部JS
以及JS
的加载或执行也会有不同的表现。
注意:不管是JS和加载还是解析,如果阻塞页面渲染,只会影响后续
DOM
的解析和渲染,并不会影响脚本之前的DOM
的解析和渲染。
内联JS
- 操作:
- 内联
JS
不存在加载操作,而它的解析和执行会阻塞后续页面的渲染。 - 遇到
JS
脚本时,JS
引擎会抢占渲染线程执行JS
文件,导致无法渲染
- 内联
-
内联
JS
放在底部原因:-
内联
JS
会阻塞它后面DOM
的渲染,放在底部不会阻塞渲染。 -
同时,放在底部可以拿到内存中已经构造好的
DOM
节点进行DOM
操作。
-
外链JS
前提:停止解析后续
DOM
并不意味着一定会阻塞页面的渲染。
外链JS
有加载和执行,同时加载又分为同步加载、异步加载(async/defer
)。
同时还要区分外链JS
所在的位置,顶部、底部。
-
加载
- 非异步加载的外链
JS
- 外部
JS
的加载可能花费很多时间,因此将JS
放在底部,防止页面白屏。 - 放在后面还可以获取已有的
DOM
- 外部
-
使用
defer/async
异步加载的外链JS
-
使用
async/defer
异步加载的文件不会阻塞后续DOM
的解析-
async
的文件在加载完就会执行,执行会阻塞页面渲染。 -
defer
的文件执行会延迟到文档完成解析后,触发DOMContentLoaded
事件前执行,有defer
属性的脚本会阻止DOMContentLoaded
事件,直到脚本被加载并且解析完成。
-
-
- 非异步加载的外链
-
执行
只要执行
JS
线程势必会抢占主线程,阻塞后续的渲染。外部脚本链接的加载和执行只会影响后续 Dom 的解析和渲的染,对于脚本之前的的 Dom 并不会阻塞它的解析以及渲染。
CSS的加载与解析是否会阻塞页面渲染
-
CSS
的加载既会也会阻塞DOM
的渲染。-
普通情况下,
link
标签的CSS
文件会交给网络进程异步下载,不会阻塞Dom Tree
的合成。 -
同时,如果页面中
CSS
未下载完,因此未解析完,无法构建render tree
,因此阻塞后续页面的渲染。 -
特殊i情况,如果
link
后存在JS
文件,那么CSS
代码的加载会阻塞后续JS
脚本的执行从而JS
脚本会阻塞后续DOM
解析,从而变相阻塞JS
代码之后的DOM
解析。
-
-
CSS
的解析一定会阻塞DOM
渲染,因为html parse
和CSS parse
二者是竞争状态。
参考文章
是JS的加载阻塞了页面的渲染还是JS的执行阻塞了页面的渲染?