vue3 + vite 性能优化

958 阅读6分钟

wallhaven-o5g6r7.png

路由懒加载

当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。

// 将 import UserDetails from './views/UserDetails.vue' 
// 替换成 const UserDetails = () => import('./views/UserDetails.vue')
把组件按组分块
  • 使用 webpack

有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4):

const UserDetails = () =>
  import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
const UserDashboard = () =>
  import(/* webpackChunkName: "group-user" */ './UserDashboard.vue')
const UserProfileEdit = () =>
  import(/* webpackChunkName: "group-user" */ './UserProfileEdit.vue')

webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。

  • 使用 vite

在Vite中,你可以在rollupOptions下定义分块:

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      // https://rollupjs.org/guide/en/#outputmanualchunks
      output: {
        manualChunks: {
          'group-user': [
            './src/UserDetails',
            './src/UserDashboard',
            './src/UserProfileEdit',
          ],
        },
      },
    },
  },
})

资源打包目录

将打包的资源划分到不同的文件夹下

 build: {
  rollupOptions: {
    output: {
      chunkFileNames: 'js/[name]-[hash].js',  // 引入文件名的名称
      entryFileNames: 'js/[name]-[hash].js',  // 包的入口文件名称
      assetFileNames: '[ext]/[name]-[hash].[ext]' // 资源文件像 字体,图片等
    }
  }
}

产物分析报告

  • rollup-plugin-visualizer插件

    • 可以生成可视化构建产物关系图页面
    • 打包完成后会在根目录下生成一个stats.html文件,打开就是文件产物分析关系图
  • 安装

    • npm i -D rollup-plugin-visualizer
import { defineConfig } from "vite";
import { visualizer } from "rollup-plugin-visualizer";

export default defineConfig({
  plugins: [
    visualizer({
      // 打包完成后自动打开浏览器,显示产物体积报告
      open: true,
    }),
  ],
});

打包后的 stats.html 文件产物分析关系图

image.png

从上面可以很直观地观察到产物体积的分布情况,定位到某些体积过大的包,然后针对性地进行优化。

Gzip压缩

Gzip 压缩使用 vite-plugin-compression 插件, 压缩后减小代码体积,提升加载性能

安装: npm i -D vite-plugin-compression

vite-plugin-compression 常用配置项

可配置项名称默认值释义
verbosetrue是否在控制台中输出压缩结果
filterRegExp or (file) => boolean指定未压缩的资源
disablefalse是否禁用
threshold1024如果体积大于阈值,则进行压缩,单位为b
algorithmgzip压缩算法,可选[‘gzip’,‘brotliCompress’,‘deflate’,‘deflateRaw’]
ext.gz生成的压缩包的后缀
compressionOptions-对应压缩算法的参数
deleteOriginFile-压缩后是否删除源文件
// build.rollupOptions.plugins[]
viteCompression({
  algorithm: 'gzip',
  threshold: 10240,
  verbose: true, // 是否在控制台中输出压缩结果
  ext: '.gz',
  deleteOriginFile: true // 源文件压缩后是否删除
})

文件大小在阈值超过 10k 后使用 gzip 算法进行压缩

压缩 gz 后缀文件,浏览器正常解析,需要 配置 nginx http 请求,告诉浏览器支持的类型,设置响应头 content-encoding: gzip 。

//在nginx添加
http {
    # 开启或者关闭gzip模块(on|off)
    gzip_static on;
    # gzip压缩比,1 压缩比最小处理速度最快,9 压缩比最大但处理最慢(传输快但比较消耗cpu)。
    gzip_comp_level 2;
}


图片压缩

使用 unplugin-imagemin 进行图片压缩

// 图片压缩
import imagemin from 'unplugin-imagemin/vite';
plugins: [
    imagemin(),
]

Treeshaking

Treeshaking:保证代码运行结果不变的前提下,去除无用的代码(修建无用的枝叶,也就是去掉没有引用的代码)

Vite 会默认使用 Rollup 进行 treeshaking ,不需要额外进行配置。但是有一些条件要注意

  • 使用 ES Modules 语法(即 ES6 的 import 和 export 关键字)
  • CommonJS 语法无法 tree-shaking(即 require 和 exports 语法)
  • 引入的时候只引用需要的模块
    • 要写 import {cloneDeep} from 'lodash-es' 因为方便 tree-shaking
    • 不要写 import _ from 'lodash' 因为会导致无法 tree-shaking 无用模块

去除debugger 和 console

第一种方法,使用 esbuild(官方推荐)

项目上,使用的是这个方案

build: {
  esbuild: {
    drop: ['console', 'debugger']
  }
}

第二种方法,使用 terser 插件

npm i -D terser

