vite打包优化(vue篇)

2,094 阅读7分钟

项目的优化背景?

假如我们开发一个应用,一开始只是简单的功能,随时间的流逝,功能在不断地添加。逐渐的从一个超小应用,变成了一个巨大应用。这时候,你的用户发现,每次访问你的网站打开的速度越来越慢,因为我们的web应用默认不配置的话,它都是构建在一个js文件中。这就是为啥用户访问感觉越来越慢的问题,为了解决这个问题,我们选择进行优化(微前端方向暂时不讨论)

首先思考我们可以从哪些点入手:

  1. 业务代码
  2. 第三方包

业务代码

业务代码又可以细分三个领域

  1. 页面开发
  2. 路由(按需加载)
  3. 全局组件

页面开发

假如我们开发了一个a页面,然后这个页面由若干个组件构成,然后我都是使用

import component from 'componentPath/componentName.vue

来引入组件

代码示例

<template>
    <div class="page1">
        我是{{ page }}
        <el-button type="primary" size="default" @click="handleToloadCom">加载组件</el-button>
        <asyncComponent v-if="isLoadCom"></asyncComponent>
        <el-button @click="handleClick">按钮</el-button>
    </div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import asyncComponent from './components/asyncComponent.vue'
import axios from 'axios'
const isLoadCom = ref(false)
const router = useRouter()
const page = ref('页面1')
const handleClick = () => {
    router.push('/page2')
}
axios.get('./data.json').then(res => {
    console.log('res', res)
})
const handleToloadCom = () => {
    isLoadCom.value = true
}
</script>

<style lang="scss" scoped>
.page1 {
    width: 100px;
    height: 100px;
    background: yellow;
}
</style>

组件的代码

<template>
    <div>
        我是按需加载的组件
    </div>
</template>
<script setup lang="ts">
</script>
<style scoped></style>

这时候,你运行yarn build会发现,这a页面在打包的过程中,把组件也构建进去了

image.png

很明显 这不是我们期望的结果,我们期望的是,只有当我点击按钮,才去加载这个组件,但现在是我加载a页面的时候,就已经加载组件了(因为都在一个js文件里面)。ok,现在我们来换一种组件的引用方式

异步组件(defineAsyncComponent)

const component = defineAsyncComponent(()=>import('componentPath/componentName.vue'))

<template>
    <div class="page1">
        我是{{ page }}
        <el-button type="primary" size="default" @click="handleToloadCom">加载组件</el-button>
        <asyncComponent v-if="isLoadCom"></asyncComponent>
        <el-button @click="handleClick">按钮</el-button>
    </div>
</template>
<script setup lang="ts">
import { ref, defineAsyncComponent } from 'vue'
import { useRouter } from 'vue-router'
const asyncComponent = defineAsyncComponent(() => import('./components/asyncComponent.vue'))
import axios from 'axios'
const isLoadCom = ref(false)
const router = useRouter()
const page = ref('页面1')
const handleClick = () => {
    router.push('/page2')
}
axios.get('./data.json').then(res => {
    console.log('res', res)
})
const handleToloadCom = () => {
    isLoadCom.value = true
}
</script>
<style lang="scss" scoped>
.page1 {
    width: 100px;
    height: 100px;
    background: yellow;
}
</style>

这时候,再去运行yarn build

image.png 通过上图可以发现,我们的a页面和组件,已经被分别打包成了index-hash.js和asyncComoponent-hash.js 这时候我们可以通过yarn preview 预览我们刚刚的打包结果。 image.png 通过这张图可以发现在访问页面的时候,asyncComponent组件并没有加载,只有当我们点击按钮的时候,它才去加载,这样通过异步组件的方式,就达到了在加载a页面时,只加载了当前页面,只有触发操作时,才会去加载对应的组件。(假设我们项目中,有几百个页面,这时候,这种提升就很很明显了。) image.png

路由(按需加载)

有人说,为啥要按需加载,不配置又能怎么样,我们这里用一个vite构建的测试项目,来说明,为什么? 假如我们这个项目有6个页面,采用直接加载页面的方式

import page from '@/views/page/index.vue'

import { createWebHashHistory, createRouter } from 'vue-router'
import page1 from '@/views/page1/index.vue'
import page2 from '@/views/page2/index.vue'
import page3 from '@/views/page3/index.vue'
import page4 from '@/views/page4/index.vue'
import page5 from '@/views/page5/index.vue'
import page6 from '@/views/page6/index.vue'
const router = createRouter({
    history: createWebHashHistory(),
    routes: [
        {
            path: '/',
            redirect: '/page1'
        },
        {
            path: '/page1',
            name: 'page1',
            component: page1
        },
        {
            path: '/page2',
            name: 'page2',
            component: page2
        },
        {
            path: '/page3',
            name: 'page3',
            component: page3
        },
        {
            path: '/page4',
            name: 'page4',
            component: page4
        },
        {
            path: '/page5',
            name: 'page5',
            component: page5
        },
        {
            path: '/page6',
            name: 'page6',
            component: page6
        }
    ]
})
export default router

这时候我们执行yarn build,会发现,我们的所有页面都构建在一个js文件

image.png 这时候我们执行yarn preview预览可以发现

image.png 我们只是访问了一个页面,但是却把整个项目的资源都加载(因为都是构建在一个js中)。很明显,这不合理。我们想要的应该是访问a页面,你就给我返回a页面的资源,我访问其他页面的时候,再给我返回其他页面的资源。这才是理想情况,而路由按需加载解决的就是这个问题。 下面 我们修改下路由加载的方式

