记录一次 Vite 构建优化

848 阅读6分钟

记录一次 Vite 构建优化

背景

最近一直在做一些工程化的事情,其中就有对项目的构建优化。在这个过程中,踩了不少的坑,也学到了不少东西,这里做一个总结。

项目是一个 Vue 2.7 + ElementUI 的后台管理系统(vue-element-template),使用 Vite 4 作为构建工具, 如何从 VueCli 迁移至 Vite 可以参考这篇文章:Vue Cli 项目重构为 Vite

分析 Analysis

怎样去优化构建,首先需要我们收集项目构建的信息,知己知彼,才能百战不殆。

那么,我们如何去收集构建信息呢?

在 Vite 的生态中也涵盖了 Rollup,所以我们可以使用 Rollup 的插件来收集构建信息。

rollup-plugin-visualizer

由于我们只关注于 builder 这个阶段,而 rollup-plugin-visualizer 这个插件可以帮我们生成一个最终构建产物的可视化的构建视图。

使用这个插件,我们可以看到构建产物的大小,以及各个模块的大小,这样我们就可以进行针对性的优化。

安装

pnpm add rollup-plugin-visualizer -D

我们只有在分析阶段才需要这个插件,所以我们可以在 vite.config.ts 中配置一个 analysis 的环境变量,然后再 package.json 中配置一个 analysis 的命令:

{
  "scripts": {
    "analysis": "pnpm run build -- --mode analysis"
  }
}

vite.config.ts 中配置:

import { defineConfig, type Plugin } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';

export default ({ mode }) => {
  const plugins: Plugin[] = [];

  if (mode === 'analysis') {
    plugins.push(
      visualizer({
        // 是否自动打开浏览器
        open: true,
        // 这里的 filename 是相对于 vite.config.ts 的
        filename: `dist/analysis.html`,
      })
    );
  }

  return defineConfig({
    // ...
    plugins,
  });
};

再运行 pnpm run analysis,我们就可以看到一个可视化的构建视图了,如下图所示:

从中我们可以看到,最大的模块是 echartselement-ui, 显然它们就是我们的构建优化中的最大敌人,本文的重点也是在讲如何优化它们。

优化 manualChunks

manualChunks 是 rollup 提供的一个配置项,也是最最最基础的优化配置项。 它可以将一些模块作为共享公共块单独打包,这样可以减少重复代码,使得打包出来的构建产物更小。

我们可以将 vueelement-ui 等模块抽离出去作为共享公共块,具体配置如下:

vite.config.ts

// ...
defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vue: ['vue'],
          echarts: ['echarts'],
          'element-ui': ['element-ui'],
        },
      },
    },
  },
});

优化 echarts 为按需引入

echarts 的按需引入方式与其版本有关,我们这里使用的是 5.x 版本,参考官网的引入方式可修改为如下所示:

src/lib/echarts.ts

import * as echarts from 'echarts/core';
import { CanvasRenderer } from 'echarts/renderers';
// 这里我们只引入了折线图、柱状图、饼图,如果你需要其他的图表,可以自行引入
import { LineChart, BarChart, PieChart } from 'echarts/charts';
// 同理,如果你需要其他的组件,也应当自行引入
import { TooltipComponent, TitleComponent, GridComponent, LegendComponent } from 'echarts/components';

echarts.use([
  LineChart,
  BarChart,
  PieChart,
  TooltipComponent,
  CanvasRenderer,
  TitleComponent,
  GridComponent,
  LegendComponent,
]);

// 最终我们导出的是一个 echarts 的实例,这样我们就可以在项目中使用了
export default echarts;

添加了这个 src/lib/echarts.ts之后,也需要同时修改项目中的引入方式:

src/views/xxx.vue

<script lang="ts">
// import * as echarts from 'echarts';
import echarts from '@/lib/echarts';
</script>

优化结果:

后,减少了 1.33M

优化 lodash

lodash 是一个非常优秀的工具库,但由于其非 esm 的构建方式,使得它并不支持 vite 默认(rollup)的 tree shaking

优化的方案可以从 package 的方向入手,我们可以使用它的替代 package lodash-es 来实现 tree shaking

安装

pnpm add lodash-es -D

修改项目中的引入方式太过繁琐(可能也还好?),我们可以使用 Vite.resolve.alias 来简化这个过程:

vite.config.ts

// ...
defineConfig({
  resolve: {
    alias: {
      // ...
      lodash: 'lodash-es',
    },
  },
});

优化 ElementUI

作为老牌的组件库,ElementUI 是非常优秀的,但是它同样不支持 tree shaking,最终打包的体积也没办法很好的降下来。

所以我们需要使其支持按需引入的方式,然而看似简单的这一步却有非常多意料之外的坑...

移除 ElementUI 的全局引入

首先我们需要移除 ElementUI 的全局引入方式才能进行优化,但不是说仅仅只改动入口文件(如 src/main.js)就可以了, 还需要修改所有的从 'element-ui' 中引入的地方。

