本文主要阐述前端工程化(webpack/vite)懒加载常用的几种模式,代码片段以vue3为主,vue2/react项目,本文代码片段同样适用
按需导入
js
const routes = [
{
path:"/",
component: ()=> import("@/view/a")
},
{
path:"/b",
component: ()=> import("@/view/b")
}
]
打包时的具体行为
这种写法是Vue中典型的路由懒加载(异步组件) 方式,打包时会触发以下关键行为:
代码分割(Code Splitting)
- Webpack/Vite(Vue 项目主流打包工具)会识别这种动态 import() 语法,将 a.vue、b.vue 及其依赖的代码从主包(app.js/main.js)中拆分出来,生成独立的异步chunk文件(比如 0.js、1.js 或带命名的 view-a.js、view-b.js)。
- 主包体积会显著减小,因为只包含路由定义、公共逻辑等核心代码,不包含各个页面的具体代码。
打包结构变化
- 以 Vue CLI 打包为例,dist 目录会变成这样
js
dist/
├── js/
│ ├── app.xxx.js // 主包(核心逻辑、路由定义)
│ ├── chunk-vendors.xxx.js // 第三方依赖(vue、vue-router 等)
│ ├── 0.xxx.js // 对应 / 路径的 a 组件代码
│ ├── 1.xxx.js // 对应 /b 路径的 b 组件代码
├── css/ // 若组件有样式,也会拆分出对应的 css 文件
└── index.html
加载时机
- 项目首次加载时,只会下载主包和必要的公共依赖,不会加载 a.js/b.js;
- 当用户访问 / 路径时,才会异步加载 0.xxx.js;访问 /b 时加载 1.xxx.js;
- 这就是 "懒加载" 的核心:按需加载,减少首屏加载时间。
优化建议
如果想让打包后的 chunk 文件更易识别(而非 0.js/1.js),可以给异步组件命名:
js
const routes = [
{
path:"/",
// /* webpackChunkName: "view-a" */ 是 Webpack 魔法注释,指定 chunk 名称
component: ()=> import(/* webpackChunkName: "view-a" */ "@/view/a")
},
{
path:"/b",
component: ()=> import(/* webpackChunkName: "view-b" */ "@/view/b")
}
]
如果你使用vite,这样也是可以的:
js
const routes = [
{
path: "/",
// Vite 推荐的注释格式:viteChunkName
component: () => import(/* viteChunkName: "view-a" */ "@/view/a")
},
{
path: "/b",
// 兼容 Webpack 的注释格式也能生效:webpackChunkName
component: () => import(/* webpackChunkName: "view-b" */ "@/view/b")
}
]
上面代码打包后会生成view-a.xxx.js、view-b.xxx.js,便于定位和调试。
补充说明
-
兼容性:
Vite同时兼容/* webpackChunkName: "xxx" */和/* viteChunkName: "xxx" */,两种注释都能生效,你可以任选其一(推荐用viteChunkName更贴合 Vite 生态)。 -
命名规则:
- 命名中不要包含特殊字符(如 /、@),建议用小写字母 + 短横线 / 下划线;
- 如果多个路由组件用同一个 chunk 名称,Vite 会把它们打包到同一个文件中(可用于 "按模块合并 chunk"),例如:
js// 把 a、b 组件打包到同一个 chunk(view-ab)中 const routes = [ { path: "/", component: () => import(/* viteChunkName: "view-ab" */ "@/view/a") }, { path: "/b", component: () => import(/* viteChunkName: "view-ab" */ "@/view/b") } ] -
验证效果:
执行
pnpm run build打包后,查看dist/js目录,能看到以你命名的 chunk 文件(如 view-a.8f7d6e.js),而非数字命名的文件
进阶:Vite全局chunk 命名配置
如果想统一规范所有chunk的命名规则,还可以在 vite.config.js 中配置build.rollupOptions(Vite 底层用Rollup打包,后面可能会改为Rolldown):
js
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
},
build: {
rollupOptions: {
output: {
// 自定义 chunk 命名规则
chunkFileNames: 'js/[name].[hash].js', // 异步 chunk 命名
entryFileNames: 'js/[name].[hash].js', // 入口文件命名
assetFileNames: '[ext]/[name].[hash].[ext]' // 静态资源命名
}
}
}
})
这个配置能让所有 chunk 文件都遵循 名称.哈希.后缀 的格式,配合上面的魔法注释,能完全掌控打包产物的命名。
动态导入
js
import { computed,defineAsyncComponent } from 'vue'
const adminPanel = computed(()=>{
if(user.role === 'admin'){
return defineAsyncComponent(()=> import("./adminPanel.vue"))
}
return null
})
</script>
上面代码只有用户权限为admin时才会加载组件adminPanel,如果你不想使用defineAsyncComponent这个api,那么下面的写法也会达到预期:
js
import { computed,defineAsyncComponent } from 'vue'
const adminPanel = computed(()=>{
if(user.role === 'admin'){
return import("./adminPanel.vue").then(res => res['module'].default)
}
return null
})
</script>
这种方式非常适合弹窗、图标、用户权限等情况
js
<template>
<button @click = "isShow = true">打开弹窗</button>
<dialogComp v-if="isShow" />
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
const isShow = ref(false)
const dialogComp = defineAsyncComponent(()=>{
import("./dialogComp.vue")
})
上面的代码当isShow为true时才会加载弹窗组件dialogComp
预加载
上面弹窗的代码虽然优化了代码加载时机,但是当点击需要弹窗的时候才去加载组件,这显然对用户体验不是很友好。因此我们可以使用预加载的方式,也就是说当前页面已加载完成,且网络闲置时,我们可以浏览器预判我们即将使用的组件,让其在网络闲置时进行下载
webpackPrefetch
webpackPrefetch是webpack提供的一种代码分割优化手段,通过魔法注释的形式标记某个异步加载的模块,让 webpack 在主代码加载完成且浏览器空闲时,提前预加载这个模块的资源文件。
它的核心价值是:当用户后续操作需要用到这个模块时(比如点击按钮加载弹窗组件),模块已经被提前下载到本地,能瞬间加载,极大提升用户体验。
js
<template>
<button @click = "isShow = true">打开弹窗</button>
<dialogComp v-if="isShow" />
</template>
<script setup>
const dialogComp = defineAsyncComponent(() =>
import(/* webpackPrefetch: true */ './dialogComp.vue')
)
</script>
webpackPreload
webpackPreload是webpack提供的一种代码分割优化手段,核心是让标记的模块与主代码并行加载(或在主代码加载时优先加载),而非等待浏览器空闲。它针对的是当前页面即将、马上要用到的模块,优先级远高于 prefetch,但需注意:过度使用会阻塞首屏加载。
js
<template>
<div>
<button @click="showChart = true">展开数据图表</button>
<Chart v-if="showChart" />
</div>
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue'
const showChart = ref(false)
// 核心:用 webpackPreload 标记即将使用的组件
const Chart = defineAsyncComponent(() =>
import(/* webpackPreload: true */ '@/components/Chart.vue')
)
</script>
vite实现prefetch和preload
Vite不支持webpack的魔法注释(/* webpackPreload: true */、/* webpackPrefetch: true */),但可以通过手动创建<link rel="preload">、<link rel="prefetch">标签 + 动态导入 的方式实现完全等效的效果,下面我会分场景给出Vue项目中可直接复用的代码
prefetch
prefetch(对应 webpackPrefetch):手动创建<link rel="prefetch">标签,让浏览器在空闲时加载资源,适用于未来可能使用的模块
- 场景 1:Vue 组件内预加载未来使用的弹窗组件
js
<template>
<button @click="openModal">打开弹窗</button>
<Modal v-if="isModalShow" @close="isModalShow = false" />
</template>
<script setup>
import { ref, defineAsyncComponent, onMounted } from 'vue'
const isModalShow = ref(false)
const Modal = defineAsyncComponent(() => import('@/components/Modal.vue'))
// 封装 prefetch 工具函数
function prefetchModule(modulePath) {
// 利用 requestIdleCallback 确保浏览器空闲时执行(核心:模拟 webpackPrefetch 的空闲加载)
if ('requestIdleCallback' in window) {
window.requestIdleCallback(() => {
const moduleMap = import.meta.glob('@/components/**/*.vue', { as: 'url' })
const moduleUrl = moduleMap[modulePath]?.()
if (!moduleUrl) return
const link = document.createElement('link')
link.rel = 'prefetch'
link.href = moduleUrl
link.as = 'script'
document.head.appendChild(link)
})
} else {
// 兼容不支持 requestIdleCallback 的浏览器(延迟执行)
setTimeout(() => prefetchModule(modulePath), 1000)
}
}
// 页面挂载后,空闲时预加载弹窗组件(prefetch 效果)
onMounted(() => {
prefetchModule('@/components/Modal.vue')
})
const openModal = () => {
isModalShow.value = true
}
</script>
- 场景 2:路由级别 prefetch(未来可能访问的路由)
js
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
afterEnter: () => {
// 首页加载完成后,空闲时预加载"我的页面"(prefetch 效果)
prefetchModule('@/views/Mine.vue')
}
},
{
path: '/mine',
name: 'Mine',
component: () => import('@/views/Mine.vue')
}
]
})
// 封装 prefetch 工具函数
function prefetchModule(modulePath) {
if ('requestIdleCallback' in window) {
window.requestIdleCallback(() => {
const moduleMap = import.meta.glob('@/views/**/*.vue', { as: 'url' })
const moduleUrl = moduleMap[modulePath]?.()
if (moduleUrl) {
const link = document.createElement('link')
link.rel = 'prefetch'
link.href = moduleUrl
link.as = 'script'
document.head.appendChild(link)
}
})
}
}
// 模拟 afterEnter 钩子(Vue Router 无原生 afterEnter,可通过导航守卫实现)
router.afterEach((to) => {
if (to.name === 'Home') {
prefetchModule('@/views/Mine.vue')
}
})
export default router
preload
preload(对应 webpackPreload):手动创建<link rel="preload">标签,让资源和主代码并行加载,适用于当前页面即将使用的组件(比如首屏内的折叠面板组件、路由内核心子组件);
- 场景 1:Vue 3 组件内预加载即将使用的异步组件
js
<template>
<button @click="showChart = true">展开数据图表</button>
<Chart v-if="showChart" />
</template>
<script setup>
import { ref, defineAsyncComponent, onMounted } from 'vue'
const showChart = ref(false)
// 1. 定义异步组件(核心:先不加载,等需要时再执行)
const Chart = defineAsyncComponent(() => import('@/components/Chart.vue'))
// 2. 手动添加 preload 标签,实现和 webpackPreload 一样的并行加载效果
onMounted(() => {
// 先获取 Chart 组件的打包后路径(Vite 中可通过 import.meta.glob 提前映射)
const chartModule = import.meta.glob('@/components/Chart.vue', { as: 'url' })
const chartUrl = chartModule['/src/components/Chart.vue']() // 实际路径根据项目调整
// 创建 preload 标签
const link = document.createElement('link')
link.rel = 'preload'
link.href = chartUrl
link.as = 'script' // 资源类型:脚本文件
link.crossOrigin = 'anonymous' // 解决跨域问题(按需添加)
// 添加到 head 中,触发预加载
document.head.appendChild(link)
})
</script>
- 场景 2:路由级别 preload(当前路由的核心子组件)
js
// router/index.js(Vue 3)
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
beforeEnter: (to, from, next) => {
// 进入首页前,预加载核心子组件(preload 效果)
preloadModule('@/views/UserPanel.vue')
next()
}
}
]
})
// 封装 preload 工具函数(可复用)
function preloadModule(modulePath) {
const moduleMap = import.meta.glob('@/views/**/*.vue', { as: 'url' })
const moduleUrl = moduleMap[modulePath]?.()
if (!moduleUrl) return
const link = document.createElement('link')
link.rel = 'preload'
link.href = moduleUrl
link.as = 'script'
document.head.appendChild(link)
}
export default router
核心区别(webpackPreload vs webpackPrefetch)
| 特性 | webpackPreload | webpackPrefetch |
|---|---|---|
| 加载时机 | 与主chunk并行加载(首屏阶段) |
主chunk加载完成后,浏览器空闲时 |
| 适用场景 | 当前页面即将使用的模块 |
未来页面 / 功能可能使用的模块 |
| 优先级 | 高(可能阻塞首屏) | 低(不阻塞首屏) |
| 典型用例 | 首屏内折叠面板的隐藏组件、路由内的核心子组件 | 二级路由、点击触发的弹窗组件 |
关键注意事项
- 避免滥用:
preload:过多会抢占首屏带宽,导致核心资源加载变慢(比如首屏 JS/CSS 被阻塞);prefetch:过多会在浏览器空闲时消耗带宽(尤其移动端弱网),甚至触发手机流量超额;
原则:只对高优先级、高使用率的资源做预加载(比如preload只用于当前页面马上要用 ,prefetch只用于用户80%概率会点的功能)。
图片之loading
HTML 的img元素有一个loading属性,用于控制图像的加载行为,它有三个可能的值。
auto是默认值,浏览器会根据自身策略决定何时加载图像;lazy表示延迟加载图像,直到图像进入视口;eager则指示浏览器立即加载图像,无论其是否在视口内
懒加载(Lazy Loading)
这是一种常用的图像加载优化技术,通过将loading属性设置为lazy来实现。当用户滚动页面时,图像会在即将进入可视区域时才开始加载,这样可以减少初始页面加载时的网络请求和数据传输量,提高首屏加载速度
html
<img src="" alt="立即加载的图片" loading="eager" />
<img src="" alt="懒加载的图片" loading="lazy" />
<img src="" alt="默认加载的图片" loading="auto" />
- 测试方法:
- 打开浏览器的「开发者工具」(F12),切换到「网络(Network)」面板;
- 刷新页面,你会看到
eager的图片立即出现在网络请求中; - 向下滚动页面,当
lazy的图片进入视口时,才会触发网络请求; auto的行为由浏览器决定(多数现代浏览器会对不在首屏的图片自动懒加载)。
手动实现懒加载(兼容旧浏览器)
js
<script setup lang="jsx">
import { useTemplateRef,onMounted,onBeforeUnmount } from 'vue'
const dataSrc = require('@/assets/img/1.png');
const lazyImg = useTemplateRef('imgRef')
const lazyImgsrc = ref(null)
const checkLazyLoad = ()= > {
const rect = lazyImg.value?.getBoundingClientRect();
// 当图片顶部进入视口(或距离视口100px内)时加载
if (rect.top <= window.innerHeight + 100 && !lazyImgsrc.value) {
lazyImgsrc.value = dataSrc
}
}
onMounted(() => {
window.addEventListener('scroll', checkLazyLoad);
window.addEventListener('resize', checkLazyLoad);
});
onBeforeUnmount(() => {
window.addEventListener('scroll', checkLazyLoad);
window.addEventListener('resize', checkLazyLoad);
});
const renderImg = () => {
return (
<>
<img ref={imgRef} src= "{lazyImgsrc}" />
</>
)
}
return renderImg()
</script>