# 浏览器渲染机制
# 进程/线程
进程(
process
):启动一个程序的时候,操作系统会为该程序创建一块内存,用来存放代码、运行中的数据和一个执行任务的主线程,我们把这样的一个运行环境叫进程。 线程(thread
)是CPU
调度和分配的基本单位,可以和同一个进程的其他线程共享进程所拥有的全部资源,线程是不能单独存在的,它是由进程来启动和管理的。
总结来说,进程和线程之间的关系有以下 4
个特点:
- 进程中的任意一线程执行出错,都会导致整个进程的崩溃。
- 线程之间共享进程中的数据。
- 当一个进程关闭之后,操作系统会回收进程所占用的内存。
- 进程之间的内容相互隔离。
最新的
Chrome
浏览器包括:1
个浏览器(Browser
)主进程、1
个GPU
进程、1
个网络(NetWork
)进程、多个渲染进程和多个插件进程。
TIP
浏览器地址输入一个URL
,浏览器根据DNS
服务器得到域名的IP
地址,这个时候浏览器进程会向这个IP
地址发送请求,获取HTML
内容。然后将这些内容交给渲染进程。渲染进程解析内容,解析遇到的请求网络的资源又返回来交给浏览器进程进行加载,同时通知主进程(浏览器进程),需要插件进程加载插件资源,执行插件代码。解析完成后,渲染进程得到图像帧,并交给GPU
进程,将其转化为图像显示屏幕。四种进程介绍
首先,当我们是要浏览一个网页,我们会在浏览器的地址栏里输入
URL
,这个时候Browser Process
会向这个URL
发送请求,获取这个URL
的HTML
内容,然后将HTML
交给Renderer Process
,Renderer Process
解析HTML
内容,解析遇到需要请求网络的资源又返回来交给Browser Process
进行加载,同时通知Browser Process
,需要Plugin Process
加载插件资源,执行插件代码。解析完成后,Renderer Process
计算得到图像帧,并将这些图像帧交给GPU Process
,GPU Process
将其转化为图像显示屏幕。
# 浏览器进程模式
TIP
Chrome浏览器多进程的好处:
- 更高的容错性。当今
WEB
应用中,HTML,JavaScript
和CSS
日益复杂,这些跑在渲染引擎的代码,频繁的出现BUG,而有些BUG
会直接导致渲染引擎崩溃,多进程架构使得每一个渲染引擎运行在各自的进程中,相互之间不受影响,也就是说,当其中一个页面崩溃挂掉之后,其他页面还可以正常的运行不收影响。 - 更高的安全性和沙盒性(
sanboxing
)。渲染引擎会经常性的在网络上遇到不可信、甚至是恶意的代码,它们会利用这些漏洞在你的电脑上安装恶意的软件,针对这一问题,浏览器对不同进程限制了不同的权限,并为其提供沙盒运行环境,使其更安全更可靠。 - 更高的响应速度。在单进程的架构中,各个任务相互竞争抢夺
CPU
资源,使得浏览器响应速度变慢,而多进程架构正好规避了这一缺点。
为了节省内存,Chrome提供了四种进程模式(
Process Models
),不同的进程模式会对tab
进程做不同的处理。
Process-per-site-instance (default)
- 同一个site-instance
使用一个进程
当你打开一个
tab
访问a.baidu.com
,然后再打开一个tab
访问b.baidu.com
,这两个tab
会使用两个进程。而如果你在a.baidu.com
中,通过JS代码打开了b.baidu.com
页面,这两个 tab 会使用同一个进程。
Process-per-site
:同一个site
一个进程Process-per-tab
:每个tab
使用一个进程single Process
:所有tab
共用一个进程
TIP
site
指的是相同的registered domain name
(如:google.com
,bbc.co.uk
)和scheme
(如:https://
)。比如a.baidu.com
和b.baidu.com
就可以理解为同一个site
(注意这里要和Same-origin policy
区分开来,同源策略还涉及到子域名和端口)。site-instance
指的是一组connected pages from the same site
,这里connected
的定义是can obtain references to each other in script code
怎么理解这段话呢。满足下面两情况并且打开的新页面和旧页面属于上面定义的同一个site
,就属于同一个site-instance
- 用户通过
<a target="_blank">
这种方式点击打开的新页面 JS
代码打开的新页面(比如window.open
)
- 用户通过
# 解析文档
上述有提到渲染进程会解析内容,然后遇到请求网络的资源就会返回给浏览器进程进行加载,那么解析这个过程又是如何进行的呢?
# 构建DOM
树
渲染进程的主线程开始工作,解析HTML
文本并转为DOM
树
# 子资源加载
当主线程解析遇到一些需要额外加载的资源时,如图片,CSS
和JavaScript
,为了提高请求速度,会向浏览器进程的网络线程发起请求。
# JS阻塞解析过程
当HTML
解析器遇到script
标签时,会暂停HTML
解析工作,转而去加载Js
代码。因为这部分代码可能会影响文档结构。也会导致CSSOM
也阻塞。
# 样式计算
主线程解析计算完CSS
产生CSS
规则树,才会对文档节点赋予最终的样式。
处理过程:1.把
CSS
转换成浏览器能够理解的结构--StyleSheets
,该结构同时具备了查询和修改功能。2.转换样式表中的属性值,使其标准化。3.计算出DoM
树中的每个节点具体样式
CSS
加载会阻塞页面显示吗?css
加载不会阻塞DOM
树的解析css
加载会阻塞DOM
树的渲染css
加载会阻塞后面js
语句的执行
下载
CSS
文件阻塞了,会阻塞DOM
树的合成吗?会阻塞页面的显示吗?
不会阻塞
dom
树构建的,因为HTML
转化为dom
树的过程,发现文件请求会交给网络进程去请求对应文件,渲染进程继续解析HTML
。 会阻塞页面的显示,当计算样式的时候需要等待css
文件的资源进行层叠样式,资源阻塞了,会进行等待,直到网络超时,network
报出错误,渲染进程继续层叠样式计算。
# 布局(初次回流)
主线程遍历
DOM
并计算样式,创建一个具体横纵坐标以及盒子边界大小数据的布局树(layout tree
)。布局树可能与DOM
树相似,但它只包含和页面即将呈现的节点相关的信息。如果某个元素设置了display: none
,虽然它会呈现在DOM
树中但并不会包含于布局树当中;如果有一个伪类元素p::before{ content: 'Hi!' }
, 那么它虽然不在DOM
树中,但仍然会出现在布局树当中。
# 分层
渲染引擎为特定的节点生成专用的图层,并生成一棵对应的图层树。拥有层叠上下文的元素会被单独提升为一层;需要剪裁的地方会被创建为图层(当超出容器内容被隐藏或出现滚动条均会被提升为单独层)
# 图层绘制
渲染引擎对图层树每个图层进行绘制。渲染引擎会把一个图层的绘制分成很多小的绘制指令,然后把这些指令按照顺序组成一个待绘制列表。当图层的绘制列表准备好之后,主线程会把绘制列表交给合成线程。
# 栅格化操作
将图块转换为位图,而图块是栅格化执行的最小单位。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在里面执行的。
# 合成和显示
一旦所有图块都栅格化,合成线程会生成一个绘制图块的命令,然后将该命令提交给浏览器进程。浏览器进程的viz
组件接收合成线程发过来的命令。然后根据命令将其内容绘制到内存中,最后再将内存显示在屏幕上
# 性能优化策略
基于上面介绍的浏览器渲染原理,
DOM
和CSSOM
结构构建顺序,初始化可以对页面渲染做些优化,提升页面性能。
JS
优化:<script>
标签加上defer
属性 和async
属性 用于在不阻塞页面文档解析的前提下,控制脚本的下载和执行。defer
属性:用于开启新的线程下载脚本文件,并使脚本在文档解析完成后执行,因此不会阻塞页面的渲染。async
属性:HTML5
新增属性,用于异步下载脚本文件,下载完毕立即解释执行代码。
TIP
defer
特性
换句话说:
- 具有
defer
特性的脚本不会阻塞页面。 - 具有
defer
特性的脚本总是要等到DOM
解析完毕,但在DOMContentLoaded
事件之前执行。 - 该特性仅适用于外部脚本,如果
<script>
脚本没有src
,则会忽略defer
特性。
<script defer src="https://javascript.info/article/script-async-defer/long.js"></script>
<script defer src="https://javascript.info/article/script-async-defer/small.js"></script>
2
浏览器扫描页面寻找脚本,然后并行下载它们,以提高性能。因此,在上面的示例中,两个脚本是并行下载的。
small.js
可能会先下载完成。但是,defer
特性除了告诉浏览器“不要阻塞页面”之外,还可以确保脚本执行的相对顺序。因此,即使small.js
先加载完成,它也需要等到long.js
执行结束才会被执行。保证脚本的执行顺序,即按照它们在HTML
文件中出现的顺序执行。在页面渲染完成后,会按照它们在HTML
文件中出现的顺序执行脚本,因此适用于需要依赖其他脚本的脚本。
async
特性 async
特性意味着脚本是完全独立的:
- 浏览器不会因
async
脚本而阻塞(与defer
类似)。 - 其他脚本不会等待
async
脚本加载完成,同样,async
脚本也不会等待其他脚本。 DOMContentLoaded
和异步脚本不会彼此等待:DOMContentLoaded
可能会发生在异步脚本之前(如果异步脚本在页面完成后才加载完成)DOMContentLoaded
也可能发生在异步脚本之后(如果异步脚本很短,或者是从HTTP
缓存中加载的)
换句话说,async
脚本会在后台加载,并在加载就绪时运行。DOM
和其他脚本不会等待它们,它们也不会等待其它的东西。async
脚本就是一个会在加载完成时执行的完全独立的脚本。异步加载脚本,但不保证脚本的执行顺序,即可能会出现多个脚本同时加载的情况。在脚本加载完成后,会立即执行脚本,不会阻塞页面的渲染,因此适用于不依赖其他脚本的单独脚本。如果有多个脚本需要依赖,可能会出现错误。
动态添加脚本 当脚本被附加到文档 (*) 时,脚本就会立即开始加载。默认情况下,动态脚本的行为是“异步”的。 也就是说:
- 它们不会等待任何东西,也没有什么东西会等它们。
- 先加载完成的脚本先执行(“加载优先”顺序)。
如果我们显式地设置了 script.async=false
,则可以改变这个规则。然后脚本将按照脚本在文档中的顺序执行,就像 defer
那样。
CSS
优化:<link>
标签的rel
属性 中的属性值设置为preload
TIP
preload
是一种浏览器预加载技术,用于在页面渲染之前,提前加载一些资源(例如JavaScript
、CSS
、字体等),以加速页面加载速度。preload
可以通过<link>
标签或HTTP
头来实现,具体实现方式如下:
<link rel="preload" href="style.css" as="style">
// 通过HTTP头实现:在服务器端设置HTTP头,以指定要预加载的资源。例如,以下代码可以预加载名为"script.js"的JavaScript文件:
Link: <script.js>; rel=preload; as=script
2
3
4
preconnect
则是一种浏览器预连接技术,用于在页面加载之前,预先建立与一些服务器的TCP
连接,以加速资源的请求和响应速度。preconnect
可以通过<link>
标签或HTTP
头来实现,具体实现方式如下:
<link rel="preconnect" href="https://example.com">
Link: <https://example.com>; rel=preconnect
2
3
prefetch
用于在浏览器的空闲时间请求资源
# 渲染过程总结
当浏览器获取
HTML
文件后,会自上而下加载并在加载过程中进行解析和渲染;加载就是获取资源的过程;如果在加载过程中遇到外部的css
文件和图片,浏览器会另外发送一个请求,去获取css
文件和图片,这个请求是异步的,并不会影响HTML
文件的加载;但如果遇到JavaScript
文件,HTML
文件会挂起渲染的进程,等待JavaScript
文件加载完毕后,再继续进行渲染。
- 浏览器解析
HTML
,CSS
和JavaScript
脚本。等脚本加载后,通过DOM API
和CSSOM API
来操作DOM Tree
和CSS Rule Tree
。 - 解析完成后,浏览器引擎通过两种树结构来构造渲染树。渲染树只包含可见节点
TIP
- 构建渲染树的过程,浏览器还做了一些关键的小动作:
- 从
DOM
树的根节点开始遍历,筛选出所有可见的节点; - 仅针对可见节点,为其匹配
CSSOM
中的CSS
规则; - 发射可见节点,连同其内容和计算样式。
- 从
Layout
回流:根据生成的渲染树,进行回流,得到节点的几何信息。- 重绘:根据渲染树以及回流得到的几何信息,得到节点的绝对像素
- 将像素发给
GPU
进程,展示在页面上。
# 回流(Reflow
,也叫重排)
是指当页面布局和几何属性改变时触发的浏览器行为,浏览器会重新计算并绘制元素的布局和几何属性。重排会导致整个页面的布局发生改变,非常消耗性能,所以要尽量避免频繁地进行页面布局的改变。回流一定会导致重绘
- 什么情况会回流(为了求一个即时性和准确性)
- 添加或者删除可见的
DOM
元素 - 元素的位置发生变化
- 元素的尺寸发生变化
- 内容发生变化
- 页面一开始的时候
- 浏览器的窗口尺寸变化
- 设置
style
属性的值 - 计算
offsetWidth/offsetHeight/clientWidth/等
的属性 - 调用
getComputedStyle/currentStyle
- 添加或者删除可见的
# 重绘(Repaint
)
是指当元素的样式发生改变,但不影响元素的布局和几何属性时触发的浏览器行为。在这种情况下,浏览器只需要重新绘制元素的外观而不需要重新计算布局和几何属性,因此比重排的代价小得多。重绘的代价比重排小,但如果频繁进行大量的重绘操作,也会对性能产生影响。重绘不一定会引起回流
TIP
- 并且很多人不知道的是,重绘和回流其实也和
Eventloop
有关。- 当
Eventloop
执行完Microtasks
后,会判断document
是否需要更新,因为浏览器是60Hz
的刷新率,每16.6ms
才会更新一次。 - 然后判断是否有
resize
或者scroll
事件,有的话会去触发事件,所以resize
和scroll
事件也是至少16ms
才会触发一次,并且自带节流功能。 - 判断是否触发了
media query
- 更新动画并且发送事件
- 判断是否有全屏操作事件
- 执行
requestAnimationFrame
回调 - 执行
IntersectionObserver
回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好 - 更新界面
- 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行
requestIdleCallback
回调。
- 当
# 减少重绘与回流
- 合并多次对
DOM
和样式的修改,然后一次性处理掉 - 使用
transform
替代top
(css3
的硬件加速,不会引起重绘回流。还有opacity
,filters
,Will-change
这些属性)
TIP
opacity
单词意思为透明度,直观视觉效果就是颜色变淡了,但最终显示的颜色其实仍然可以用RGB
三个通道来表示,从数值运算的角度来看,它实际上表示了它采用一般混合策略和其他颜色进行混合时的比例。显示颜色 = 合入色 x opacity + 底色 x (1 - opacity)
opacity
这个属性本身就是用在重叠部分颜色处理的过程中使用的,对于分层的图原来说就可以看作是与图层内容无关的系数,因为合成过程中当前层中所有像素都需要经历上面的颜色混合公式,所以opacity
的动画过程既不会影响布局,也不需要重绘。这样图层中保存的RGB
像素数据的缓存在动画过程中也就不需要更新了,如果不使用opacity
属性的话,每一帧对于变化部分都需要手动重计算RGB
颜色值(这也就相当于是重绘了),因为这些区域的像素颜色一直都在变化,缓存也就没有意义。transform
在动画过程中也不需要改变缓存的记录,而在图层合成时遍历当前层的点然后用矩阵matrix
计算出对应的新坐标点就可以了,它也可以视作一种与图层内容无关的变换,图层中的元素首次生成的位图信息缓存可以被反复使用。比如一段平移动画,如果使用绝对定位+改变left
值的方式来实现,就需要不断计算动画元素的布局并更新它的像素信息,但如果使用translate
来实现,动画元素在文档流中的位置并不需要改变,无论后续平移到多远,都可以使用位图缓存中保存的初始位置信息,再加上变换矩阵的影响在层合并时计算出来,同样既不影响布局,也不需要重绘,这就是它高性能的原因。
transform
动画由GPU
控制,支持硬件加速
- 使用
visibility
替换display: none
,前者只会引起重绘,后者会引起回流(布局改变了) - 不要把节点的属性值放在一个循环里当成循环的变量
- 不要使用
table
布局 - 动画速度越快,回流次数越多,可以使用
reuqestAnimationFrame
CSS
避免节点层级过多- 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点
- 使用
will-change
生成新图层 video/iframe
标签
- 使用
- 对
DOM
一系列修改时,我们可以使元素脱离文档流,对其进行多次修改,然后将元素带回到文档中。因为这个时候修改,元素已经不在渲染树中。脱离文档流方式:- 隐藏元素,应用修改,重新显式
- 使用文档片段(
document fragment)
在当前DOM
之外构建一个子树,再把它拷贝回文档。 - 将原始元素拷贝到一个脱离文档的节点中,修改节点后,再替换原始的元素。
# 为什么操作DOM慢
因为
DOM
是属于渲染引擎中的东西,而JS
又是JS
引擎中的东西。当我们通过JS
操作DOM
的时候,其实这个操作涉及到了两个线程之间的通信,那么势必会带来一些性能上的损耗。操作DOM
次数一多,也就等同于一直在进行线程之间的通信,并且操作DOM
可能还会带来重绘回流的情况,所以也就导致了性能上的问题。