build: {
    //移除生产环境log
    minify: 'terser',
    terserOptions: {
      compress: {
        //生产环境时移除console
        drop_console: true,
        drop_debugger: true,
      },
    }
  }

CDN加速

通过配置 CDN 让用户从最近的服务器请求资源,提升网络请求的响应速度。

使用 vite-plugin-cdn-import 配置,以 lodash 为例

// vite.config.js
import { defineConfig } from 'vite'
import viteCDNPlugin from 'vite-plugin-cdn-import'

export default defineConfig({
  plugins: [
    viteCDNPlugin({
      // 需要 CDN 加速的模块
      modules: [
        {
          name: 'lodash',
          var: '_',
          path: `https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js`
        }
      ]
    })
  ]
})

lodash 在代码使用方式

import _ from 'lodash'

const obj = _.cloneDeep({})

构建成功后,Vite 会自动帮我们将 cdn 资源通过 script 标签插入到 html 中

9537000c706c418fa74cdb2afd6c2fc0~tplv-k3u1fbpfcp-jj-mark_3024_0_0_0_q75.webp

开启HTTP2

浏览器有请求并发限制,一般是 6 个,超过限制请求需要排队,之前可以通过域名分发、资源合并来解决,而使用 HTTP2 协议后,其可以在一个TCP连接分帧处理多个请求(多路复用),不受此限制。(头部压缩也带来了一定性能提升)

在 Nginx 中开启 HTTP2

按需加载

这里以 ElementPlus 为例

如果我们项目中完整引入 ElementPlus , 那打包的大小就会偏大

如果我们手动导入, 那么每次用的时候都要导入一下,也是很麻烦

官网 推荐我们使用按需导入

安装依赖:
npm install -D unplugin-vue-components unplugin-auto-import

// 组件库按需加载
import Components from 'unplugin-vue-components/vite';
// 组件库解析器
import {
  ElementPlusResolver,
} from 'unplugin-vue-components/resolvers'

// 自动导入
import AutoImport from 'unplugin-auto-import/vite';

const pathSrc = path.resolve(__dirname, 'src')
export default defineConfig({
    plugins: [
         // 按需加载组件库 element-plus
        Components({
          dts: false, // 不生成 components.d
          // ui库解析器,也可以自定义
          resolvers: [
            ElementPlusResolver(),
          ]
        }),
        AutoImport({
           // 自动导入 Vue 相关函数,如:ref, reactive, toRef 等
          imports: ['vue'],
          // 自动导入 Element Plus 相关函数,如:ElMessage, ElMessageBox... (带样式)
          resolvers: [
             // 自动导入 Element Plus 组件
            ElementPlusResolver(),
          ],
          dts: path.resolve(pathSrc, 'components.d.ts'),
          imports: [
              'vue'
          ],
          include: [
              /\.[j]sx?$/, // .ts, .tsx, .js, .jsx
              /\.vue$/, /\.vue\?vue/ // .vue
          ],
          eslintrc: {
              enabled: true, // 默认false, true启用。生成一次就可以,避免每次工程启动都生成
              filepath: './.eslintrc-auto-import.json', // 生成json文件
              globalsPropValue: true
          },

        }),
    ]
})

注: 如果运行报错: Cannot find module 'consola', 找不到consola模块,那就下载一下, npm i -D consola 就好了

当你使用unplugin-vue-components来引入ui库的时候, message, notification 等引入样式不生效

安装vite-plugin-style-import即可

import {
  createStyleImportPlugin,
  ElementPlusResolve,
} from 'vite-plugin-style-import'

export default defineConfig({
    plugins: [
        createStyleImportPlugin({
          resolves: [
            ElementPlusResolve(),
          ]
        })
    ]
})

ElementPlus 的图标也支持按需引入

可参考这里

// 自动导入图标组件
import Icons from 'unplugin-icons/vite'
import IconsResolver from 'unplugin-icons/resolver'

export default defineConfig({
    plugins: [
         // 按需加载组件库 element-plus
        Components({
          // ui库解析器,也可以自定义
          resolvers: [
            // 自动注册图标组件
            IconsResolver({
              enabledCollections: ['ep'],
            }),
          ]
        }),
        AutoImport({
          // 自动导入 Element Plus 相关函数,如:ElMessage, ElMessageBox... (带样式)
          resolvers: [
            // 自动导入图标组件
            IconsResolver({
              prefix: 'Icon',
            }),
          ],
        }),
        Icons({
          autoInstall: true,
        }),
    ]
})

按需导入图标后写法就跟原来不一样了

// 原来的写法
<el-icon><Lock /></el-icon>

// 按需导入后
<i-ep-lock></i-ep-lock>