工程化懒加载的几种形式

本文主要阐述前端工程化(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>
相关推荐
2503_928411562 小时前
12.23 page页面的逻辑
前端·小程序
学前端搞口饭吃2 小时前
vite最新版+eslint最新版+vue3+Ts配置
javascript·vue.js·ecmascript
llxxyy卢2 小时前
JAVA安全-目录遍历访问控制XSS等安全
前端·安全·xss
骐骥13 小时前
鸿蒙开发使用DevTools工具调试ArkWeb组件中的前端页面
前端·harmonyos·调试·arkweb·纯鸿蒙
WHOVENLY10 小时前
【javaScript】- 笔试题合集(长期更新,建议收藏,目前已更新至31题)
开发语言·前端·javascript
指尖跳动的光11 小时前
将多次提交合并成一次提交
前端·javascript
程序员码歌11 小时前
短思考第263天,每天复盘10分钟,胜过盲目努力一整年
android·前端·后端
oden11 小时前
1 小时速通!手把手教你从零搭建 Astro 博客并上线
前端
若梦plus11 小时前
JS之类型化数组
前端·javascript