人人都会AI编程

21.3 组件与资源懒加载

更新时间:2026-07-11

前面两节讲了如何减少不必要的重渲染和优化列表渲染,但性能优化还有一个无法回避的问题:用户首次访问时,到底需要下载多少代码? 一个中大型项目打包出的 app.js 可能轻松超过 1MB,在移动端弱网下,这意味着用户要盯着白屏好几秒。

懒加载(Lazy Loading)的核心思想很简单:不在一开始就把所有东西都塞给浏览器,而是“用到的时候再加载”。Vue 生态提供了多层面的懒加载支持,覆盖从页面、组件到图片的完整链路。

路由级代码分割

这是最常用、收益也最大的懒加载手段。Vue Router 天然支持动态导入(Dynamic Import),只需把路由配置里的组件改为 () => import(...) 即可:

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

const router = createRouter({
  history: createWebHistory(),
  routes: [
    {
      path: '/',
      component: () => import('@/views/Home.vue')  // 单独打包
    },
    {
      path: '/about',
      component: () => import('@/views/About.vue') // 单独打包
    }
  ]
})

Vite 或 Webpack 在构建时会为每个动态导入的组件生成一个独立的 chunk 文件。只有用户导航到对应路由时,浏览器才会去下载这个 chunk。对于一个有几十个后台管理页面的项目,首屏只需要加载登录页和菜单页的代码,其余页面按需加载,初始包体可以从数 MB 降到几百 KB。

命名 chunk 便于调试(可选):

component: () => import(/* webpackChunkName: "about" */ '@/views/About.vue')

Vite 也支持类似的魔法注释,不过默认已经能生成可辨识的文件名。

实际效果:打开 Network 面板,你会发现首页只加载了 home-abc123.js,当点击“关于”菜单时,才额外请求 about-def456.js。这就是“路由懒加载”的直观体现。

组件级异步加载

某些场景下,不仅页面需要懒加载,某个组件内部的部件也可以延迟加载。比如一个复杂的图表组件、富文本编辑器、视频播放器等,它们体积较大,但不是每个访问该页面的用户都会用到。这时候可以使用 defineAsyncComponent

<template>
  <div>
    <button @click="show = true">打开编辑器</button>
    <RichEditor v-if="show" />
  </div>
</template>

<script setup>
import { ref, defineAsyncComponent } from 'vue'

const show = ref(false)

const RichEditor = defineAsyncComponent(() =>
  import('@/components/RichEditor.vue')
)
</script>

只有当 show 变为 true 时,RichEditor.vue 及其依赖才会被请求。Vue 还提供了友好的加载状态和错误处理:

const RichEditor = defineAsyncComponent({
  loader: () => import('@/components/RichEditor.vue'),
  loadingComponent: () => <div>加载中...</div>,
  errorComponent: () => <div>加载失败,请重试</div>,
  delay: 200, // 延迟 200ms 再显示 loading,避免闪烁
  timeout: 3000
})

在 Vue 3.3+ 中,异步组件还可以配合内置的 <Suspense> 组件,为整个异步加载过程提供统一的“等待中”占位:

<Suspense>
  <template #default>
    <AsyncDashboard />
  </template>
  <template #fallback>
    <LoadingSkeleton />
  </template>
</Suspense>

这样就避免了在多个异步组件上重复写加载状态。

图片懒加载

页面中的图片通常是带宽的“大户”。一个首屏不可见的长列表,如果一次性加载所有图片,不仅浪费流量,还会抢占关键渲染资源的下载优先级。图片懒加载让图片在即将滚动到视口时才加载。

最简单的方式:使用原生 loading 属性
Chrome、Firefox 等主流浏览器已支持 loading="lazy",直接加在 <img> 上即可:

<img src="photo.jpg" loading="lazy" alt="照片" />

浏览器会自动判断图片距离视口的远近,决定何时发起请求。这是零成本、零代码的优化,强烈建议全站默认使用。

对于需要更精细控制的场景(如自定义 placeholder、淡入效果),可以借助 Intersection Observer 封装一个 Vue 指令:

// v-lazy.js
const vLazy = {
  mounted(el, binding) {
    const imgSrc = binding.value
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        el.src = imgSrc
        observer.unobserve(el)
      }
    }, { rootMargin: '50px' }) // 提前 50px 开始加载
    observer.observe(el)
  }
}
export default vLazy
<img v-lazy="imageUrl" alt="懒加载图片" />

配合 UI 组件库(如 Element Plus 的 el-image 组件),它们通常也内置了懒加载支持,设置 lazy 属性即可。

资源预加载

懒加载解决了“不提前加载”,但有时我们也需要预测用户下一步操作,提前加载,以消除切换时的网络等待。比如鼠标悬停在导航菜单上时,预加载该菜单对应的页面的 chunk。

  • <link rel="prefetch">:在浏览器空闲时下载未来可能用到的资源,优先级较低,不影响当前页面加载。
  • <link rel="preload">:强制浏览器提前下载当前页面必需的高优资源。

在 Vite / Webpack 中,可以直接在动态导入时添加魔法注释来生成预取提示:

// 当 Vue Router 匹配到这个链接时,自动添加 <link rel="prefetch">
component: () => import(/* webpackPrefetch: true */ '@/views/Dashboard.vue')

也可以手动添加 <link> 标签或使用 <router-link>prefetch 属性(Vue Router 默认在 <router-link> 上启用了预取,可以通过 prefetch="false" 关闭)。

还有更精确的场景:用户将鼠标指向一个按钮时,就预加载对应的组件:

const preloadVideoPlayer = () => import('./VideoPlayer.vue')
function onMouseEnter() {
  preloadVideoPlayer() // 只是触发下载,不渲染
}

这样当用户点击时,资源大概率已经缓存好了,交互体验极为无缝。

注意事项

  • 不要拆得太细。每个 import() 都会产生一个独立的 HTTP 请求,如果组件很小(如几十行代码),拆得过多反而会因为请求开销拖慢后续加载。一般以“页面”或“大型功能模块”为拆分单位,保持 chunk 大小在 50~200KB 左右较合适。
  • 公共依赖要留意。多个异步 chunk 中引用了同一个第三方库(如 lodash),Vite/Webpack 会智能提取公共部分到单独的 chunk,但要确保 split chunks 配置合理,避免重复打包。
  • 闪动和加载态。异步组件加载有网络延迟,必须配合 loading 或骨架屏,否则用户会看到突然冒出的内容,体验变差。

小结:懒加载是“按需加载”思想的极致体现。从路由到组件再到图片,每一层都可以根据用户行为的预测进行延迟加载。一个优化的项目,应该是“首屏只加载最必要的东西,其他一切都在需要时悄然现身”。这不仅提升了加载速度,也节省了用户流量,在移动端尤其关键。