2022-12-03-【优化】前端性能优化

前端性能优化

优化方式分类
1、免费

包体积
ng 配置
缓存
代码压缩 gzip,webpack、vite 压缩
最小化 HTTP 请求、响应大小
JavaScript 中的性能优化,1.避免 JS 动画 2.节流(滚动加载、提交按钮)和防抖(搜索、鼠标移动,窗口 resize)
图片懒加载
使用 CSS3 代替图片
提取公共代码

2、花费
cdn
带宽
cpu/mem

背景

为满足不同企业对系统登录页个性化设计,且需要独立域名访问的要求,另外合同层面及部署层面强要求,将页面进行拆分部署,以达到合规目的。

遂将原有应用一分为二,将登录页独立部署(每新增一个企业使用,独立部署一套个性化 UI 的登录 NG),所有访问请求需通过登录页 ng 进行代理。

考虑到每个企业的部署成本,需给出该部署架构下登录页节点的最优资源配置;为找到最合理的部署资源配置参数,我们将对系统进行压测以获取 CPU、内存的部署资源参数。

部署架构图

压测目的&体验目标

  • 为节省腾讯云部署资源,找出最优资源配置;

根据架构师按用户使用情况进行预估,给到的最大并发 10,合理并发 5,通过压测工具 jmeter 进行测试;
将资源不断减半压缩,以测试当前部署架构下,系统的抗压能力,最终找到最低的资源配置参数,以达到降低成本的目的。

渲染目标 FP 及 FCP 在 2 秒内完成,体验优秀
LCP(Largest Contentful Paint)代表了页面的速度指标,小于 2.5 秒
FID(First Input Delay)代表了页面的交互体验指标,100ms 以内
CLS(Cumulative Layout Shift)代表了页面的稳定指标,0.1 或以下

_ 初始部署资源 _
request 1C1G, limit 2C2G
_ 目标部署资源 _
request 0.5C512M,limit 1C1G

压测结果【优化前&优化后】

  1. 并发测试
    测试工具:jmeter
    腾讯云环境:单节点测试(开放主机端口进行访问)
    测试方法:

    • 利用大体积静态资源(JS/CSS)的 http 请求,预估页面并发下资源加载时长

    • 大文件 http 请求(动态 GZIP 压缩)JS 文件 519K

    • 大文件 http 请求(动态 GZIP 压缩)css 文件 43.6K

    • 普通 http Get 请求

      腾讯云资源

      优化前

      • [测试 1] request:1C1G limit:2C2G
        • 10 并发
        • 5 并发
      • [测试 2] request:0.5C512M limit:1C1G
        • 10 并发
          0.5C512MB10T.old.jpg
        • 5 并发
          0.5C512MB5T.old.jpg
      • [测试 3] request:0.3C256M limit:0.5C512G
        • 10 并发
          0.3C256MB10T.old.jpg
        • 5 并发
          0.3C256MB5T.old.jpg
  2. 页面性能测试
    测试工具:Lighthouse

  • 优化前
    0.3C256MB5T.old.jpg

优化思路

影响并发的主要因素有服务器端及客户端的网络、CPU、内存。而影响前端访问性能的因素分为两个部分,加载效率以及运行效率。

加载时优化
1、包体积优化
2、CDN 加载
3、GZIP(动态+静态)
4、网络优化,合并请求、减少资源大小

运行时优化
1、减少重排 重绘(减少 dom 规模-content-visibility)
2、避免页面卡顿、
3、长列表(虚拟列表 滚动优化 passive)、代码性能、防抖节流

1
2
3
window.addEventListener('touchstart', onTouchStart, { passive: true });
window.addEventListener('touchmove', onTouchStart, { passive: true });
window.addEventListener('mousewheel', onTouchStart, { passive: true });

由于项目中体积最大的依赖:UI 组件库 tdesign 只有最新版本的 CDN 资源,不提供历史版本的 CDN,仅优化其他小体积的,改造意义不大。

通过 TOP 指令监控腾讯云部署节点的 CPU/MEM 使用情况,观察数据变化分析结果得出,影响低并发的最大因素为网络,而同等带宽下影响下载时长的最大因素为下载资源的体积,所以优化最大资源体积会取得最明显的优化效果。

工程背景

1
2
"vue": "2.6.11",
"vite": "2.8.6",

打包优化

依赖包体积检查

bundlejs.com 这样的工具可以用来做快速的检查,但是根据实际的构建设置来评估总是最准确的。

原始打包后静态资源分析
原始包体积

