1.背景
老板:某宝的官网1m内就打开了,为什么我们的官网每次都要转10来秒呀,小陈老陈你们过来看看
小陈:打开页面、F12 查看、console没有报错,emmmm肯定是网络不好😅
老陈:F12都打开了,不如我们使用WEB性能瓶颈定位大法【Performance】进行望、闻、问、切
2.目标
目标一: 了解Performance的重要结构组成
目标二:Performance中 理解回流重绘和事件循环机制
目标三: 定位web性能瓶颈,体验解决过程
3.认识Performance
3.1启动
启动: 使用谷歌浏览器,按下f12开启开发者工具,选择Performance
方式一: 点击圆形按钮/Ctrl + E, 记录当前到stop时的操作,并生成可视化报告
方式一: 点击刷新按钮/Ctrl + Shift + E, 记录重新渲染到stop时的操作,并生成可视化报告
3.2结构组成
通过可视化报告图,我们发现是由:控制面板、概览面板、主线程火焰图、详细面板组成。
可视化报告
控制面板
控制面板
【Disable JavaScript samples】:开启会使工具忽略记录 JS 的调用栈;
【Enable advanced paint instrumentation】:开启会详细记录某些渲染事件的细节;
【Network】:模拟不同的网络环境,测试性能瓶颈;
【CPU】:模拟不同的计算机资源环境,测试性能瓶颈;
概览面板
概览面板
【FPS】
帧率,就是1000ms内有多少帧,一般24帧算动画,40帧算流畅,但优质的web体验应该是60帧,也就是16.7ms~60fsp;
【CPU面积图】:横轴为时间,纵轴为CPU使用率,色块代表不同的事件类型
前置介绍: 色块代表不同的事件类型
- 蓝色:加载(Loading)事件
- 黄色:脚本运算(Scripting)事件
- 紫色:渲染(Rendering)事件
- 绿色:绘制(Painting)事件
- 灰色:其他(Other)
- 闲置:浏览器空闲
举例来说,示意图的第一列:总 CPU 使用率为 18,加载事件(蓝色)和脚本运算事件(黄色)各占了一半。随着时间增加,脚本运算事件的 CPU 使用率逐渐增加,而加载事件的使用率在 600ms 左右降为 0;另一方面渲染事件(紫色)的使用率先升后降,在 1100ms 左右将为 0。整张图可以清晰地体现哪个时间段什么事件占据 CPU 多少比例的使用率。
主线程火焰图
主线程火焰图
【Frames】: 帧线程时序图,记录每一帧画面和消耗的时间(两条灰色线之间为一帧)
【Main】:倒置事件火焰图,横轴为时间,纵轴为事件深度;
最上层是父级函数或应用,越往下则调用栈越浅,最底层的一小格(如果时间维度拉得不够长,看起来像是一小竖线)表示的是函数调用栈顶层。默认情况下火焰图会记录已执行 JS 程序调用栈中的每层函数(精确到单个函数的粒度),非常详细。而开启「Disable JS Samples」后,火焰图只会精确到事件级别(调用某个 JS 文件中的函数是一个事件),忽略该事件下的所有函数调用栈。
前置介绍:事件介绍(Main火焰图和详细面板中使用)
| Parse HTML | Chrome 执行其HTML解析算法 |
|---|---|
| Event | JavaScript事件 (例如【mousemove】) |
| Layout | 页面布局已被执行 |
| Recalculate style | Chrome 重新计算了元素样式 |
| Paint | 合成图层被绘制到显示画面的一个区域 |
| Composite | Chrome的渲染引擎合成了图像曾层 |
详细面板
详细面板
Chrome根据所选时间片,分析合成出事件占用详情图。
4.demo演示
4.1前置条件及示例代码
前置条件:根据前面的知识,我们已经了解Performance的
启动和重要组成部分(目标一已完成)。后续目标: 通过具体案例,了解回流重绘、事件循环原理,根据各项指标分析定位性能瓶颈。
4.2回流重绘
页面渲染过程
页面渲染过程
- 解析HTML,构建 DOM 树
- 解析 CSS ,生成 CSS 规则树
- 合并 DOM 树和 CSS 规则,生成 render 树
- 布局 render 树( Layout / reflow ),负责各元素尺寸、位置的计算
- 绘制 render 树( paint ),绘制页面像素信息
- 浏览器会将各层的信息发送给 GPU,GPU 会将各层合成( composite ),显示在屏幕上
回流重绘概念及触发机制
回流重绘概念及触发机制
回流: 回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流
重绘: 触发回流一定会触发重绘,可以把页面理解为一个黑板,黑板上有一朵画好的小花。现在我们要把这朵从左边移到了右边,那我们要先确定好右边的具体位置,画好形状(回流),再画上它原有的颜色(重绘)
得出结论: Layout是回流的标志,会触发回流也会触发重绘,Paint是重绘的标志
performance中了解回流重绘
运行代码: 使用上面代码运行
paint-demo分析: 使用了requestAnimation捕获每一帧,发现:1帧内10次Layout,但只触发1次Paint
结论:回流确实会引起重绘,但会汇总一帧内的所有Layout之后进行update Layout Tree,再触发Paint
\
渲染瓶颈定位
- 模拟场景
- 修改app.js,初始化为200个元素;初始化默认方案为3;
- 开启Performance
- 点击"方案*"按钮, 每8m切换一次方案
(function(window) {
var app = {},
...
minimum = 200
app.scheme = 3;
...
})
- 可视化报告图
- 根据cpu、main火焰图、详细面板的数据,我们可以确认方案性能排名为:方案3、方案2、方案1
- cpu暂用情况: 方案1 > 方案2 -> 方案3
- 主要色块为: 紫色 渲染事件(Rendering 事件),黄色 脚本运算(Scripting 事件)
- 分析
- 方案一,从Main火焰图入手,我们发现蓝色框圈起来的,里面有一些色块标红,点击查看
现象:
app.js中的121行执行了requestAnimation,一个任务队列耗时41.36ms
调用了app.update方案,触发了200次Layout,这谁顶得住emmmm
上代码:
每次修改top后,都调用了m.offsetTop获取位置,m.offsetTop会强制触发浏览器位置计算
app.update = function (timestamp) {
for (var i = 0; i < app.count; i++) {
...
var pos = m.classList.contains('down') ?
m.offsetTop + distance : m.offsetTop - distance;
if (pos < 0) pos = 0;
if (pos > maxHeight) pos = maxHeight;
m.style.top = pos + 'px';
if (m.offsetTop === 0) {
m.classList.remove('up');
m.classList.add('down');
}
if (m.offsetTop === maxHeight) {
m.classList.remove('down');
m.classList.add('up');
}
...
}
}
- 方案二
修改: 使用变量提前存储好位置,不通过m.offsetTop而是使用该变量做判断
现象:
200次的Layout变成了1次
Task的时间由41.36ms降低为14.41ms
详细面板中,Rendering渲染事件占比明显降低
上代码:
app.update = function (timestamp) {
for (var i = 0; i < app.count; i++) {
...
var pos = parseInt(m.style.top.slice(0, m.style.top.indexOf('px')));
m.classList.contains('down') ? pos += distance : pos -= distance;
if (pos < 0) pos = 0;
if (pos > maxHeight) pos = maxHeight;
m.style.top = pos + 'px';
if (pos === 0) {
m.classList.remove('up');
m.classList.add('down');
}
if (pos === maxHeight) {
m.classList.remove('down');
m.classList.add('up');
}
...
}
}
- 方案三
修改: 使用transform 代替 top,开启GPU加速
现象:
Task的时间由14.41ms降低为2.71ms
Experience的整个Layout shift 消失
上代码:
app.update = function (timestamp) {
for (var i = 0; i < app.count; i++) {
...
if (pos > maxHeight) pos = maxHeight;
m.style.transform=`translateY(${pos}px)`;
...
}
}
4.3事件循环
概念理解
首先,JavaScript是一门单线程的语言,意味着同一时间内只能做一件事,但是这并不意味着单线程就是阻塞,而实现单线程非阻塞的方法就是事件循环
同步任务与异步任务
在JavaScript中,所有的任务都可以分为
- 同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行
- 异步任务:异步执行的任务,比如
ajax网络请求,setTimeout定时函数等
同步任务与异步任务的运行流程图如下:
从上面我们可以看到,同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。上述过程的不断重复旧事件循环
异步任务:宏任务与微任务
微任务
一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前
常见的微任务有:
- Promise.then
- MutaionObserver
- Object.observe(已废弃;Proxy 对象替代)
- process.nextTick(Node.js)
宏任务
宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合
常见的宏任务有:
- script (可以理解为外层同步代码)
- setTimeout/setInterval
- UI rendering/UI事件
- postMessage、MessageChannel
- setImmediate、I/O(Node.js)
事件循环: 执行一个宏任务 -> 执行里面的所有微任务 ->里面有宏任务(开启新的事件循环),无宏任务(本轮宏任务结束)
代码示例
代码地址:task-demo
function Microtasks() {
console.log('微任务')
}
function MacroTask() {
console.log('宏任务')
}
function fn3() {
new Promise((r) => {
console.log('fn3')
r()
}).then(_ => {
Microtasks()
})
}
function fn2() {
setTimeout(() => {
MacroTask()
})
fn3()
}
function fn1() {
console.log('fn1')
fn2()
}
function clickDom () {
// changeHeight()
fn1()
}
clickDom()
输出结果:fn1 -> fn3 -> 微任务 -> 宏任务
- clickDom 同步触发 fn1
- fn1 打印 'fn1', 同步触发 fn2
- fn2 挂起 异步任务-宏任务(MacroTask),同步触发fn3
- fn3 同步打印 'fn3', 异步挂起-微任务(Microtasks)
- 主线程,同步任务结束
- 检测本轮中是否有微任务:有Microtasks,执行Microtasks(), 打印 '微任务'
- 本轮中微任务已结束,检测是否有宏任务,有MacroTask,执行MacroTask(),开启新的事件循环,本轮事件循环结束
- 打印 '宏任务'
Performance中了解事件循环
区块介绍
【区块1】:运行task-demo, 点击黄色块
【区块2】:火焰图观察事件循环
【区块3】:事件调用树
【区块4】:打印结果
第一个task
观察现象:
- 事件执行顺序:
- fn1、fn2、挂起注册setimeout、fn3、匿名函数(其实是new Promise(匿名函数))
- 执行microTasks
得出结论:
- 先执行同步任务
- 遇到异步任务注册挂起
- 同步任务执行完后,执行本次task的微任务
第二个Task
观察现象:
- 开启了新的 task
- 和前一个task中间有空余时间差
得出结论:
- 一个task就是一轮宏任务结束,第二个task是宏任务setTimeout开启的
- setTimeout是宏任务,就算不设置延迟时间,
还是有时间丢失,时间颗粒度高的事情,请找微任务
5.课代表总结
注: 以上大多数结论是学习过程的猜想以及找相关文章验证,有什么不对请大家帮忙指出
GPU 硬件加速是指应用 GPU 的图形性能对浏览器中的一些图形操作交给 GPU 来完成,因为 GPU 是专门为处理图形而设计,所以它在速度和能耗上更有效率
GPU 加速通常包括以下几个部分:Canvas2D,布局合成(Layout Compositing), CSS3转换(transitions),CSS3 3D变换(transforms),WebGL和视频(video) 本质上是用transform代替top,用一种“欺骗的方式”触发GPU代替cpu处理图形操作