浏览器
架构
单进程 -> 多进程 -> SOA
在进行渲染进程的准备上,默认每个标签对应一个渲染进程。但如果从一个页面打开了另一个新页面,而新页面和当前页面属于同一站点(根域名相同、协议相同)的话,那么新页面会复用父页面的渲染进程
渲染流程
DOM树 + CSS规则树 -> 渲染树 -> 布局(Layout) -> 绘制(Painting) -> 合成
即关键渲染路径JavaScript/CSS --> Style --> Layout --> Paint --> Composite
- 构建DOM树:将下载的 HTML 经由解析器解析,最终输出树状结构的 DOM
- 样式计算:计算出 DOM 节点中每个元素的具体样式
- 将CSS转为浏览器能够理解的结构,也就是 styleSheets
- 标准化样式表中的属性值
- 根据styleSheets 将样式应用到DOM上
- 布局阶段:计算出 DOM 树中可见元素的几何位置
- 分层:为实现注入3D变换、Z轴排序等效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树
- 拥有层叠上下文属性的元素会被提升为单独的一层
- 需要剪裁(clip)的地方也会被创建为图层
- 图层绘制:把一个图层的绘制拆分成很多小的绘制指令,然后再把这些指令按照顺序组成一个待绘制列表
- 栅格化:将图层划分为图块(tile),合成线程会按照视口附近的图块来优先生成位图
- 合成与显示:有图块都被光栅化,合成线程就会生成一个绘制图块的命令——“DrawQuad”,浏览器进程里面有一个叫 viz 的组件,用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,将其页面内容绘制到显存中,最后再将内存显示在屏幕上
合成操作是在合成线程上完成的,这也就意味着在执行合成操作时,是不会影响到主线程执行的。这就是为什么经常主线程卡住了,但是 CSS 动画依然能执行的原因
- 请求发出去之后,到提交数据阶段,这时页面展示出来的还是之前页面的内容
- 提交数据之后渲染进程会创建一个空白页面,称为解析白屏,并等待 CSS 文件和 JavaScript 文件的加载完成,生成 CSSOM 和 DOM,然后合成布局树,最后还要经过一系列的步骤准备首次渲染
- 首次渲染完成之后,就开始进入完整页面的生成阶段了,然后页面会一点点被绘制出来
相关概念
- 重排:DOM元素的大小和位置(几何位置属性)发生变化的时候,会触发布局。在布局阶段中,计算元素在设备视口(viewport)内的确切位置和大小,计算这些值的过程称为回流、布局或重排(Reflow)
- 重绘:当直接修改元素的样式属性,重绘省去了布局和分层阶段
- 直接合成:更改一个既不要布局也不要绘制的属性,比如CSS 的 transform 来实现动画效果,要事先定义好will-change:transform; ,才能避免layout 和paint
对于在交互过程中的性能优化,一个大的原则就是让单个帧的生成速度变快:
- 减少 JavaScript 脚本执行时间
- 分解执行过久的函数
- 采用 Web Workers,但 Web Workers 中没有 DOM、CSSOM 环境
- 避免强制同步布局
- 强制同步的话,也就是我们说的DOM读写不分离,就会让DOM立刻改变,因为他需要读取DOM的改变
- 避免布局抖动
- 和强制同步布局一样,都是尽量不要在修改 DOM 结构时再去查询一些相关值
- 合理利用 CSS 合成动画
- 合成动画是直接在合成线程上执行的,这和在主线程上执行的布局、绘制等操作不同
- 避免频繁的垃圾回收 STW
垃圾回收
栈内存回收
堆内存回收
跟JVM的垃圾回收理论有许多相通之处
V8 中也会把堆分为新生代和老生代两个区域,两个区域分别由副、主垃圾回收器进行回收
- 新生代中用 Scavenge 算法来处理,即标记复制算法
- 主垃圾回收器是采用标记清除算法
为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,让垃圾回收阶段与正常JS执行交替执行
代码编译与执行
- 解释器 Ignition 会根据 AST 生成字节码,并解释执行字节码
- 在 Ignition 执行字节码的过程中,如果发现有热点代码,,那么后台的编译器 TurboFan 就会把该段热点的字节码编译为高效的机器码,即 JIT
页面循环系统
while(running) {
const task = getTaskFromQueue();
task.run();
}
宏任务微任务
宿主发起的任务称为宏观任务,时间粒度比较大,执行的时间间隔是不能精确控制的
- 渲染事件(如解析 DOM、计算布局、绘制)
- 用户交互事件(如鼠标点击、滚动页面、放大缩小等)
- JavaScript 脚本执行事件
- 网络请求完成、文件读写完成事件
JavaScript 引擎发起的任务称为微观任务
- MutationObserver 监控
- 使用 Promise
有一个独立的线程执行事件循环不断消费宏观任务队列的任务,Promise 还会产生异步代码,JavaScript 必须保证这些异步代码在一个宏观任务中完成,因此,每个宏观任务中又包含了一个微观任务队列
执行主线程宏任务-->执行所有的微任务,直到清空微任务的队列-->执行一个宏任务--->执行微任务(因为这时候可能又产生了新的微任务)--一个宏-->所有微,然后一直循环往复直到结束
为了实现对任务的优先级处理,就可以把一些优先级高的任务插入到微任务队列
为了规避某些任务执行过长导致整体阻塞的问题,JavaScript 可以通过回调功能也就是让任务滞后执行来强制规避阻塞
setTimeout
while(running) {
const task = getTaskFromQueue();
task.run();
runDelayTask();
}
延迟任务的实现就是在宏任务执行完成之后,从延迟任务队列取任务进行执行,所以这就会导致如果宏任务里面的任务执行比较久,延迟任务也会相应的延迟
setTimeout 存在嵌套调用被嵌套调用 5 次以上的情况,系统会判断该函数方法被阻塞了,那么系统会设置后续的调用最短时间间隔为 4 毫秒
function cb() { setTimeout(cb, 0); }
setTimeout(cb, 0);
为了优化后台页面的加载损耗以及降低耗电量,未激活的页面,setTimeout 执行最小间隔是 1000 毫秒
大部分浏览器都是用32位整数存储延迟值,所以当大于 2147483647 时就会溢出变成 0
虚拟DOM
- 创建阶段:JSX 和基础数据创建出来虚拟 DOM,由虚拟 DOM 树创建出真实 DOM 树,真实的 DOM 树生成完后,再触发渲染流水线往屏幕输出页面
- 更新阶段:如果数据发生了改变,那么就根据新的数据创建一个新的虚拟 DOM 树;然后比较两个树,找出变化的地方,并把变化的地方一次性更新到真实的 DOM 树上;最后渲染引擎更新渲染流水线,并生成新的页面
React 的核心算法是 reconciliation,新的算法称为 Fiber reconciler,其利用了协程的特性,可以让出调度,避免 diff 算法过久造成页面卡顿
虚拟 DOM 主要是一个抽象层,用以描述视图,除了可以解决频繁更新真实 DOM 带来的性能问题,还可以作为一种中间描述,实现跨端