1. 关闭一些打包配置项

  • 这个东西一般是在测试阶段调试使用的
1
2
3
4
5
6
7
8
9
10
11
12
13
build: {
terserOptions: {
compress: {
//生产环境时移除console
drop_console: true,
drop_debugger: true,
},
},
// 关闭文件计算
reportCompressedSize: false,
// 关闭生成map文件 可以达到缩小打包体积
sourcemap: false, // 这个生产环境一定要关闭,不然打包的产物会很大
}

原始包体积

2. webpackChunkName 按路由配置 moudle 合并 chunk

引入包vite-plugin-webpackchunkname,使 vite 支持。

配合动态路由配置

优化前 82 个文件
优化后 53 个文件

  • 2.1 装包
1
yarn -D add vite-plugin-webpackchunkname
  • 2.2 配置 vite.config.js
1
2
3
4
5
6
7
// vite.config.js
import { manualChunksPlugin } from "vite-plugin-webpackchunkname";
// Other dependencies...

export default defineConfig({
plugins: [manualChunksPlugin()],
});
  • 说明:Support for user defined manual chunks 支持用户自定义 chunks *

3. 拆分依赖包

  • 根据依赖包体积进行规划,均匀划分 vendor 大小,均匀拆分依赖;
  • 通过 gzip 压缩降低 2/3 体积。
3.1 vite 相关配置
  • 包体积分析
1
2
# 安装依赖
yarn -D add rollup-plugin-visualizer
  • 配置分析
1
2
3
4
5
6
7
8
plugins: [
vue(),
visualizer({
open:true, //注意这里要设置为true,否则无效
gzipSize:true,
brotliSize:true
})
],
  • 合理分包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/* build.rollupOptions.output.manualChunks */
