前端性能优化 优化方式分类 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
压测结果【优化前&优化后】
并发测试 测试工具:jmeter 腾讯云环境:单节点测试(开放主机端口进行访问) 测试方法:
利用大体积静态资源(JS/CSS)的 http 请求,预估页面并发下资源加载时长
大文件 http 请求(动态 GZIP 压缩)JS 文件 519K
大文件 http 请求(动态 GZIP 压缩)css 文件 43.6K
普通 http Get 请求
腾讯云资源
优化前
[测试 1] request:1C1G limit:2C2G
[测试 2] request:0.5C512M limit:1C1G
10 并发
5 并发
[测试 3] request:0.3C256M limit:0.5C512G
10 并发
5 并发
页面性能测试 测试工具:Lighthouse
优化前
优化思路 影响并发的主要因素有服务器端及客户端的网络、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 : { drop_console : true , drop_debugger : true , }, }, reportCompressedSize : false , sourcemap : false , }
2. webpackChunkName 按路由配置 moudle 合并 chunk 引入包vite-plugin-webpackchunkname,使 vite 支持。
配合动态路由配置
优化前 82 个文件 优化后 53 个文件
1 yarn -D add vite-plugin-webpackchunkname
1 2 3 4 5 6 7 import { manualChunksPlugin } from "vite-plugin-webpackchunkname" ;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 rollupOptions : { 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' ; } }, }, },
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
包文件分析
yarn -D add webpack-bundle-analyzer
添加配置
1 2 3 4 5 6 7 8 chainWebpack : (config ) => { if (process.env .NODE_ENV === "production" ) { config .plugin ("webpack-bundle-analyzer" ) .use (require ("webpack-bundle-analyzer" ).BundleAnalyzerPlugin ); } };
合理拆分 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 : { ..... ..... chunks : ['chunk-vendors' , 'vendors-utils' , 'vendors-tdgv' , 'vendors-core' , 'index' ] }, }, 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
1 yarn -D add vite-plugin-compression
1 2 3 4 5 6 7 8 9 import compress from 'vite-plugin-compression' ;... defineConfig({ plugins: [ compress({ threshold: 10240 }), ... ] })
命名 chunkFileNames 命名 chunkFileNames,会额外生成文件,未研究明白。。。
1 2 3 4 5 6 7 8 9 10 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 的大小无法在做更细粒度拆分。
优化后
参考文档 vue 官方性能优化 Puppeteer+Lighthouse 2022 前端性能优化最佳实践 JMETER 压力测试 常见的性能优化方案
参考指标 页面渲染 DOMContentLoaded < 2s
用户体验三大核心指标 Google 在 20 年五月提出了网站用户体验的三大核心指标
Largest Contentful Paint (LCP) LCP 代表了页面的速度指标,虽然还存在其他的一些体现速度的指标,但是上文也说过 LCP 能体现的东西更多一些。一是指标实时更新,数据更精确,二是代表着页面最大元素的渲染时间,通常来说页面中最大元素的快速载入能让用户感觉性能还挺好。
1 2 3 <img > 标签 <image > 在svg 中的image 标签 <video > video 标签
First Input Delay (FID) FID 代表了页面的交互体验指标,毕竟没有一个用户希望触发交互以后页面的反馈很迟缓,交互响应的快会让用户觉得网页挺流畅。
Cumulative Layout Shift (CLS) CLS 代表了页面的稳定指标,它能衡量页面是否排版稳定。尤其在手机上这个指标更为重要,因为手机屏幕挺小,CLS 值一大的话会让用户觉得页面体验做的很差。CLS 的分数在 0.1 或以下,则为 Good。
知识点:拆分依据主要有以下三种:
项目入口(entry) 入口会拆成一个 chunk,多个项目入口会拆分成多个不同的 chunk
通过 import()动态引入的代码 例如路由里组件的动态载入,导致每个路由界面都会拆成一个 chunk
通过 splitChunks 拆分代码 看下面”通过 splitChunks 拆分代码”的具体介绍