从一个假死页面引发的思考: 作为前端开发,除了要攻克页面难点,也要有更深的自我目标,性能优化是自我提升中很重要的一环; 在前端开发中,会偶遇到页面假死的现象, 是因为当 js 有大量计算时,会造成 UI 阻塞,出现界面卡顿、掉帧等情况,严重时会出现页面卡死的情况;
进程与线程的区别
线程具有许多传统进程所具有的特征,故又称为轻型进程 (Light—Weight Process) 或进程元;而把传统的进程称为重型进程 (Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。
◦根本区别:进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位;
◦资源开销:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
◦包含关系:如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
◦内存分配:同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的;
◦影响关系:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
◦执行过程:每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行;
=> 回到阻塞原因分析
浏览器有 GUI 渲染线程与 JS 引擎线程,这两个线程是互斥的关系,当 JS 引擎执行时 GUI 线程会被挂起(相当于被冻结了),GUI 更新会被保存在一个队列中,等到 JS 引擎空闲时,立即被执行。js 引擎不是每次在执行更新 dom 语句时,都会停下来等 Gui 渲染引擎更新完 dom 再执行后面的 js 代码; 详见 GUI 渲染线程与 JS 引擎线程
1、performance Api
录制步骤: 刷新页面加载流水线详情页面,然后找到某个用例技术端,进行结果更新;因此下面的图分 3 个部分,第一个是详情页渲染; 第二段是需要大量计算的用例渲染部分; 第三段是操作结果,数据重新刷新,重新计算和渲染的部分;
其中,FPS 上方显示红色条的时候,意味着帧率特别低,用户体验特别不好。一般来说,绿色条越高,FPS 越高。 分析性能图标第一眼看 FPS;性能面板底部,图形图表的色彩越多,意味着 CPU 性能已经达到极限。当我们看到 CPU 长时间处于最大值状态,就需要考虑怎样去优化
接口的监控 + 详情页渲染 + 用例渲染;
上面的 Group 面板非常有用。我们可以很清晰明了得分析按照活动,目录,域,子域,URL 和 Frame 进行分组的前端性能;
Bottom-Up: 是 The Heavy (Bottom Up) view is available in the Bottom-Up tab,类似事件冒泡;
Call Tree:是 And the Tree (Top Down) view is available in the Call Tree tab,类似事件捕获;
更新结果,页面重新绘制,计算;
黄色 (Scripting):JavaScript 执行
紫色 (Rendering):样式计算和布局,即重排
蓝色 (Loading):网络通信和 HTML 解析
绿色 (Painting):重绘
灰色 (System):其它事件花费的时间
白色 (Idle):空闲时间: 可能是同时请求了很多接口,promise.all 需要等待所有接口返回成功后才会渲染页面,idle 时间变长了很多,浏览器一直在等待接口全部返回;
用例列表和添加按钮部分产生了偏移, 红色 Layout Shift;
2、lightHouse 分析数据;分数很低;
3、performace monitor: 加载过程中,CPU 飙 80%;
主要可改善指标如下:
相关名词解析:https://web.dev/metrics/;
指标 | 指标解析 |
---|---|
Self Time | Self Time 代表函数本身执行消耗时间 |
Total Time | Total Time 则是函数本身消耗再加上在调用它的函数中消耗的总时间 |
Activity | 浏览器活动的意思 |
DOM GC | DOM 垃圾回收 |
Timer Fried | 销毁计时器 |
XMR Load | 异步加载对象加载 |
Major GC | 清理年老区(Tenured space) |
Minor GC | 每次 Minor GC 只会清理年轻代 |
Run Microtasks | 运行微服务 |
Recalculate Style | |
HitTest | https://www.jianshu.com/p/f6aff12fc08b |
DCLDomContentloaded | 当 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,无需等待样式表、图像和子框架的完成加载. |
SI (Speed Index) | 指标用于显示页面可见部分的显示速度, 单位是时间, |
FPFirst Paint 首次绘制 | 首次绘制(FP)这个指标用于记录页面第一次绘制像素的时间,如显示页面背景色。FP 不包含默认背景绘制,但包含非默认的背景绘制。 |
FCPFirst contentful paint | 首次内容绘制 (FCP): LCP 是指页面开始加载到最大文本块内容或图片显示在页面中的时间。如果 FP 及 FCP 两指标在 2 秒内完成的话我们的页面就算体验优秀。 |
LCPLargest contentful paint | 最大内容绘制 (LCP): 用于记录视窗内最大的元素绘制的时间,该时间会随着页面渲染变化而变化,因为页面中的最大元素在渲染过程中可能会发生改变,另外该指标会在用户第一次交互后停止记录。官方推荐的时间区间,在 2.5 秒内表示体验优秀 |
FID(First input delay) | 首次输入延迟,FID(First Input Delay),记录在 FCP 和 TTI 之间用户首次与页面交互时响应的延迟 |
TTITime to Interactive | 可交互时间 (TTI) 首次可交互时间,TTI(Time to Interactive)。这个指标计算过程略微复杂,它需要满足以下几个条件:1、从 FCP 指标后开始计算 2、持续 5 秒内无长任务(执行时间超过 50 ms)且无两个以上正在进行中的 GET 请求往前回溯至 5 秒前的最后一个长任务结束的时间 3、对于用户交互(比如点击事件),推荐的响应时间是 100ms 以内。那么为了达成这个目标,推荐在空闲时间里执行任务不超过 50ms( W3C 也有这样的标准规定),这样能在用户无感知的情况下响应用户的交互,否则就会造成延迟感。 |
TBTTotal blocking Time | 总阻塞时间 (TBT) 阻塞总时间,TBT(Total Blocking Time),记录在 FCP 到 TTI 之间所有长任务的阻塞时间总和。 |
CLSCumulative Layout Shift | 记录了页面上非预期的位移波动。页面渲染过程中突然插入一张巨大的图片或者说点击了某个按钮突然动态插入了一块内容等等相当影响用户体验的网站。这个指标就是为这种情况而生的,计算方式为:位移影响的面积 * 位移距离。 |
上述的指标太多了,哪些才是核心指标呢? Google 在 2020 年五月提出了网站用户体验的三大核心指标
1、Largest Contentful Paint (LCP)
LCP:最大内容绘制 , 代表了页面的速度指标,虽然还存在其他的一些体现速度的指标,但 LCP 能体现的东西更多一些。一是指标实时更新,数据更精确,二是代表着页面最大元素的渲染时间,通常来说页面中最大元素的快速载入能让用户感觉性能还挺好。
最大元素:
◦ 标签
◦ 在 svg 中的 image 标签
◦ video 标签
◦CSS background url() 加载的图片
◦包含内联或文本的块级元素
影响元素:
◦服务端响应时间 ---- 接口性能
◦Javascript 和 CSS 引起的渲染卡顿 ✨✨✨---webWork.js 计算问题
◦资源加载时间✨✨✨---CDN
◦客户端渲染✨✨---SSR
2、First Input Delay (FID):
FID:首次输入延迟, 代表了页面的交互体验指标,就是看用户交互事件触发到页面响应中间耗时多少,如果其中有长任务发生的话那么势必会造成响应时间变长,推荐响应用户交互在 100ms 以内。
影响因素
◦减少第三方代码的影响
◦减少 Javascript 的执行时间
◦最小化主线程工作
◦减小请求数量和请求文件大小
3、Cumulative Layout Shift (CLS)
CLS 代表了页面的稳定指标,它能衡量页面是否排版稳定。尤其在手机上这个指标更为重要,因为手机屏幕挺小,CLS 值一大的话会让用户觉得页面体验做的很差。CLS 的分数在 0.1 或以下,则为 Good。
影响因素: 通过下面的原则避免非预期布局移动:
◦图片或视屏元素有大小属性,或者给他们保留一个空间大小,设置 width、height,或者使用 unsized-media feature policy 。
◦不要在一个已存在的元素上面插入内容,除了相应用户输入。
◦使用 animation 或 transition 而不是直接触发布局改变。
上述讲了网络请求,渲染过程,那下面我们看下一个简单的页面的整个过程是怎么样的;
1. 导航阶段,请求 HTML 数据阶段:
◦该任务的第一个子过程就是 Send request,该过程表示网络请求已被发送。然后该任务进入了等待状态。
◦接着由网络进程负责下载资源,当接收到响应头的时候,该任务便执行 Receive Respone 过程,该过程表示接收到 HTTP 的响应头了。
◦接着执行 DOM 事件:pagehide、visibilitychange 和 unload 等事件,如果你注册了这些事件的回调函数,那么这些回调函数会依次在该任务中被调用。
◦些事件被处理完成之后,那么接下来就接收 HTML 数据了,这体现在了 Recive Data 过程,Recive Data 过程表示请求的数据已被接收,如果 HTML 数据过多,会存在多个 Receive Data 过程。
◦等到所有的数据都接收完成之后,渲染进程会触发另外一个任务,该任务主要执行 Finish load 过程,该过程表示网络请求已经完成。
2. 解析 HTML 数据阶段
其中一个主要的过程是 HTMLParser:解析的上个阶段接收到的 HTML 数据。
◦在 ParserHTML 的过程中,如果解析到了 script 标签,那么便进入了脚本执行过程,也就是图中的 Evalute Script。
◦DOM 生成完成之后,会触发相关的 DOM 事件,比如:典型的 DOMContentLoaded,还有 readyStateChanged。
◦DOM 生成之后,ParserHTML 过程继续计算样式表,也就是 Reculate Style,这就是生成 CSSOM 的过程
3. 生成可显示位图阶段
该阶段需要经历布局 (Layout)、分层、绘制、合成等一系列操作:
在生成完了 DOM 和 CSSOM 之后,渲染主线程首先执行了一些 DOM 事件,诸如 readyStateChange、load、pageshow。
总而言之,大致过程如下:
◦首先执行布局,这个过程对应图中的 Layout。
◦然后更新层树 (LayerTree),这个过程对应图中的 Update LayerTree。
◦有了层树之后,就需要为层树中的每一层准备绘制列表了,这个过程就称为 Paint。
◦准备每层的绘制列表之后,就需要利用绘制列表来生成相应图层的位图了,这个过程对应图中的 Composite Layers。走到了这步,主线程的任务就完成了。
接下来主线程会将合成的任务完全教给合成线程来执行,下面是具体的过程,也可以对照着 Composite、Raster 和 GPU 这三个指标来分析
对于前端应用来说,网络耗时、页面加载耗时、脚本执行耗时、渲染耗时等耗时情况会影响用户的等待时长,
而 CPU 占用、内存占用、本地缓存占用等则可能会导致页面卡顿甚至卡死。
因此,性能优化可以分别从耗时和资源占用两方面来解决,也可以理解成时间和空间两个方面。
时间角度(耗时)
在时间角度进行优化主要是减少耗时,浏览器在页面加载的过程中,主要会进行以下的步骤:
耗时优化的着手点:
1、网络请求优化: 目标在于减少网络资源的请求和加载耗时,如果考虑 HTTP 请求过程,显然我们可以从几个角度来进行优化:
对于请求链路,核心的方案常常包括使用缓存,比如 DNS 缓存、CDN 缓存、HTTP 缓存、后台缓存等等,前端的话还可以考虑使用 Service Worker、PWA 等技术。使用缓存并非万能药,很多使用由于缓存的存在,我们在功能更新修复的时候还需要考虑缓存的情况。除此之外,还可以考虑使用 HTTP/2、HTTP/3 等提升资源请求速度,以及对多个请求进行合并,减少通信次数;对请求进行域名拆分,提升并发请求数量。
数据大小则主要考对请求资源进行合理的拆分(CSS、Javascript 脚本、图片/音频/视频等)和压缩,减少请求资源的体积,比如使用 Tree-shaking、代码分割、移除用不上的依赖项等。在请求资源返回后,浏览器会进行解析和加载,这个过程会影响页面的可见时间,通过对首屏加载的优化,可有效地提升用户体验。
2、首屏加载优化: 首屏加载优化核心点在于两部分:
◦将页面内容尽快地展示给用户,减少页面白屏时间。
◦将用户可操作的时间尽量提前,避免用户无法操作的卡顿体验。
我们的页面也需要在客户端进行展示,此时可充分利用客户端的优势:
◦配合客户端进行资源预请求和预加载,比如使用预热 Web 容器配合客户端将资源和数据进行离线,可用于下一次页面的快速渲染使用秒看技术,通过生成预览图片的方式提前将页面内容提供给用户除了首屏渲染以外,用户在浏览器页面过程中,也会触发页面的二次运算和渲染,此时需要进行渲染过程的优化
3、渲染过程优化:渲染过程的优化可以理解成首屏加载完成后,用户的操作交互触发的二次渲染。主要思路是减少用户的操作等待时间,以及通过将页面渲染帧率保持在 60FPS 左右,提升页面交互和渲染的流畅度。包括但不限于以下方案:
◦使用资源预加载,提升空闲时间的资源利用率--preload
◦减少/合并 DOM 操作,减少浏览器渲染过程中的计算耗时
◦使用离屏渲染,在页面不可见的地方提前进行渲染(比如 Canvas 离屏渲染)
◦通过合理使用浏览器 GPU 能力,提升浏览器渲染效率(比如使用 css transform 代替 Canvas 缩放绘制)
以上这些,是对常见的 Web 页面渲染优化方案。对于运算逻辑复杂、计算量较大的业务逻辑,我们还需要进行计算/逻辑运行的提速。
什么是重绘和回流
回流比重绘更加消耗性能,付出的代价更高。
◦recalculate style (style):结合 DOM 和 CSSOM,确定各元素应用的 CSS 规则
◦layout:重新计算各元素位置来布局页面,也称 reflow
◦update layer tree (layer):更新渲染树
◦paint:绘制各个图层
◦composite layers (composite):把各个图层合成为完整页面
核心: 布局会不会变! 回流一定会导致重绘,重绘不一定导致回流
1.重绘:简单来说就是重新绘画,当给一个元素更换颜色、更换背景,虽然不会影响页面布局,但是颜色或背景变了,就会重新渲染页面,这就是重绘。
2.回流: 当增加或删除 dom 节点,或者给元素修改宽高时,会改变页面布局,那么就会重新构造 dom 树然后再次进行渲染,这就是回流。
哪些会引起回流呢?
总结
重绘不会引起 dom 结构和页面布局的变化,只是样式的变化,有重绘不一定有回流。
回流则是会引起 dom 结构和页面布局的变化,有回流就一定有重绘。
怎么进行优化或减少?
1.计算/逻辑运行提速
通过将 Javscript 大任务进行拆解,结合异步任务的管理,避免出现长时间计算导致页面卡顿的情况
将耗时长且非关键逻辑的计算拆离,比如使用 Web Worker
通过使用运行效率更高的方式,减少计算耗时,比如使用 Webassembly
通过将计算过程提前,减少计算等待时长,比如使用 AOT 技术
通过使用更优的算法或是存储结构,提升计算效率,比如 VSCode 使用红黑树优化文本缓冲区的计算
通过将计算结果缓存的方式,减少运算次数
空间角度(资源)
在做性能优化的时候,其实很多情况下会依赖时间换空间、空间换时间等方式,只能根据自己项目的实际情况做出取舍,选择相对合适的一种方案去进行优化。
资源占用常见的优化方式包括:
1.合理使用缓存,不滥用用户的缓存资源(比如浏览器缓存、IndexDB),及时进行缓存清理;
2.避免存在内存泄露,比如尽量避免全局变量的使用、及时解除引用等
3.避免复杂/异常的递归调用,导致调用栈的溢出
4.通过使用数据结构享元的方式,减少对象的创建,从而减少内存占用
注意:new Worker(xxx.js)里的 xxx.js 必须和 HTML 文件同源必须在 http/https 协议下访问 HTML 文件,不能用文件协议(类似 file:///E:/wamp64/www/t.html 这种
因此我用 mamp 搭建的一个环境指向测试的文件夹;本地配置http://www.performance.com/进行测试;
主进程代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>web-worker小测试</title>
</head>
<body>
<!-- <script type="text/javascript" src="worker.js"></script> -->
<script>
// const work = new Worker('worker.js');
// work.postMessage('hello worker')
// work.onmessage = (e) => {
// console.log(`主进程收到了子进程发出的信息:${e.data}`);
// // 主进程收到了子进程发出的信息:你好,我是子进程!
// work.terminate();
// };
let cnt = 0;
for (let i = 0; i < 900000000; i += 1) {
cnt += 1;
}
console.log(cnt);
</script>
</body>
</html
worker.js
onmessage = (e) => {
console.log(`收到了主进程发出的信息:${e.data}`);
let cnt = 0;
for (let i = 0; i < 900000000; i += 1) {
cnt += 1;
}
console.log(cnt);
//收到了主进程发出的信息:hello worker
postMessage(`你好,我是子进程!${cnt}`);
}
在 vue.js 中的使用过程, 除了配置,其他同上;
1、npm install vue-worker
2、chainWebpack中进行配置:
config.module
.rule('worker')
.test(/\.worker\.js$/)
.use('worker-loader')
.loader('worker-loader')
.options({
inline: 'fallback',
});
config.module.rule('js').exclude.add(/\.worker\.js$/);
更新后,当前页面代码段中,计算 300ms+ 提升,效果一般,主要还是 dom 节点过多引起的;
Web Worker 的限制
1.在 Worker 线程的运行环境中没有 window 全局对象,也无法访问 DOM 对象
2.Worker 中只能获取到部分浏览器提供的 API,如定时器、navigator、location、XMLHttpRequest 等
3.由于可以获取 XMLHttpRequest 对象,可以在 Worker 线程中执行 ajax 请求
4.每个线程运行在完全独立的环境中,需要通过 postMessage、 message 事件机制来实现的线程之间的通信
计算的运算时长 - 通信时长 > 50ms,推荐使用 Web Worker
1.生产环境关闭 productionSourceMap、css sourceMap
SourceMap 就是当页面出现某些错误,能够定位到具体的某一行代码,SourceMap 就是帮你建立这个映射关系的,方便代码调试;
关闭 css sourceMap 以后,js 文件从 55M 降低到 12M, 文件从 650 个减少至 325 个;
分析大文件, 目前项目的情况: js 总计 12.1M (325 个项目); css 总计 11.1M (249 个项目),
◦element-ui 年 1.85M--涉及改造的有点多,暂时不更新;
◦Ecahrts 2.55M --cdn 引入
◦handsontable 3.34M---在线引入后,由于 handsontable-vue 还会安装 handsontable, 因此需要把 handsontable-vue 也使用在线的方式,cdh 资源可在https://www.jsdelivr.com/package/npm/@handsontable/vue中找到;
◦vue-json-editor 1.24M--没找到在线 js 资源,建议组件引入的时候,直接使用 json-editor;
◦将资源进行 cdn 方式引入;使用 CDN 引入以后,js 总计 9.3M(326 个项目), css 总计 10.9(249 个项目)
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
// 正式环境不打包公共js
let externals = {};
// 储存cdn的文件
const cdn = {
css: [
'https://cdn.jsdelivr.net/npm/handsontable/dist/handsontable.full.min.css',
],
js: [],
};
// 正式环境才需要
// if (isProduction) {
externals = { // 排除打包的js
vue: 'Vue',
echarts: 'echarts',
vueHandsontable: 'vue-handsontable',
};
cdn.js = [
'https://cdn.bootcdn.net/ajax/libs/vue/2.6.11/vue.min.js', // vuejs
'https://cdn.jsdelivr.net/npm/echarts@5.4.0/dist/echarts.min.js',
'https://cdn.jsdelivr.net/npm/handsontable/dist/handsontable.full.min.js',
'https://cdn.jsdelivr.net/npm/@handsontable/vue@12.1.3/dist/vue-handsontable.min.js',
];
// }
**************
configureWebpack: {
// 常用的公共js 排除掉,不打包 而是在index添加cdn,
externals,
plugins: [
new BundleAnalyzerPlugin(), // 分析打包大小使用默认配置
],
},
*************
chainWebpack: (config) => {
// 注入cdn变量 (打包时会执行)
config.plugin('html').tap((args) => {
args[0].cdn = cdn; // 配置cdn给插件
return args;
});
}
使用 CDN 引入以后,js 总计 9.3M(326 个项目)=> 3.1M, css 总计 10.9(249 个项目)--没有发生变化
nginx 的配置, 这篇文章很不错https://www.cnblogs.com/wwjj4811/p/15847916.html, 这块我没线上测试;不过应该没啥问题;
1、安装 compression-webpack-plugin(vue2--npm install --save-dev compression-webpack-plugin@5.0.2)
2、const CompressionPlugin = require('compression-webpack-plugin');
3、chainWebpack中配置
if (isProduction) {
config.plugin('compressionPlugin').use(new CompressionPlugin({
test: /\.(js)$/, // 匹配文件名
threshold: 10240, // 对超过10k的数据压缩
minRatio: 0.8,
deleteOriginalAssets: true, // 删除源文件
}));
}
4、nginx中增加配置后重启
gzip on; #开启gzip功能
gzip_types *; #压缩源文件类型,根据具体的访问资源类型设定
gzip_comp_level 6; #gzip压缩级别
gzip_min_length 1024; #进行压缩响应页面的最小长度,content-length
gzip_buffers 4 16K; #缓存空间大小
gzip_http_version 1.1; #指定压缩响应所需要的最低HTTP请求版本
gzip_vary on; #往头信息中添加压缩标识
gzip_disable "MSIE [1-6]\."; #对IE6以下的版本都不进行压缩
gzip_proxied off; #nginx作为反向代理压缩服务端返回数据的条件
tree shaking:通常用于描述移除 JavaScript 上下文中的未引用代码 (dead-code)。它依赖于 ES2015 模块系统中的静态结构特性,例如 import 和 export,是 webpack 4 版本,扩展的这个检测能力;
使用 tree-shaking 以后,js 总计 3.1M(326 个项目)=> 2.9M(315 个项目), css 总计 10.9(249 个项目)--384K(15 个项目)
作者:京东零售 苏文静
来源:京东云开发者社区 转载请注明来源