React+Vite技术栈下首屏资源优化
前言
最近小组做的H5应用需要通过iframe嵌入到第三方站点里(第三方站点也是H5应用,目的是利用第三方应用的流量)。对方老板希望我们站点打开速度能快一点,不要影响到他们的用户体验。为此,专门做了一轮首屏优化。 谈到H5应用的首屏优化,首屏资源体积优化是重中之重。我们的H5应用采用的技术栈是Vite + React + React-Router + Arco-Design,接下来我将详细介绍如何实现首屏资源体积减少这一目标。
问题现状
当初为了快速开发,采用的是Vite下的react-ts模板
pnpm create vite h5-app --template react-ts
该模板下默认是没有任何优化措施的:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
因此构建出来的js、css产物都会集中在两个文件中:
通过rollup-plugin-visualizer插件可以直观查看到依赖包所占的体积:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import visualizer from 'rollup-plugin-visualizer';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react(), visualizer()],
})
通过图可以看到
lodash
,echarts
,vconsole
,arco-design
这些依赖包的打包体积占比很大。
当然
vconsole
是不需要关注的,因为生产环境打包不会囊括进去,上图体现的是测试环境构建产物体积盒图。
优化措施
通过上面的分析,可以发现当前构建包存在部分依赖产物体积过大、首屏资源没有懒加载的问题。接下来就是针对这两个问题进行优化。
构建产物按需加载
按需加载是为了减少依赖产物体积过大的问题,典型的如 lodash
、echarts
、arco-design
,整个包存在很多方法、组件,而在项目实际使用到的其实只有一部分。在构建时进行按需加载,可以有效减小最终产物体积。
echarts的按需加载官方提供了解决方案,本文直接采用了官方推荐的方法
// 引入 echarts 核心模块,核心模块提供了 echarts 使用必须要的接口。 import * as echarts from 'echarts/core'; // 引入柱状图图表,图表后缀都为 Chart import { BarChart } from 'echarts/charts'; // 引入提示框,标题,直角坐标系,数据集,内置数据转换器组件,组件后缀都为 Component import { TitleComponent, TooltipComponent, GridComponent, DatasetComponent, TransformComponent } from 'echarts/components'; // 标签自动布局、全局过渡动画等特性 import { LabelLayout, UniversalTransition } from 'echarts/features'; // 引入 Canvas 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步 import { CanvasRenderer } from 'echarts/renderers'; // 注册必须的组件 echarts.use([ TitleComponent, TooltipComponent, GridComponent, DatasetComponent, TransformComponent, BarChart, LabelLayout, UniversalTransition, CanvasRenderer ]); export default echarts;
对于lodash
、arco-design
库,需要借助一个vite
插件 vite-plugin-imp。该插件的原理是在构建阶段将如:
import { forEach } from 'lodash'
的导入写法改为:
import forEach from 'lodash/forEach'
如此就能实现依赖的按需加载。这个插件目前支持了主流的工具库和组件库:
@arco-design/mobile-react的按需加载
通过查看vite-plugin-imp
的文档,结合@arco-design/mobile-react
库的目录结构,可以得到如下配置:
{
libName: '@arco-design/mobile-react',// npm包的名称
libDirectory: 'esm',// 组件所在目录
// 一些文档引入的是less样式文件,其实arco-design也提供了css样式文件,可以省去安装less的步骤
style: name => [
`@arco-design/mobile-react/esm/${name}/style/css/index.css`,
],
},
上面的配置里,libName
是包名,libDirectory
则需要去node_modules
里查看包的目录结构,确定具体组件所在的包名,style
是组件的样式文件,需要一并引入,这里也需要结合组件的目录来配置:
lodash的按需加载
同样的,参考对@arco-design/mobile-react
的配置,可以得到lodash
的按需加载配置:
{
libName: 'lodash',
libDirectory: '',
camel2DashComponentName: false,
},
因为lodash
下的二级文件直接放在根目录下,所以libDirectory
设置为''
,表示无需增加二级路径。
lodash
下的工具函数采用的是low camelcase命名法,因此camel2DashComponentName
设置为false
。
通过上述按需加载措施,构建产物里的@arco-design/mobile-react
和lodash
体积减少了很多:
具体的包体积数据如下表:
依赖包 | 按需加载前 | 按需加载后 |
---|---|---|
@arco-design/mobile-react | 543.7KB | 160.78KB |
lodash | 547.05KB | 78.23KB |
构建出来的js
产物体积也降低了不少:
路由懒加载
经过按需加载优化,总的包体积下降了一些,但是因为所有产物集中在一个包里,等于访问首屏就需要加载全量的js、css脚本,这是没有必要的。完全可以按照路由对产物进行分割,以此减少首屏体积。
对于React
框架实现路由懒加载的方式有很多,本文借助了 @loadable/component 这个库。
// 之前的代码(示例)
// import Page1 from '@/pages/page1';
// import Page2 from '@/pages/page2';
// import Page3 from '@/pages/page3';
// import Page4 from '@/pages/page4';
// import Page5 from '@/pages/page5';
// 采用路由懒加载后的代码
import loadable from '@loadable/component';
const Page1 = loadable(() => import('@/pages/page1'));
const Page2 = loadable(() => import('@/pages/page2'));
const Page3 = loadable(() => import('@/pages/page3'));
const Page4 = loadable(() => import('@/pages/page4'));
const Page5 = loadable(() => import('@/pages/page5'));
看一下路由懒加载后构建产物吧:
可以发现之前整体的index-[hash].js
、index-[hash].css
被拆分成了很多小文件,这便是路由懒加载的能力,将产物按路由进行分割。
代码二次分割
你以为这样就结束了吗?
no!!!
还可以继续分割。
分析一下,既然路由做了懒加载,那么那些在非首页才用到的依赖是不是可以单独分割为一个chunk
呢,这样的话可以进一步减少index-[hash].js
的体积。说干就干,翻阅Vite
官方文档,看到这样一段描述:
你可以通过配置
build.rollupOptions.output.manualChunks
来自定义 chunk 分割策略(查看 Rollup 相应文档)。
再看看 Rollup的文档,找到关于chunk 分割的配置 output.manualChunks:
{ [chunkAlias: string]: string[] } | ((id: string, {getModuleInfo, getModuleIds}) => string | void)
该选项允许你创建自定义的公共 chunk。当值为对象形式时,每个属性代表一个 chunk,其中包含列出的模块及其所有依赖,除非他们已经在其他 chunk 中,否则将会是模块图(module graph)的一部分。chunk 的名称由对象属性的键决定。
结合项目自身的特性,决定对echarts
、react-markdown
进行单独拆分(因为这样依赖在首页没有用到)。vite.config.ts
中的配置如下:
build: {
outDir: 'build',
target: 'chrome87',
cssTarget: 'chrome61',
chunkSizeWarningLimit: 650,
rollupOptions: {
output: {
manualChunks: {
echarts: ['echarts'],
markdown: ['react-markdown', 'remark-gfm'],
},
experimentalMinChunkSize: 100 * 1024,
},
},
},
再来看看构建产物:
可以发现独立出来了markdown-[hash].js
,echarts-[hash].js
两个独立的 chunk。
优化效果
做了这么多的优化,最终效果如何呢? 先看下未优化前的首屏资源体积及加载耗时:
优化之后的加载耗时:
总加载资源体积减少约50%,加载速度提升约40%。 嗯嗯,效果基本达预期^_^。
小结
- 首屏资源优化应该先分析问题所在,识别出哪些包体积过大;
- 代码分割需要配合路由懒加载才能达到效果,否则即使代码进行了分割,但加载首屏还是会因为路由提前注册的原因,使得浏览器加载所有资源。
- 优化也有成本,比如文本中虽然通过代码分割+路由懒加载减少了首屏资源体积,但随之也带来了资源请求数增加的问题,对浏览器的并发请求带来了压力。
附录
完整的vite.config.ts
配置代码:
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import visualizer from 'rollup-plugin-visualizer';
import vitePluginImp from 'vite-plugin-imp';
// https://vitejs.dev/config/
export default defineConfig({
base: '/',
build: {
outDir: 'build',
target: 'chrome87',
cssTarget: 'chrome61',
chunkSizeWarningLimit: 650,
rollupOptions: {
output: {
manualChunks: {
echarts: ['echarts'],
markdown: ['react-markdown', 'remark-gfm'],
},
},
},
},
plugins: [
react(),
visualizer(),
vitePluginImp({
libList: [
{
libName: '@arco-design/mobile-react',
libDirectory: 'esm',
style: name => [
`@arco-design/mobile-react/esm/${name}/style/css/index.css`,
],
},
{
libName: 'lodash',
libDirectory: '',
camel2DashComponentName: false,
},
],
}),
],
});
转载自:https://juejin.cn/post/7268183236738793513