移除入口文件中的导入 src/main.js

// import Element from 'element-ui'
// import './assets/styles/element-variables.scss'

// Element.Dialog.props.closeOnClickModal.default = false

// Vue.use(Element, {
//   size: Cookies.get('size') || 'medium', // set element-ui default size
// })

只要有一个地方引入了 'element-ui',就会导致编译结果包含 'element-ui' 的所有代码

移除从 'element-ui' 中引入的地方,如 src/permission.js (示例)

// 移除
// import { Message } from 'element-ui'
// 修改为从 lib 中引入
import Message from 'element-ui/lib/message';

添加 unplugin-vue-components

unplugin-vue-components 可以帮助我们按需引入组件,来支持 element-ui 的 tree shaking

安装

pnpm add unplugin-vue-components -D

vite.config.ts

import Components from 'unplugin-vue-components/vite';
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';

// ...
defineConfig({
  plugins: [
    Components({
      resolvers: [
        ElementUiResolver({
          // 这里是样式的导入方式,如果你没有使用 sass/scss,那么就改为 css
          importStyle: 'sass',
        }),
      ],
    }),
  ],
});

一般来说 unplugin-vue-components 自动生成的 components.d.ts 不需要提交至 git,所以我们可以在 .gitignore 中添加 components.d.ts

注册全局方法

ElementUI 中有默认会注册一些全局方法,如 MessageMessageBox 等。而我们取消了全局引入的方式自然需要在入口文件中补充这些全局方法。

ElementUI 提供的全局方法主要有以下内容,具体的可以直接上在其源码中查看:

  • $msgbox
  • $alert
  • $confirm
  • $prompt
  • $notify
  • $message
  • $loading

注册它们的方式也与 Element-UI 的源码相同即可,具体如下所示:

src/main.js

Vue.prototype.$loading = Loading.service;
Vue.prototype.$msgbox = MessageBox;
Vue.prototype.$alert = MessageBox.alert;
Vue.prototype.$confirm = MessageBox.confirm;
Vue.prototype.$prompt = MessageBox.prompt;
Vue.prototype.$notify = Notification;
Vue.prototype.$message = Message;

这时千万千万别漏了导入它们的样式文件

src/main.js, 导入样式文件

import 'element-ui/lib/theme-chalk/message.css';
import 'element-ui/lib/theme-chalk/message-box.css';
import 'element-ui/lib/theme-chalk/notification.css';
import 'element-ui/lib/theme-chalk/loading.css';

注册必要的全局组件

当你的组件中存在使用 component:is 动态组件时,unplugin-vue-components 仅仅只负责动态导入并不会将其注册至全局组件,在这种情况下 component:is 是无法正常解析模块的。

所以我们需要手动的在全局注册这些组件,注册方式如下:

src/main.js

// 样式文件
import 'element-ui/lib/theme-chalk/date-picker.css';
import 'element-ui/lib/theme-chalk/select.css';

// 全局组件
ElDatePicker.install(Vue);
ElSelect.install(Vue);

这时我们就可以正常使用 SelectDatePicker 了。

Dev 模式优化

如果你使用过 unplugin-vue-components 动态导入组件,你会发现它并没有支持 Vitedependencies optimized。在这种情况下,每次切换页面都会导致大量的组件被 optimized:

image.png

这在开发的阶段可以说是无间地狱的终极折磨,感兴趣的朋友可以试试。

那么我们要怎么处理这种情况呢,最简单的方式就是在 Dev 环境里全量引入 element-ui 而在 build 阶段再使用 unplugin-vue-components 动态导入实现构建的优化。

vite.config.ts

// 使用 defineConfig function形式
export default defineConfig(({ mode, command }) => {
  plugins: [
    Components({
      // 添加dev环境的检测
      resolvers: command === 'serve' ? [] : [ElementUiResolver({ importStyle: 'sass' })],
    }),
    // 添加自动全量引入 element-ui
    {
      name: 'vite-element-ui-auto-import-plugin',
      transform(code, id) {
        if (command === 'serve' && /src\/main.(js|ts)$/.test(id)) {
          return {
            // 这里是注入 main.ts/js 的代码
            code: `import Element from 'element-ui';\nimport 'element-ui/lib/theme-chalk/index.css';\n${code}\nVue.use(Element);`,
            map: null,
          }
        }
      },
    },
  ],
})

这样就解决了在开发环境中重复 dependencies optimized 的问题。

优化结果

后,减少了 1.22M

总结

本文主要介绍了如何使用 rollup-plugin-visualizer 来分析项目的构建信息,以及如何使用 manualChunks按需引入unplugin-vue-components 等方式来优化项目的构建产物。

优化构建是一件很花时间精力的事情,而且很多时候并不能提高多少用户体验,但对于构建方式以及对于工程化的理解是非常有帮助的。 也希望本文能够帮助到你。

参考