likes
comments
collection
share

React+Vite技术栈下首屏资源优化

作者站长头像
站长
· 阅读数 45

前言

最近小组做的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产物都会集中在两个文件中:

React+Vite技术栈下首屏资源优化

通过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()],
})

React+Vite技术栈下首屏资源优化 通过图可以看到 lodashechartsvconsolearco-design这些依赖包的打包体积占比很大。

当然vconsole是不需要关注的,因为生产环境打包不会囊括进去,上图体现的是测试环境构建产物体积盒图。

优化措施

通过上面的分析,可以发现当前构建包存在部分依赖产物体积过大、首屏资源没有懒加载的问题。接下来就是针对这两个问题进行优化。

构建产物按需加载

按需加载是为了减少依赖产物体积过大的问题,典型的如 lodashechartsarco-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;

对于lodasharco-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是组件的样式文件,需要一并引入,这里也需要结合组件的目录来配置:

React+Vite技术栈下首屏资源优化

lodash的按需加载

同样的,参考对@arco-design/mobile-react的配置,可以得到lodash的按需加载配置:

{
    libName: 'lodash',
    libDirectory: '',
    camel2DashComponentName: false,
},

因为lodash下的二级文件直接放在根目录下,所以libDirectory设置为'',表示无需增加二级路径。 lodash下的工具函数采用的是low camelcase命名法,因此camel2DashComponentName设置为false

通过上述按需加载措施,构建产物里的@arco-design/mobile-reactlodash体积减少了很多:

React+Vite技术栈下首屏资源优化

具体的包体积数据如下表:

依赖包按需加载前按需加载后
@arco-design/mobile-react543.7KB160.78KB
lodash547.05KB78.23KB

构建出来的js产物体积也降低了不少:

React+Vite技术栈下首屏资源优化

路由懒加载

经过按需加载优化,总的包体积下降了一些,但是因为所有产物集中在一个包里,等于访问首屏就需要加载全量的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'));

看一下路由懒加载后构建产物吧:

React+Vite技术栈下首屏资源优化

可以发现之前整体的index-[hash].jsindex-[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 的名称由对象属性的键决定。

结合项目自身的特性,决定对echartsreact-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,
    },
  },
},

再来看看构建产物:

React+Vite技术栈下首屏资源优化

可以发现独立出来了markdown-[hash].js,echarts-[hash].js两个独立的 chunk。

优化效果

做了这么多的优化,最终效果如何呢? 先看下未优化前的首屏资源体积及加载耗时:

React+Vite技术栈下首屏资源优化

优化之后的加载耗时:

React+Vite技术栈下首屏资源优化

总加载资源体积减少约50%,加载速度提升约40%。 嗯嗯,效果基本达预期^_^。

小结

  1. 首屏资源优化应该先分析问题所在,识别出哪些包体积过大;
  2. 代码分割需要配合路由懒加载才能达到效果,否则即使代码进行了分割,但加载首屏还是会因为路由提前注册的原因,使得浏览器加载所有资源。
  3. 优化也有成本,比如文本中虽然通过代码分割+路由懒加载减少了首屏资源体积,但随之也带来了资源请求数增加的问题,对浏览器的并发请求带来了压力。

附录

完整的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,
         },
       ],
     }),
  ],
});