rollupOptions: {
// input: viteMultiPages,
output: {
manualChunks: (id) => {
if (
id.indexOf('node_modules/lodash/') !== -1 ||
id.indexOf('node_modules/tinycolor2/') !== -1 ||
id.indexOf('node_modules/aegis-web-sdk/') !== -1 ||
id.indexOf('node_modules/sortablejs/') !== -1 ||
id.indexOf('node_modules/@babel/') !== -1 ||
id.indexOf('node_modules/regenerator-runtime/') !== -1 ||
id.indexOf('node_modules/clipboard/') !== -1 ||
id.indexOf('node_modules/dayjs/') !== -1 ||
id.indexOf('node_modules/@popperjs/') !== -1
) {
return 'vendor-utils';
}
if (
id.indexOf('node_modules/core-js/') !== -1 ||
id.indexOf('node_modules/@vue/') !== -1 ||
id.indexOf('node_modules/vue/') !== -1 ||
id.indexOf('node_modules/vue-router/') !== -1 ||
id.indexOf('node_modules/vuex/') !== -1 ||
id.indexOf('node_modules/axios/') !== -1
) {
return 'vendor-core';
}
if (id.indexOf('node_modules/@wecity/') !== -1 || id.indexOf('node_modules/tdesign-icons-vue/') !== -1) {
return 'vendor-tdgv';
}
// if (id.indexOf('node_modules/tdesign-vue/es/') !== -1) {
// return 'vendor-td';
// }
},
},
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// TODO 打包效果与上面有点差异,暂未分析原因
// if (id.includes('node_modules')) {
// const arr = id.toString().split('node_modules/')[1].split('/');

// switch (arr[0]) {
// case 'lodash':
// case 'lodash-es':
// case 'tinycolor2':
// case 'aegis-web-sdk':
// case 'sortablejs':
// case '@popperjs':
// case '@babel':
// case 'js-base64':
// return 'vendor-utils';
// case 'core-js':
// case '@vue':
// case 'vue':
// case 'vue-router':
// case 'vuex':
// case 'axios':
// return 'vendor-core';
// case '@wecity':
// case 'tdesign-icons-vue':
// return 'vendor-tdgv';
// default:
// return null;
// }
// }

优化后包体积

3.2 webpack4 拆包相关配置 SplitChunksPlugin
  1. 包文件分析
  • yarn -D add webpack-bundle-analyzer
  • 添加配置
1
2
3
4
5
6
7
8
// vue.config.js
chainWebpack: (config) => {
if (process.env.NODE_ENV === "production") {
config
.plugin("webpack-bundle-analyzer")
.use(require("webpack-bundle-analyzer").BundleAnalyzerPlugin);
}
};
  1. 合理拆分 module、chunck,配置如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
module.exports = {
pages: {
index: {
.....
.....
// 1、引入新拆分的chunk
chunks: ['chunk-vendors', 'vendors-utils', 'vendors-tdgv', 'vendors-core', 'index']
},
},
// 2、webpack4 拆分chunk
optimization: {
splitChunks: {
cacheGroups: {
'vendors-core': {
name: 'vendors-core',
test: /[\\/]node_modules[\\/](vue|vue-router|axios|@vue|vuex)/,
chunks: 'all',
priority: 10,
enforce: true,
},
'vendors-tdgv': {
name: 'vendors-tdgv',
test: /[\\/]node_modules[\\/](@wecity|tdesign-icons-vue)/,
chunks: 'all',
priority: 8,
enforce: true,
},
'vendors-utils': {
name: 'vendors-utils',
test: /[\\/]node_modules[\\/](lodash|tinycolor2|aegis-web-sdk|clipboard|core-js)/,
chunks: 'all',
priority: 8,
enforce: true,
},
'chunk-vendors': {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
chunks: 'all',
priority: 1,
enforce: true,
},
},
},
}

4. 静态 GZIP

  • 4.1 安装依赖
1
yarn -D add vite-plugin-compression
  • 4.2 配置 vite.config.js
1
2
3
4
5
6
7
8
9
import compress from 'vite-plugin-compression';
...
defineConfig({
plugins: [
// 压缩10kb以上的文件
compress({ threshold: 10240 }),
...
]
})

命名 chunkFileNames

命名 chunkFileNames,会额外生成文件,未研究明白。。。

1
2
3
4
5
6
7
8
9
10
/* build.output.chunkFileNames */
/* 添加一下配置,将按组件文件名 生成chunk 文件 */
output: {
chunkFileNames: (chunkInfo) => {
console.log(`===========${chunkInfo.facadeModuleId}`);
const facadeModuleId = chunkInfo.facadeModuleId ? chunkInfo.facadeModuleId.split('/') : [];
const fileName = facadeModuleId[facadeModuleId.length - 2] || '[name]';
return `${fileName}.[hash].js`;
},
},

优化结果

通过前端包体积优化及静态 GZIP,最大资源大小由 519kb 降低到 199kb;0.3 核 256MB 服务器,10 并发下的耗时从 7.7s 降低到 2.6s 左右,能够满足低并发下正常使用;瓶颈在依赖包 Tdesign 的大小无法在做更细粒度拆分。

优化后

  • request:0.3C256M limit:0.5C512G

    • 10 并发
      0.3C256MB10T.old.jpg
    • 5 并发
      0.3C256MB5T.old.jpg
  • lighthouse 结果
    0.3C256MB5T.old.jpg

参考文档

vue 官方性能优化
Puppeteer+Lighthouse
2022 前端性能优化最佳实践
JMETER 压力测试
常见的性能优化方案

参考指标

页面渲染

DOMContentLoaded < 2s

用户体验三大核心指标

Google 在 20 年五月提出了网站用户体验的三大核心指标

  1. Largest Contentful Paint (LCP)
    LCP 代表了页面的速度指标,虽然还存在其他的一些体现速度的指标,但是上文也说过 LCP 能体现的东西更多一些。一是指标实时更新,数据更精确,二是代表着页面最大元素的渲染时间,通常来说页面中最大元素的快速载入能让用户感觉性能还挺好。
1
2
3
<img> 标签
<image> 在svg中的image标签
<video> video标签
  1. First Input Delay (FID)
    FID 代表了页面的交互体验指标,毕竟没有一个用户希望触发交互以后页面的反馈很迟缓,交互响应的快会让用户觉得网页挺流畅。
  2. Cumulative Layout Shift (CLS)
    CLS 代表了页面的稳定指标,它能衡量页面是否排版稳定。尤其在手机上这个指标更为重要,因为手机屏幕挺小,CLS 值一大的话会让用户觉得页面体验做的很差。CLS 的分数在 0.1 或以下,则为 Good。

WEBPACK4 参考文档SplitChunksPlugin

知识点:拆分依据主要有以下三种:

  1. 项目入口(entry)
    入口会拆成一个 chunk,多个项目入口会拆分成多个不同的 chunk
  2. 通过 import()动态引入的代码
    例如路由里组件的动态载入,导致每个路由界面都会拆成一个 chunk
  3. 通过 splitChunks 拆分代码
    看下面”通过 splitChunks 拆分代码”的具体介绍

2022-12-03-【优化】前端性能优化
https://zhangyingxuan.github.io/2022-12-03-【优化】前端性能优化/
作者
blowsysun
许可协议