从一个假死页面引发的思考: 作为前端开发,除了要攻克页面难点,也要有更深的自我目标,性能优化是自我提升中很重要的一环; 在前端开发中,会偶遇到页面假死的现象, 是因为当js有大量计算时,会造成 UI 阻塞,出现界面卡顿、掉帧等情况,严重时会出现页面卡死的情况;
在这里简单穿插概念之进程和线程
•进程: 一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的demo.exe就是一个进程。
•线程: 进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。
•进程与线程的区别
=> 回到阻塞原因分析
浏览器的渲染进程的线程:
浏览器有GUI渲染线程与JS引擎线程,这两个线程是互斥的关系,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中,等到JS引擎空闲时,立即被执行。js引擎不是每次在执行更新dom语句时,都会停下来等Gui渲染引擎更新完dom再执行后面的js代码; 详见GUI渲染线程与JS引擎线程
1.GUI渲染线程: 负责渲染浏览器页面,解析HTML、CSS,构建DOM树、构建CSSOM树、构建渲染树和绘制页面;当界面需要重绘或由于某种操作引发回流时,该线程就会执行。注意:GUI渲染线程和JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
2.JS引擎线程: JS引擎线程也称为JS内核,负责处理Javascript脚本程序,解析Javascript脚本,运行代码;JS引擎线程一直等待着任务队列中任务的到来,然后加以处理,一个Tab页中无论什么时候都只有一个JS引擎线程在运行JS程序;注意:GUI渲染线程与JS引擎线程的互斥关系,所以如果JS执行的时间过长,会造成页面的渲染不连贯,导致页面渲染加载阻塞。
3.事件触发线程:事件触发线程属于浏览器而不是JS引擎,用来控制事件循环;当JS引擎执行代码块如setTimeOut时(也可是来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件触发线程中;当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理;注意:由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行);
4.定时器触发线程: 定时器触发线程即setInterval与setTimeout所在线程;浏览器定时计数器并不是由JS引擎计数的,因为JS引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确性;因此使用单独线程来计时并触发定时器,计时完毕后,添加到事件队列中,等待JS引擎空闲后执行,所以定时器中的任务在设定的时间点不一定能够准时执行,定时器只是在指定时间点将任务添加到事件队列中;注意:W3C在HTML标准中规定,定时器的定时时间不能小于4ms,如果是小于4ms,则默认为4ms。
5.异步http请求线程
•背景: 测试流水线开发过程中,为了满足用户操作方便,从节点中提取不同的指标来作为查询条件的指标来源和数据来源; 因此用户的每次操作都会进行重新计算,引起回流; 由于前端组件父子嵌套层级很多,在数据过多的时候也会形成页面假死的状态。
•基于不同的性能工具检测;
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%;
主要可改善指标如下:
•TBT: total Blocking Time总阻塞时间
•CLS:记录了页面上非预期的位移波动
相关名词解析:https://web.dev/metrics/;
上述的指标太多了,哪些才是核心指标呢? Google 在2020年五月提出了网站用户
体验的三大核心指标
1、Largest Contentful Paint (LCP)
LCP:最大内容绘制 , 代表了页面的速度指标,虽然还存在其他的一些体现速度的指标,但LCP能体现的东西更多一些。一是指标实时更新,数据更精确,二是代表着页面最大元素的渲染时间,通常来说页面中最大元素的快速载入能让用户感觉性能还挺好。
最大元素:
◦<img> 标签
◦<image> 在svg中的image标签
◦<video> 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、Lighthouse(速度也比较快, 支持json指标产出和页面html产出),在本地进行测量,根据报告给出的一些建议进行优化;---例如CLS,用这个就很方便, 满足分值后或者通过评审后上线;
2、项目发布上线后,我们可以使用PageSpeed Insights去看下线上的性能情况;
3、使用PageSpeed Insights,可以使用Chrome User Experience Report API去捞取线上过去28天的数据;
4、数据有异常,可使用DevTools工具进行具体代码定位分析;---performance分析, 方案总结和优化
5、使用Search Console’s Core Web Vitals report查看网站功能整体情况;
6、使用Web Vitals扩展方便的看页面核心指标情况;
上述讲了网络请求,渲染过程,那下面我们看下一个简单的页面的整个过程是怎么样的;
•导航阶段,该阶段主要是从网络进程接收 HTML 响应头和 HTML 响应体。
•解析 HTML 数据阶段,该阶段主要是将接收到的 HTML 数据转换为 DOM 和 CSSOM。
•生成可显示的位图阶段,该阶段主要是利用 DOM 和 CSSOM,经过计算布局、生成层树 (LayerTree)、生成绘制列表 (Paint)、完成合成等操作,生成最终的图片。
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 这三个指标来分析
•首先主线程执行到 Composite Layers 过程之后,便会将绘制列表等信息提交给合成线程,合成线程的执行记录你可以通过 Compositor 指标来查看。合成线程维护了一个 Raster 线程池,线程池中的每个线程称为 Rasterize,用来执行光栅化操作,对应的任务就是 Rasterize Paint。当然光栅化操作并不是在 Rasterize 线程中直接执行的,而是在 GPU 进程中执行的,因此 Rasterize 线程需要和 GPU 线程保持通信。然后 GPU 生成图像,最终这些图层会被提交给浏览器进程,浏览器进程将其合成并最终显示在页面上。
对于前端应用来说,网络耗时、页面加载耗时、脚本执行耗时、渲染耗时等耗时情况会影响用户的等待时长
而 CPU占用、内存占用、本地缓存占用等则可能会导致页面卡顿甚至卡死。
因此,性能优化可以分别从耗时和资源占用两方面来解决,也可以理解成时间和空间两个方面。
时间角度(耗时)
在时间角度进行优化主要是减少耗时,浏览器在页面加载的过程中,主要会进行以下的步骤:
•网络请求相关(发起 HTTP 请求从服务端获取页面资源,包括 HTML/CSS/JS/图片资源等)浏览器解析 HTML 和渲染页面加载 Javascript 代码时会暂停页面渲染(包括解析到外部资源,会发起 HTTP 请求获取并加载)
耗时优化的着手点:
1、网络请求优化: 目标在于减少网络资源的请求和加载耗时,如果考虑 HTTP 请求过程,显然我们可以从几个角度来进行优化:
•请求链路:DNS 查询、部署 CDN 节点、缓存等
对于请求链路,核心的方案常常包括使用缓存,比如 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: 单线程到多线程的实践
注意:new Worker(xxx.js)里的xxx.js必须和HTML文件同源必须在http/https协议下访问HTML文件,不能用文件协议(类似file:///E:/wamp64/www/t.html 这种
因此我用mamp搭建的一个环境指向测试的文件夹;本地配置http://www.performance.com/进行测试;
主进程代码
<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
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}`);
}
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$/);
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;
});
}
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作为反向代理压缩服务端返回数据的条件
- END -