人人都会AI编程

路由级代码分割

更新时间:2026-07-11

路由级代码分割是前端性能优化中最直接、收益最明显的手段之一。它的核心思路很简单:不要把整个应用的所有页面代码打包到一个 JS 文件里,而是按路由拆分成独立的 chunk,用户访问哪个路由时,才加载哪个路由对应的代码

为什么需要它

一个中大型后台管理系统,可能有几十个页面。如果所有页面代码都打包进 app.js,这个文件很容易超过 1MB。用户首次访问时,即使他只看一个 dashboard 页面,也不得不下载所有其他页面的代码——包括那些他可能永远不会点开的设置页、报表页。这直接导致首屏加载变慢,尤其在弱网环境下体验很差。

路由级代码分割解决了这个问题:首屏只加载用户当前访问页面所需的资源,其他页面按需加载

怎么实现:动态 import

Vue Router 4 天然支持基于动态 import() 的懒加载。你只需要将路由配置中的组件定义从一个直接 import 改为一个返回 import() 的函数即可。

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      name: 'home',
      // 直接 import,会打包进主 bundle
      component: () => import('@/views/Home.vue')
    },
    {
      path: '/dashboard',
      name: 'dashboard',
      // 懒加载:访问 /dashboard 时才加载
      component: () => import('@/views/Dashboard.vue')
    },
    {
      path: '/settings',
      name: 'settings',
      component: () => import('@/views/Settings.vue')
    }
  ]
})

export default router

注意这个写法上的小变化:原来可能是 component: Home(假设 import Home from '@/views/Home.vue'),现在变成了 () => import('@/views/Home.vue')只改路由配置,组件本身不用任何改动

构建结果:生成独立 chunk

使用 Vite 或 Webpack 打包时,每个这样的动态 import 都会生成一个独立的 JS 文件。以上述配置为例,最终产物大致如下:

  • index.html 引入主入口 chunk
  • Home.xxxxxxxx.js — 首页代码
  • Dashboard.xxxxxxxx.js — 仪表盘代码
  • Settings.xxxxxxxx.js — 设置页代码

当用户访问 /dashboard 时,浏览器才会请求 Dashboard.xxxxxxxx.js。如果用户从不访问设置页,那个 chunk 永远不会被下载——这就节省了带宽和解析时间。

更进一步:给 chunk 起个有意义的名字

打包工具默认会给每个 chunk 分配一个随机哈希的文件名(如 Dashboard-a3b2c1.js),调试或查看网络请求时不易识别。你可以在 import 里使用魔法注释(magic comment)来指定 chunk 名:

component: () => import(/* webpackChunkName: "dashboard" */ '@/views/Dashboard.vue')
// 或者 Vite 中同样支持这个注释

打包后这个文件就会变成 dashboard.xxxxxxxx.js,一眼就能看出是哪个页面的资源。

配合路由导航的加载状态处理

懒加载带来的一个体验问题是:当用户点击导航时,如果目标页面 chunk 还在下载中,页面会短暂卡住,没有任何反馈。Vue Router 允许你展示一个加载中的占位 UI,有两种常用方式:

方式一:利用异步组件的 Suspense(Vue 3 内置)

<template>
  <Suspense>
    <template #default>
      <router-view />
    </template>
    <template #fallback>
      <div class="loading">页面加载中...</div>
    </template>
  </Suspense>
</template>

<Suspense> 会在异步组件加载时展示 fallback 插槽的内容,加载完成后切换回 default 插槽。注意,异步路由组件本质就是异步组件,所以这个方式直接可用。

方式二:路由守卫级别的全局加载条

router.beforeEach 中显示一个顶部加载条(如 NProgress),在 afterEach 中隐藏,通过 router.isLoading 等辅助判断是否存在异步加载。这种方式更轻量,且对现有代码侵入小。

import NProgress from 'nprogress'

router.beforeEach((to, from, next) => {
  NProgress.start()
  next()
})

router.afterEach(() => {
  NProgress.done()
})

注意事项与最佳实践

  • 不是每个路由都必须分割:首屏关键路径上的页面(如首页、登录页)如果代码量很小,可以考虑直接引入,避免额外的网络请求和延迟。一般建议首屏路由保留同步加载,非首屏路由全部懒加载。
  • 避免过度分割:如果一个组件的体积非常小(比如几 KB),单独拆成一个 chunk 反而增加了请求开销。通常一个页面的体积在几十 KB 以上时,分割才有明确收益。
  • 公共依赖会被自动提取:Vite/Webpack 会自动将多个 chunk 中共享的库(如 Vue、Element Plus、lodash)提取到公共 chunk 中,避免重复下载。你可以通过 manualChunks 进一步手动拆分大型第三方库。
  • 预加载优化:对于用户很可能会点击的链接(比如导航栏中的页面),可以在 <link rel="prefetch"><router-link>prefetch 属性中预先下载对应 chunk,这样点击时页面几乎瞬间打开,兼顾了体积与体验。

实际收益

在一个包含 15 个页面的后台项目中,未分割时主 bundle 体积约 1.2MB,首屏加载时间约 3.2 秒。将所有非首屏页面改为懒加载后,主 bundle 降至 320KB,首屏时间降到 1.1 秒。用户点击其他页面时,额外加载的 chunk 平均在 100~200KB,加载时间在几百毫秒内,配合加载条几乎无感知。

路由级代码分割不是“锦上添花”,而是构建任何多页面 Vue 应用时的默认配置。 你只需要花几分钟修改路由配置,就能换来显著的用户体验提升,并且它完全无侵入,不影响任何业务逻辑。