() => import('@/views/page1/index.vue')

import { createWebHashHistory, createRouter } from 'vue-router'
const router = createRouter({
    history: createWebHashHistory(),
    routes: [
        {
            path: '/',
            redirect: '/page1'
        },
        {
            path: '/page1',
            name: 'page1',
            component: () => import('@/views/page1/index.vue')
        },
        {
            path: '/page2',
            name: 'page2',
            component: () => import('@/views/page2/index.vue')
        },
        {
            path: '/page3',
            name: 'page3',
            component: () => import('@/views/page3/index.vue')
        },
        {
            path: '/page4',
            name: 'page4',
            component: () => import('@/views/page4/index.vue')
        },
        {
            path: '/page5',
            name: 'page5',
            component: () => import('@/views/page5/index.vue')
        },
        {
            path: '/page6',
            name: 'page6',
            component: () => import('@/views/page6/index.vue')
        }
    ]
})
export default router

在执行yarn build,发现每一个页面都单独构建了一个js文件

image.png

我们这时候,再去预览,执行yarn preview

image.png 可以发现我们访问a页面的时候,只返回了a页面的资源,其他的并没有加载,这就是路由按需加载的作用。

全局组件

一般来说我们都是使用这种方式,来注册全局组件。

import { App } from 'vue'
import component1 from './component1.vue'
import component2 from './component2.vue'
import component3 from './component3.vue'
import component4 from './component4.vue'
import component5 from './component5.vue'
import component6 from './component6.vue'
const registerComponent = (app: App<Element>) => {
    app.component('component1', component1)
    app.component('component2', component2)
    app.component('component3', component3)
    app.component('component4', component4)
    app.component('component5', component5)
    app.component('component6', component6)
}
export default registerComponent

这时候,我们如果构建项目yarn build 可以发现,这五个组件都被打到一个index-hash.js文件中

image.png 那假如我们项目中,有个页面仅仅用到component1,但是却加载了全部组件(因为都在一个js文件)。很明显,这是不合理的。所以我们要修改下代码,使用按需加载。

() => import('./component.vue')

import { App } from 'vue'
const registerComponent = (app: App<Element>) => {
    app.component('component1', () => import('./component1.vue'))
    app.component('component2', () => import('./component2.vue'))
    app.component('component3', () => import('./component3.vue'))
    app.component('component4', () => import('./component4.vue'))
    app.component('component5', () => import('./component5.vue'))
    app.component('component6', () => import('./component6.vue'))
}
export default registerComponent

这时候,我们再来运行yarn build

image.png

可以发现,每个组件都单独打了一个包 但是大家有没有发现一个问题,其实我这6个页面,根本都没有地方用到了这些组件,但是他们还是会在打包资源中生成,这是因为在构建的过程中,app.component(componentName,component)加载了你注册的这个资源,所以构建工具认为你这个资源有用,所以就生成了对应的js。但其实项目里面并不一定需要的。

所以在这里推荐大家,非必要不是使用全局组件注册,因为会影响打包结果(造成很多无用的资源被打包)

第三方包

  1. 按需加载
  2. 拆包

按需加载

我们这里用element-plus来做示例说明 不拆分

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { createPinia } from 'pinia'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import registerComponent from '@/components'
const app = createApp(App)
app.use(router)
app.use(createPinia())
app.use(ElementPlus)
registerComponent(app)
app.mount('#app')

执行yarn build

image.png 可以发现一个element-plus接近1M 下面是配置了按需加载

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'node:path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
const rootPath = path.resolve(__dirname)
export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    })
  ],
  resolve: {
    alias: [
      {
        find: '@',
        replacement: path.resolve(rootPath, 'src')
      }
    ]
  }
})

再次执行yarn build

image.png 可以只有100多kb 所以按需加载还是很有用的

拆包

先来看,未拆包打包构建的资源。 image.png

可以发现所有的第三方包都在一个js文件中

这时候,我们来进行拆包

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'node:path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
const rootPath = path.resolve(__dirname)
export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      resolvers: [ElementPlusResolver()],
    }),
    Components({
      resolvers: [ElementPlusResolver()],
    })
  ],
  resolve: {
    alias: [
      {
        find: '@',
        replacement: path.resolve(rootPath, 'src')
      }
    ]
  },
  build: {
    rollupOptions: {
      output: {
        manualChunks(id: string) {
          if (id.includes('node_modules')) {
            const moduleName = id.split('node_modules/')[1].split('/')[0]
            return moduleName
          }
        }
      }
    }
  }
})

这是拆包后构建的项目资源 image.png

可以发现index.js文件已经没了,取而代之的是:pinia、vue-use、vue、vue-router、axiosjs文件。这样就可以保证,假如你的项目只用到了其中的某一个模块,不是把其他模块也加载进来,避免无用资源的浪费。

总结

  1. 写业务代码时,注意非页面一开始加载的组件,就使用异步组件,非必要,不用全局组件,实在需要,在注册全局组件的时候,也是用按需加载的方式注册(就是异步组件),路由尽量使用按需加载的方式来配置。
  2. 使用第三方包,尽量配置按需加载,然后,打包构建的时候,进行拆分更小包。

优化本质是一种妥协(全量和按需加载,各有好处,我们能做的就是按照自己的业务来做取舍)。