钱包工程性能优化-老板直呼涨工资
前言
这次是针对公司内部的商家钱包工程的优化,内部代码混乱。
但是好在使用了umi
框架,默认的路由分包,打包压缩,线上gizp,http2.0都已经开启,但是整体的项目LCP
指标还是超过了3秒
,此次进行相关的二次优化工作。
整体优化思路
项目调整
删除无用代码
运行 umi deadcode
的无用文件检查命令,其会按照如下步检查文件:
- 初始会读取文件匹配所有符合 exclude 正则的文件目录,整理出所有 exclude file;
- 调用 madge 从入口文件出发并传入 tsconfig信息,分析所有文件的依赖关系,生成madge tree依赖树;
- 最后用fs 所有目录文件依次执行 filter 方法去除 exclude file 和 madge tree各自包含的文件,剩下的就是deadcode了。 packages/preset-umi/src/commands/deadcode.ts
运行结果:
书写一个简单的统计代码行数的node脚本,运用fs读取src主文件夹内的文件,递归检索行数。 count-line.js:
const fs = require('fs');
let totalLines = 0;
let filesNum = 0
function countLinesInDirectory(directory) {
const files = fs.readdirSync(directory);
files.forEach((file) => {
const filePath = `${directory}/${file}`;
const stats = fs.statSync(filePath);
if (stats.isFile()) {
const fileContent = fs.readFileSync(filePath, 'utf-8');
const lines = fileContent.split('\n').length;
totalLines += lines;
filesNum ++
} else if (stats.isDirectory()) countLinesInDirectory(filePath);
});
}
const directoryPath = './src';
countLinesInDirectory(directoryPath);
console.log(`Total lines of code: ${totalLines},filesNum: ${filesNum}`);
运行 node count-line.js
,总共我们通过命令的检索和自己的排查 删除69个文件,9632行代码。
整体src主包文件减少 1.9mb => 1.6mb
, 缩小 15.7%
项目结构调整
项目目录中,layout
目录下有非常多的文件夹,而通常layout
目录下为全局布局,默认会在所有路由下生效,所以正常我们不应该将页面 pages
的内容放在 layout
内,且有很多 pages
重复书写了多套 header
样式,我们将目录结构调整,把layout内容整理,只留下登录后布局和未登录布局,优化后目录结构如下:
优化前:
优化后:
service层抽离
优化前:
优化后:
目录结构:
| ├── services // 服务接口
| | └── api-hook // 接口hook
| | | └── 模块1 // 模块1 比如资金管理
| | | | └── index.ts // 接口hook
| | | | └── types.d.ts // 类型命名空间
| | | └── 模块2 // 接口hook
| | | └── ... // 接口hook
| | └── apis.ts // 单个api文件
模块1/index.ts:
模块1/index.d.ts:
性能检测
方案设计:
具体工具使用步骤我就不赘述了,直接概括我用来做什么
- network面板:查看初始化时,各个模块的加载情况,比如接口的并发,哪一个接口占用了最多的下载时间;也可以针对JS,CSS,img等资源类型进行单独的检测和优化。
- Performance面板:可以帮我们检测出很多性能指标,方便后期的优化效果比对;也可以task进行分析,查看初始化时最损耗性能的长任务,看看能不能让其初始化不加载或者拆包。
- lighthouse:给项目一个整体评价,并且会给出一些优化手段建议,可以参考其上的做些优化。
- Memory面板:可以通过内存快照分析和对比查看有没有内存溢出,异常的内存占用情况
- umi analyze:chunk分包分析,可视化的把整体项目的资源文件内容分布情况展示出来,针对比较大的包,和冗余的资源可以做针对性的拆包和剔除。
资源文件优化
资源的预链接
我们的前端资源都是通过 CDN
的形式加载,但是在初始化时项目会向一个地址发送多个 CDN
地址请求资源,同一时间,多个请求都发送给同一个服务器,会导致 DNS
解析多次重复触发。这样会使整体的网页加载有延迟
的情况。
预连接: 添加
preconnect
或 dns-prefetch
资源提示,以尽早与重要的第三方源建立连接,防止重复的DNS解析。
在html文件 head
上配置资源预链接
:
<link rel="dns-prefetch" href="https://at.alicdn.com">
<link rel="dns-prefetch" href="https://g.alicdn.com">
图片资源
初始化时会加载多个图片资源,其会占用较多的接口并发量
解决:
- 用iconfont,css样式来代替部分部分图片
如项目中我们有一些小图标可以用组建库内的图标代替,初始化时的加载图片也可以直接使用组建库的loading动画代替等等
- 用webp资源,代替png资源(webp资源在清晰度,体积上都优于png)
由于webp资源有浏览器兼容问题,所以需要书写一个webp兼容组件: utils
// 创建一个1*1像素的webp图片,来检测浏览器是否兼容webp图片格式
const checkSupportsWebp = () => {
return new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => {
createImageBitmap(image)
.then(() => {
resolve(true);
})
.catch(() => {
reject(false);
});
};
image.onerror = () => {
reject(false);
};
image.src = 'data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA=';
})
}
const isSupportsWebp = await checkSupportsWebp()
组件: (但是这需要线上图片资源存在webp格式)
import { isSupportsWebp } from '@/utils'
const Image = (props) => {
const { src } = props
return <img src={isSupportsWebp ? src.replace('.png', '.webp') : src}/>
}
<Image src="https:aliglobal.img/213fwafawd.png"/>
结果: 优化后的图片资源数量从 7个 => 1个,体积从 27kb => 4.7kb, 图片资源体积缩小 83%。
多语言资源
项目中默认多语言资源的是一次性加载全部的语言资源。项目内也有中英文两种语言的兜底文件
我们需要对资源按需加载,通过当前的语言类型来按需加载对应语言的资源文件。
before:
<script src="https://y.alicdn.com/daily/1.0.0/AEPAY_MERCHANT.json"></script>
after:
assetsLang = getCookie("_lang")
<script src="https://y.alicdn.com/daily/1.0.0/AEPAY_MERCHANT_$!{assetsLang}.json"></script>
项目内部的多语言兜底文件也只需要留下一份中文的兜底语言就可以了,英文的直接删掉。
资源externals 和 CDN
CDN资源的下载速度会比项目自身的资源下载速度要快,且浏览器有缓存,如果有其他项目下载过此文件则可以直接从缓存中读取。
所以部分比较公共的资源入 react
,组建库包
可以打包时配置 排除,然后通过往 html
文件插入对应 script
链接加载。
config.ts
{
externals: {
react: 'React',
'react-dom': 'ReactDOM',
'@alife/next': 'Next'
},
}
配置html文件头部插入:
<script crossorigin src="{{ assetsHost }}/code/lib/react/{{ versions.react }}/umd/react.production.min.js"></script>
<script crossorigin src="{{ assetsHost }}/code/lib/react-dom/{{ versions.react }}/umd/react-dom.production.min.js"></script>
<script crossorigin src="{{ assetsHost }}/code/lib/alifd__next/{{ versions.next }}/next.min.js"></script>
值得注意的是部分引入方式可能无法被 extends 配置排除比如:
// 正常引入
import { menu } from '@alife/next'
// 其他方式 无法被 extends 排除
import Next from '@alife/next/lib'
const menu = Next.menu
我们需要额外增加一个配置专门排除此类引入方式:
{
externals: {
react: 'React',
'react-dom': 'ReactDOM',
'@alife/next': 'Next',
'@alife/next/lib/nav': 'Next.Nav' // 重点
},
}
结果:资源包内体积从3.4mb减少到了 2.43mb。
terser 压缩
问题:
umi
默认的压缩工具是 esbuild
,也就是在 jsMinifier
内配置的,但是esbuild
的压缩方法优先,就只有简单的三项,压缩效率可以尝试和其他工具对比。
{
jsMinifier: 'esbuild',
jsMinifierOptions: {
minifyWhitespace: true,
minifyIdentifiers: true,
minifySyntax: true,
}
}
解决:
我们可以采用更成熟的 terser
进行代码压缩,且对于ES5过去语法的支持更加优异,另外加上一个去除 console.log
的配置项。
config.ts
jsMinifier: 'terser',
jsMinifierOptions: {
compress: {
drop_console: true, // 移除所有 console.log
},
},
结果:
terser
可以更好的透视主chunk内部子包分布,对比两者在打包产物 的 umi.js
内部分布的区别:
esbuild:
terser:
包体积对比,体积缩小 5.8%。
依赖优化
问题
- 有较多小包和冗余依赖:
项目内含有过多的依赖文件,所有引入的资源对于开发人员来讲是一个黑盒,只有分析打包chunk的时候,才会发现问题,增加了长期维护难度。
2. 重复引入的时间依赖:
moment和dayjs都是时间依赖,两个同时存在,占用资源体积:
3. 重复版本的资源:
项目中有比较多的重复依赖,比如 bn.js 有5.2.1和4.12.0两个版本, 还有像readable-stream 也有两个版本, 这种资源内部会有非常多的重复率,增加了我们chunk包的体积。
解决
- 删除无用依赖,简单依赖项自己手写一个并删除。
- 替换moment
我们不能简单的只是把项目上moment的依赖删除,缓存dayjs就可以,因为组建库以及很多的依赖包都依赖了moemnt,所以项目中删了但是项目的依赖包还是引用了moment,所以需要全局替换。
alias 替换:得益于moment和dayjs的api大部分都相同,所以我们可以将所有的
moemnt
全部替换为dayjs,我们使用umi
的alias
能力将所有的moment
引入在打包时全部替换成dayjs
。
config.ts
alias: {
moment: 'dayjs',
},
- 解决不同版本的重复资源:package.json 文件中的"resolutions" 参数。 它是一个特殊的字段,提供对其他依赖项的版本限制。 当依赖项的版本冲突时,使用"resolutions" 字段可以告诉npm 哪个版本应该被安装。
packages.json:
"resolutions": {
"@alifd/next": "~1.25.51",
"@alife/theme-csp-seller": "^0.12.0",
"bn.js": "~5.2.1",
"moment": "~2.29.4",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"readable-stream": "~2.3.8"
},
结果
整体减少了7个依赖包,资源体积缩小了230kb。
模块拆分合并
问题
chunk体积的分配不均匀:资源的分配不均匀会导致首屏加载的资源过多,资源使用覆盖率低,减慢首屏速度。
仔细观察在chunk图内5518模块过大,已经超过了警戒线500kb最大体积,甚至超过了umi.js,这显然是一个性能炸弹,虽然其采取了异步措施,但是其资源解析的时候一定会触发long task,导致浏览器出现卡顿现象。
umi默认拆包方案
umi默认就会按照路由将资源进行拆分,不过其内部在这之上还有一些优化方案:
- bigVendors: 是大 vendors 方案,会将 async chunk 里的 node_modules 下的文件打包到一起,可以避免重复。同时缺点是,1)单文件的尺寸过大,2)毫无缓存效率可言。
- depPerChunk:和 bigVendors 类似,不同的是把依赖按 package name + version 进行拆分,算是解了 bigVendors 的尺寸和缓存效率问题。但同时带来的潜在问题是,可能导致请求较多。
- granularChunks:在 bigVendors 和 depPerChunk 之间取了中间值,同时又能在缓存效率上有更好的利用。
对应umi分包代码
packages/preset-umi/src/features/codeSplitting/codeSplitting.ts
通过测试和分析 最终还是采用 granularChunks
方案:
config.ts
codeSplitting: {
jsStrategy: 'granularChunks'
}
chainWebpack 逐个拆分
6252,2057资源的体积都较大
2057资源会默认加载kernel-check组件,但是其显然是不会初始化就使用的。
我们在umi的基础上加上我们对逐个包的拆分
config.ts
chainWebpack: function (memo) {
memo.optimization.splitChunks({
...
cacheGroups: {
lodash: {
test: /lodash/,
priority: 30,
chunks: 'async',
name: 'lodash',
},
elliptic: {
chunks: 'all',
priority: 30,
name: 'elliptic',
test: /elliptic/,
},
bn: {
chunks: 'all',
priority: 30,
name: 'bn.js',
test: /bn.js/,
},
'readable-stream': {
test: /readable-stream/,
priority: 30,
chunks: 'async',
name: 'readable-stream',
},
'kernel-check': {
test: /kernel-check/,
priority: 30,
chunks: 'async',
name: 'kernel-check',
},
'crypto-js': {
chunks: 'async',
priority: 30,
name: 'crypto-js',
test: /crypto-js/,
},
},
});
},
代码拆包
React.lazy
: 可以将比较大的包动态引入,这会使打包工具将此文件相关依赖资源单独打成一个模块,并且在我们这个组建的逻辑加载到时候才会 await import 此模块,从而达到按需引入和分包的效果。
import { lazy, Suspense } from 'react';
const LazyPageStepOne = lazy(() => import('./step1'));
export default function() {
return (
<Suspense fallback={<div>loading...</div}>
<LazyPageStepOne />
</Suspense>
)
}
await import
: 可以拆分代码块,将一些需要异步引入的资源滞后加载:
比如我这边有一个加密算法文件比较大,但是加密只需要在用户点击提交的时候才需要加载,所以可以在用 await import
拆分
// before
const { body, key } = encryptForGooglePay(PUBLIC_KEY, JSON.stringify(payload));
// after
const encryptForGooglePay = (await import('@/vendors/alipay-password'))?.default;
const { body, key } = await encryptForGooglePay(PUBLIC_KEY, JSON.stringify(payload));
其他优化
- 开启
http2.0
和gizp
压缩:非常重要,且优化会比较显著,但是项目一开始就已经都开启了,这边就不赘述了。 - UI的优化:加入骨架屏,以及加载动画,来减缓用户感知加载的时间。
- 初始化串行接口换成并行。
优化结果
参数
参数名称 | 优化前 | 优化后 |
---|---|---|
DCL | 797.25ms | 663.62ms |
FP | 804.62ms | 728.94ms |
FCP | 804.62ms | 728.94ms |
L | 1.73s | 1.37s |
LCP | 3.08s | 2.05s |
正常网速:LCP
指标提升 33.4%
。
快速3g:LCP
指标提升 50%
。
资源体积
优化前:
优化后:
压缩前总打包资源从
3.0mb => 2.26mb
, 体积缩小 24.7%
。
首页资源体积从 1934.1kb => 1206kb
,体积缩小 37.6%
。
转载自:https://juejin.cn/post/7371815617463156770