- 作者:老汪软件技巧
- 发表时间:2024-09-14 00:01
- 浏览量:
背景
去年接手一个比较老的前端项目,使用react16.13开发,webpack4.44.2打包,spa的项目。上线后业务侧反馈页面速度慢。
项目存在的问题
公司的项目都是使用sentry做监控,所以想着根据监控去排查下,再去看优化哪些链路。通过排查,项目存在的问题如下:
js体积大(组件库、html2canvas、mobx、),用户在移动设备上打开,大都是4g网络,带宽有限,加载时间长。页面内容依赖2个接口串行调用(下文中简称接口A、接口B),js加载完成后,还需要等待接口调用完成,才能展示出内容。js、css、图片等静态资源没有使用cdn,通过监控发现,ip为南方地区的用户请求js、css、图片时,时间会比较长页面内容非常多,有大概10屏的内容。使用react去渲染的时间也比较长。优化方式
找到了存在的问题,就针对这几点去进行优化。
SSR重构
要减少首屏时间,最有效果的就是将项目重构为ssr,但在实际工作中,将一个项目重构为ssr项目,成本还是比较大的,最后也是因为成本过大的原因,放弃了重构。
减少单个js文件的大小
减少单个js文件的体积,首先想到的就是webpack分包,首先通过webpack-bundle-analyzer进行打包依赖分析。这里有两点
通过下面的分包配置,我将单个js文件的体积都控制在了100kb以下。
// 以下配置webpack版本为4.44.2
optimization: {
concatenateModules: true, //启动作用域提升
splitChunks: {
maxInitialRequests: 100,
maxAsyncRequests: 100,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
chunks: 'initial',
name: 'vendors',
priority: 1,
},
common: {
name: 'common',
minChunks: 3,
priority: 2,
reuseExistingChunk: true
},
echarts: {
test: /[\\/]node_modules[\\/]echarts/,
chunks: 'all',
name: 'echarts',
priority: 3
},
sentry: {
test: /sentry/,
chunks: 'all',
name: 'sentry',
priority: 3
},
zrender: {
test: /[\\/]node_modules[\\/]zrender/,
chunks: 'all',
name: 'zrender',
priority: 3
},
antd: {
test: /[\\/]node_modules[\\/]antd-mobile/,
chunks: 'all',
name: 'antd-mobile',
priority: 3
},
reactSpring: {
test: /[\\/]node_modules[\\/]@react-spring/,
chunks: 'all',
name: 'react-spring',
priority: 3
},
mobx: {
test: /[\\/]node_modules[\\/]mobx/,
chunks: 'all',
name: 'mobx',
priority: 3
},
cssVarsPonyfill: {
test: /[\\/]node_modules[\\/]css-vars-ponyfill/,
chunks: 'all',
name: 'css-vars-ponyfill',
priority: 3
},
},
}
},
这两个配置主要是为了限制请求的并发数量,因为http1.1在同一个域名下有并发请求数量限制(6个)在分包后实际上请求的数量是变多的,因为项目开启了http2的支持,所以这个地方设置为了一个较大的数。
动态加载依赖动态加载
项目中实际有很多依赖,在首屏加载时是用不到的,这个时候可以使用import来进行动态引入。
比如点击按钮将页面生成图片进行下载,使用了html2canvas。
import(
/* webpackChunkName: "html2canvas", webpackPrefetch: true */
'html2canvas'
).then(async ({default: html2canvas}) => {
...
}, 'image/jpeg');
}).catch(() => {
Toast.show('生成报告图片失败,请点击重试');
});
注意此处的webpackPrefetch配置,此配置是在浏览器空闲时预先获取html2canvas,如果不加此配置,那么在点击按钮的时候再去加载html2canvas,用户会感到明显的延迟,那带来的就是用户体验上的问题了。
通过此配置,可以将html2canvas从打包的vendors中抽离出来。
注意我上面的配置,chunks设置为了initial,只会把静态引入的依赖打入到vendors.js中
vendors: {
test: /[\\/]node_modules[\\/]/,
chunks: 'initial',
name: 'vendors',
priority: 1,
},
组件动态加载
react中组件的动态加载是使用的lazy方法。我在项目中的实际应用是将echarts绘制的图片进行了动态加载。
const EchartsVigourAssignPie = lazy(() => import(/* webpackChunkName: "echartsVigourAssignPie" */'./echartsVigourAssignPie'));
{/* echarts扇形图 */}
<Suspense fallback={<Loading />}>
<EchartsVigourAssignPie
dataList={energyDistributionData}
onFinished={() => onEchartsRenderFinished(EEchartsRenderFinishedType.energyDistributeEcharts)}
/>
Suspense>
使用动态加载的组件时,一定要搭配组件来进行使用,否则react是不知道这个组件在动态加载完成之前如何进行展示的。
这里我将使用echarts进行绘制的组件,进行了动态加载,所以在首屏渲染时,是不需要加载echarts的js依赖的,而是等到组件加载时,才需要引入echarts的依赖。
echarts的按需加载
这里还要提的一点是,在使用echarts的时候,是一定要进行echarts的按需加载的,如果使用import * as echarts from 'echarts'; 进行引入,会将整个echarts的包引入进来,体积会非常的大。具体引入方式可以参考echarts官网的教程:echarts按需引入
其他静态资源的体积压缩
除了js文件的压缩,项目中经常用到的静态资源还有html、css、图片、字体
html
html文件相比于其他类型的文件,压缩带来的提升不是很明显(因为一般的html文件本来就已经非常小了),只需要用html-webpack-plugin来进行常规压缩即可
new HtmlWebpackPlugin({
template: indexPath,
filename: 'index.html',
inject: true,
scriptLoading: 'defer',
minify: {
collapseWhitespace: isProduction,
removeComments: isProduction,
minifyJS: true, //压缩html文件中的js代码
minifyCSS: true, //压缩html文件中的css代码
minifyURLs: true //压缩html文件中的url链接
},
})
css (对此方面研究不太多,后续进行补充)压缩插件css按需加载(主要是在开发前端组件库时需要进行css按需加载)图片
1. 图片格式的选择
常见的图片格式有svg、jpg、png、webp,下面给出google给出的如何选择图片的建议
看到这张图时,可能会产生1个疑问:
Q:既然svg的体积更小,还是矢量图,为什么不全部的图片都使用svg?
A:svg格式的图片并不一定比其他格式的图片更小。图标和一些小的icon,svg的体积确实更小,但是对于一些复杂的图片,svg的格式实际上是比其他格式更大的。 在开发时,我们从设计稿网站上去选择不同格式的图片进行下载,会发现对于一些负杂的图片,svg格式的大小,是远远大于png格式的3倍图的,所以实际开发过程中,要根据图片实际的大小去选择
webp格式的兼容性:
在使用webp格式的图片时,一定要判断当前浏览器是否支持webp格式,如果在不支持的浏览器中使用webp,那就显示不出来导致元素空白了
2. 懒加载
懒加载是最常见的一种优化手段,此处不做赘述
3. 渐进式图片(jpg)
对于某些比较大的jpg图片,还可以考虑使用渐进式图片(一种图片格式)
字体
1. font-display
一般我们定义和使用自定义字体的方式:
@font-face {
font-family: "fangzheng_mid";
src: url(~@/assets/font/fangzheng-mid-text.woff2);
font-style: normal;
font-display: swap;
}
其中font-display是比较重要的一个属性。具体可以看MDN中关于此属性的介绍 font-display 。这里只给出结论,一般可以使用font-display: swap;进行优化
2. 字体抽离 + unicode-range
当我们项目引入了一个字体包,字体包中包含所有字符,但实际上项目中并没有用到全部的字符。这个时候就需要对字体包进行抽离,只保留其中我们用到的字符,从而达到缩小字体包体积的目的。
可以使用fonttools工具对字体进行抽离,具体使用方法如下:
pip install fonttools
比如抽离abcdef:pyftsubset input-font-file.ttf --text="abcdef"
pyftsubset input-font-file.ttf --unicodes=U+0020-007F
在实际开发中,一般有两种情况:
1.只对某几个特殊的文字使用特殊字体展示
这种场景相对简单,只需要使用上面的命令,将特定的字体抽离出来作为一个字体包即可
2.不能确定有哪些字符需要使用字体
实际上在不考虑国际化的情况下,我们用到的字符也就是由三部分组成
同时,@font-face中有1个属性:unicode-range,可以指定当前字体的编码范围。当浏览器使用的字符符合当前的编码范围时,才会加载对应的字体文件。
//fangzheng_mid基本中文汉字
@font-face {
font-family: "fangzheng_mid";
src: url(~@/assets/font/fangzheng-mid-text.woff2);
font-style: normal;
font-display: swap;
unicode-range: U+4E00-9FFF;
}
//fangzheng_mid基本拉丁字符
@font-face {
font-family: "fangzheng_mid";
src: url(~@/assets/font/fangzheng-mid-number.woff2);
font-style: normal;
font-display: swap;
unicode-range: U+0020-007F;
}
.note {
font-family: "fangzheng_mid", sans-serif;
}
若上代码所示,我使用fonttools工具将fangzheng字体包中的基本中文汉字和基本拉丁字符分别抽离出来,作为两个字体包,同时我使用font-face将这两个字体包设置为:
同一个名字fangzheng_mid。不同的编码范围
当我类名为note的元素文本内容为中文汉字时,浏览器只会加载fangzheng-mid-text.woff2,文本内容为数字时,只会加载fangzheng-mid-number.woff2,文本内容为汉字+数字时,两个文件都会加载。
假设我的文本内容是接口中返回的,不能确定有哪些内容,就可以利用此机制,让浏览器去自动选择加载哪个文件,从而做到减小字体包体积的目的。
在使用时,还要注意两个问题,
上面说的三个字符范围,只能包括常见的字体,某些生僻字是不包括在内的,如果有则需要将生僻字单独处理在使用fonttools,还有很多参数,这里我贴出我使用chatgpt生成的命令。其中各个参数的具体意义可以参考fonttools文档
pyftsubset ./fangzheng-mid.ttf --output-file=fangzheng-mid-number.woff2 --flavor=woff2 --unicodes=U+0020-007F --layout-features=' *' --glyph-names --symbol-cmap --legacy-cmap --notdef-glyph --notdef-outline --name-IDs='* '
todo:
预加载接口调用时机提前页面内容渲染优先级的控制