浏览器架构:
早期浏览器架构: 单进程,多线程,带来的问题是效率低,不稳定,当一个线程崩溃,整个浏览器进程也崩溃
现代浏览器架构: 多进程,其主要进程包含:
浏览器进程: 负责浏览器页面,用户交互,缓存,子进程管理
网络进程: 负责网络资源请求
GPU进程:负责网页的渲染
插件进程: 负责如chrome浏览器中的插件,在沙箱(sandbox)中,如果崩溃不会影响浏览器
渲染进程(重要!): 每个页面对应一个渲染进程,也称为浏览器的内核
其中,主流浏览器对应的内核如下:(面试可以简单记一下)
Chrome: Blink
Safari: webkit
IE: Trident
FireFox: Gecko
渲染进程(内核)
渲染是什么?
HTML字符串 -> 屏幕上每个像素的颜色信息
GUI渲染引擎 & javascript解析引擎
渲染进程包含一个主线程,这个主线程上工作着 GUI渲染引擎 和 javascript解析引擎。其中GUI渲染引擎负责解析html字符串,javascript引擎负责解析javascirpt代码(chrome浏览器中的V8)
GUI渲染引擎和javascript引擎互斥的工作在主线程上,当渲染引擎工作时,javascript引擎会被阻塞无法工作,反之亦然,当javascipt引擎释放主线程时,渲染引擎才工作,也就是js运行和页面渲染是互斥的,这也解释了为什么js会阻塞网页渲染。
为什么要互斥?
1. 设计上因为GUI渲染引擎和js解析引擎运行在主线程中,无法脱离宿主
2. 用途上,因为要操作DOM,多线程运行会导致出现临界资源的问题,需要互斥锁,信号量等等机制来解决 很复杂。
任务队列
为了协调渲染引擎和js解析引擎的工作,引入一个任务队列(event loop),任务队列相当于中间人在浏览器地址栏输入url之后,主进程中的ui线程会判断是否为url,如果是则让网络进程去处理并且请求资源。
请求到的资源 (html)是一串字符串,会包装成一个(C++)渲染任务,并且放到任务队列,此时GUI渲染引擎会从任务队列中取出任务并且执行html的解析和渲染,当解析到<script>标签(不考虑defer和async)会将scipr中的js代码包装成任务放到任务队列并挂起,此时js解析引擎来解析执行js代码。当js执行完其中的所有同步代码,也会挂起。
当js遇到一个异步任务如
// 宏任务
settimeout(()=>{
console.log(xxxx)
},1000)
//微任务
Promise.resolve(()=>{
console.log(xxx)
})
// 网络请求
fetch(url).then((res)=>{
console.log(res)
})
也会将任务交给浏览器中其他的进程处理,如计时器,网络进程等等,当这些任务执行完成后,会将相应的callback包装成任务放到任务队列中,排队执行。我们可以通过将代码包装到异步任务中的方式挂起js解析引擎,让出主线程,对于一些运算量大的代码可以分段处理,防止渲染卡顿,白屏,影响用户体验。
现代浏览器由多个任务队列
微任务队列 优先级最高
交互队列 优先级高
延时队列 优先级 中
渲染引擎工作流程
html&css 解析
html和css的解析是并行发生的,其中主线程中的渲染引擎会解析html字符串,同时主线程之外的预解析线程会扫描文档找到下载并且解析css代码,这样有利于提升解析速度。
渲染引擎会线创建一个Document对象(C++对象)作为根节点,然后根据html的结构,创建DOM(Document Object Module 文档对象模型 )树。
注意 DOM是C++对象构成的树,js是在上面做了一层包装,让我们可以通过js访问DOM节点
由于css的解析和dom的解析不干扰 ,为了同时解析,Css解析不在主线程,会生成CSSOM树, 也是C++对象 其子节点是所有css来源,通过js在document.cssStyleSheets 可以访问。
具体流程如下图所示:也就是到attachment的步骤
通过上图可以看到 css的解析,不干扰html的解析,但是会影响下面的html渲染,因为cssom树要和dom树合并成布局树,后面的步骤会受到css下载解析渲染的影响。
同时,因为js中可以访问css对象,如document.style 所以js的解析运行必须要在css解析之后进行,也就是说 css的解析会影响js的解析运行
样式计算
html解析生成dom树 ,css解析生成cssom树,将HTML解析生成的 DOM树遍历,结合CSSOM树,对每个节点计算出最终样式,生成一棵带有最终样式(Computed Style 计算后的样式)的DOM树。样式计算之后,所有的样式属性 都必须有value,如图所示:
布局Layout
布局阶段就是将样式计算之后的树,做一些修改,保留包含有几何信息的元素,并且增加一些dom中不包含的元素 如伪元素,匿名行内元素等。
渲染树(布局树)中仅仅包含有几何信息的元素,比如:
- 比如display: none的元素,这些元素在布局树中就没有 因为没有空间,但是visbility: none这会有 因为只是看不见 但是空间还有(底层原理)
- 再比如伪元素,在DOM中无法找到,就是在布局阶段挂在布局树上了,因为其有几何信息
- 内容必须在行盒中,如果内容在块盒中,则会包裹一个匿名行内元素,这个元素也会挂在layout树上,但是在DOM中也是找不到的
- 行和块盒不能相邻,所以会创建匿名块盒也是放到布局树中,在dom中看不到
可以简单的观察一下,在浏览器中的元素检察中,是无法看到伪元素,匿名行内元素等等的,因为其在布局树不在dom树上面。
注意,布局树也是C++对象,但是不提供类似于cssom和dom树这样js包装提供访问的方式。 只能通过类似于clientWidth clientHeight这样的方式间接访问。
分层
为了提高效率,会对页面进行分层,将经常渲染的部分进行拆分,以避免发生变动(如reflow)之后要全部修改,比如滚动条就是分层的 如下图所示:
使用will-change opacity等 可以改变分层策略
绘制 paint
根据每一层布局树的几何信息,生成绘制指令,注意这里类似于canvas,不是每个像素点的颜色信息,而是一些描述了如何绘画的信息(举个例子,比如向右移动xxx像素 在向下移动xxx个像素画线)
此时,渲染引擎在主线程中的任务完成,总结其任务包含 解析html -> 样式计算 -> 布局 -> 分层 -> 绘制 生成绘制指令
合成 (合成线程)
绘制指令会被交给渲染进程中的另一个线程: 合成线程进行处理
合成线程会在线程池中启动多个线程完成分块工作,会把大的网页分块,然后将对应的小块显示在viewpoint视口上
光栅化 (GPU进程 - 注意在渲染进程之外)
光栅化也就是将分好块的绘制指令转换成位图,也就是对应每个像素点的颜色信息,此过程中,合成线程会将分好块的绘制指令交给渲染进程之外的GPU进程来处理,GPU进程处理完成后会将位图信息返回给合成线程。
画
合成线程会将位图给gpu进程,gpu进程操作硬件(显卡)
为什么要走GPU进程做中转 不麻烦吗?
这里注意 合成线程在渲染进程,渲染进程在沙箱内 sandbox 没有直接调用硬件的能力
(第三方插件进程和渲染进程都在沙箱内 保证一个进程出错不会影响整个浏览器的运行!)
此时,网页内容就被显卡渲染到浏览器上了,渲染结束。
优化
transform 滚动
对于transform动画和滚动页面,会直接走最后一步,也就是直接重新画,因为不需要重新生成前面的信息了,这也是为什么滚动和transform动画的效率很高
可以在html中加入一段死循环的同步js代码做实验,即便页面卡死了,渲染线程不工作,页面也能完成transform动画和滚动,因为只走最后一步,页面卡死不影响画的过程。
重排(回流/reflow)和 重绘(repaint)
重排: resize页面,修改了几何信息,修改了dom之间的排版,此时需要从布局开始 -> 分层 -> 绘制 -> 分块 -> 光栅化 -> 画后面所有的步骤 (重新生成布局树)
所以尽可能不要用js频繁修改元素的尺寸,如<AutoHeightContianer/>开销就很大 尽量少用
repaint(重绘) 开销更低
如果变化的元素,只是更改了元素的背景色,文字颜色、边框颜色等等不影响它周围或者内部布局的属性,那这种行为只会引起repaint(重绘) 走绘制 -> 分块 -> 光栅化 -> 画后面所有的步骤,不重新生成布局树,所以repaint的速度明显比reflow快
对比
-
重排一定会触发重绘,重绘未必触发重排(只改了背景 颜色 等等)
-
修改布局,修改元素位置,添加删除元素,尺寸改变,内容改变,窗口resize等等修改布局layout会触发重排reflow
-
修改属性, 修改元素的背景色、边框颜色等样式属性,添加或删除元素的伪类,如:hover等,操作canvas、SVG等图形元素。 触发重绘
合并渲染
如果在javasciript中修改了元素的几何信息,比如修改了元素的宽高,会生成一个渲染任务加入到任务队列中,并且在当前js代码执行完成后,js引擎挂起释放主进程后,会轮训检查任务队列并且让渲染引擎介入完成重新渲染,但是如果频繁变更几何信息,如:
dom.style.width = '1000px'
dom.style.height = '2000px'
dom.style.marginTop = '20px'
浏览器为了优化性能,不会生成三个渲染任务
浏览器会维护一个渲染队列,每次对布局有修改,都会push进队列中,等到队列数量够多或者达到一定时间flush队列,生成一个渲染任务,加入到事件循环中,等待js引擎让出主线程后,渲染引擎接入,完成渲染。
但是,如果我们直接读取布局树信息 如.clientWidth,由于更新渲染还没有执行,为了保证读取的正确性,浏览器会将保存在flush队列中的值返回,并且强制flush队列生成一个渲染任务,所以
dom.style.width = '1000px'
// 读区几何信息
console.log(dom.clientWidth)
dom.style.height = '2000px'
// 读区几何信息
console.log(dom.clientHeight)
dom.style.marginTop = '20px'
// 读区几何信息
console.log(dom.clientWidth)
此代码会生成三个渲染任务,浏览器的合并优化就不起作用,导致三次reflow,影响性能。
所以尽量少的去读区布局树信息,因为每次读取,都会强制flush渲染队列,生成任务,导致重排数量增加。
总结:
- 不要在js中频繁读区样式
- 如果要修改样式尽可能的集中修改
- 尽可能使用 transform实现动画效果 (这个只设计画的这个步骤 包括滚动条滚动)
javascript资源 script async defer
我们知道,使用<script/> 直接引入javascript代码会阻塞渲染引擎的渲染,因为js代码是可以动态的修改前面的dom结构,这可能使dom解析渲染没有意义,所以正常情况下,渲染引擎会等待js代码执行结束之后继续解析渲染。
这样可能导致一些问题,由于js的下载,解析,执行,都需要耗费时间,此时主线程被占用,渲染引擎无法工作,会造成页面白屏,用户体验很差。
如果js代码中包含了同步的delay如:
<div>hello</div>
<div>hello</div>
<div>hello</div>
<div>hello</div>
<div>hello</div>
<div>hello</div>
<div>hello</div>
<script>
const delay = (times) => {
const startTime = new Date().valueOf()
while (new Date().valueOf() - startTime <= times) {
console.log('delay')
}
}
// 同步delay
delay(1000)
</script>
此时js引擎占据主线程,渲染引擎也被挂起无法进入,页面会白屏,会堵在attachment之前,导致上面的hello也展示不出来。
同时这种方式也会造成,如果在html中间引入js代码,那么js代码无法获取下方的dom元素,所以一般会将script标签放到html body的最下方,但是造成的问题是,在html代码都解析完成之后再去下载,解析,执行js,也会增加渲染的时间,能否让下载提前的情况下,也能最后运行js代码
所以引入async和defer,注意defer和async都无法作用域inline的script 必须是引用外部的js文件才起作用
async
当浏览器遇到带有 async 属性的 script 时,请求该脚本的网络请求是异步的,不会阻塞浏览器解析 HTML,一旦网络请求回来之后,如果此时 HTML 还没有解析完,浏览器会暂停解析,先让 JS 引擎执行代码,执行完毕后再进行解析,图示如下:
当然,如果在 JS 脚本请求回来之前,HTML 已经解析完毕了,那就啥事没有,立即执行 JS 代码,如下图所示:
所以 async 是不可控的,因为执行时间不确定,你如果在异步 JS 脚本中获取某个 DOM 元素,有可能获取到也有可能获取不到。而且如果存在多个 async 的时候,它们之间的执行顺序也不确定,完全依赖于网络传输结果,谁先到执行谁。
defer
当浏览器遇到带有 defer 属性的 script 时,获取该脚本的网络请求也是异步的,不会阻塞浏览器解析 HTML,一旦网络请求回来之后,如果此时 HTML 还没有解析完,浏览器不会暂停解析并执行 JS 代码,而是等待 HTML 解析完毕再执行 JS 代码,图示如下:
如果存在多个 defer script 标签,浏览器(IE9及以下除外)会保证它们按照在 HTML 中出现的顺序执行,不会破坏 JS 脚本之间的依赖关系。
最后,根据上面的分析,不同类型 script 的执行顺序及其是否阻塞解析 HTML 总结如下:
script 标签 | JS 执行顺序 | 是否阻塞解析 HTML |
<script> | 在 HTML 中的顺序 | 阻塞 |
<script async> | 网络请求返回顺序 | 可能阻塞,也可能不阻塞 |
<script defer> | 在 HTML 中的顺序 | 不阻塞 |
注意 defer还是会影响dom的渲染过程的,但是不会阻塞dom tree生成的过程,如下代码,页面依旧无法生成出来,因为在DomContentLoaded事件之前就被卡住了