工程化懒加载的几种形式

本文主要阐述前端工程化(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.jsview-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")
})

上面的代码当isShowtrue时才会加载弹窗组件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实现prefetchpreload

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>
相关推荐
崔庆才丨静觅18 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606119 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了19 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅19 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅19 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅20 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment20 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅20 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊20 小时前
jwt介绍
前端
爱敲代码的小鱼20 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax