懒加载与按需加载

懒加载与按需加载

懒加载(Lazy Loading)

✅ 定义

懒加载指的是------在需要时才加载资源,而不是一次性加载所有内容

⏰ 「用到再加载,用不到就不加载」

📦 常见应用场景

场景 示例 实现方式
图片懒加载 长列表中的图片 / IntersectionObserver
路由懒加载 某个页面未访问时不加载 import() 动态加载组件
组件懒加载 UI 组件、弹窗、图表等 React/Vue 异步组件机制

按需加载(Code Splitting)

✅ 定义

按需加载指的是------将代码拆分成多个 bundle,在运行时动态加载需要的模块

📦 「让 JS 模块也能懒加载」

🚀 常见实现方式

  1. 动态 import
ini 复制代码
import('./moduleA').then(module => {module.doSomething();
});
  1. 👉 import() 会返回一个 Promise,只有执行到这一行才去加载对应模块。
  2. 路由级拆分
ini 复制代码
const About = React.lazy(() => import('./pages/About'));
<Route path="/about" element={<About />} />
  1. 构建工具自动分包

    1. Webpack:SplitChunksPlugin
    2. Vite:基于 Rollup 自动拆包(import() 会生成独立 chunk)

⚙️ 构建后的效果

假设你的项目中有三个模块:

css 复制代码
main.js
chart.js
editor.js

使用按需加载后:

  • 首次只加载 main.js
  • 当用户进入图表页时,再加载 chart.js
  • 当进入编辑器页时,再加载 editor.js

📉 这样就能大幅减少首屏加载时间。

对比项 懒加载 按需加载
作用对象 资源(图片、视频等) 代码(JS 模块)
触发时机 用户即将看到 代码执行到某处
实现方式 IntersectionObserver / loading="lazy" import() 动态导入
目标 节省流量、提高页面加载速度 减少首屏 JS 体积,加快首屏渲染

懒加载 是"图片/资源层面"的优化, 按需加载 是"代码层面"的优化,

懒加载注重「何时加载」资源,

按需加载注重「如何拆分」代码。

二者配合能显著提升页面性能和首屏速度 🚀。

⚛️懒加载在 React 中的实际用法

一、为什么要在 React 中使用懒加载?

React 应用最终会被打包成一个或多个 JS bundle。 如果所有组件一次性加载,会导致:

  • 首屏加载时间过长
  • JS 执行与解析开销大
  • 用户还没看到内容,CPU 就被占满

解决方案 : 使用 React.lazy + Suspense 实现组件级懒加载(按需加载)

二、React.lazy() 基本用法

javascript 复制代码
import React, { lazy, Suspense } from "react";

// 懒加载组件
const About = lazy(() => import("./pages/About"));
const Dashboard = lazy(() => import("./pages/Dashboard"));

function App() {return (<div><Suspense fallback={<div>加载中...</div>}><About /><Dashboard /></Suspense></div>
  );
}

export default App;

📘 说明

  • React.lazy(fn) 接收一个 动态导入函数,返回一个异步组件。
  • Suspense 组件用于包裹懒加载组件,并在加载未完成前显示 fallback
  • 当模块加载完成后,React 会自动渲染真实组件。

三、路由级懒加载(React Router v6)

懒加载最常见的应用场景之一是路由页面👇

javascript 复制代码
import { lazy, Suspense } from "react";
import { Routes, Route } from "react-router-dom";

// 按需加载每个页面组件
const Home = lazy(() => import("./pages/Home"));
const Profile = lazy(() => import("./pages/Profile"));
const NotFound = lazy(() => import("./pages/NotFound"));

export default function AppRouter() {
  return (
    <Suspense fallback={<div>页面加载中...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/profile" element={<Profile />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </Suspense>
  );
}

✅ 优点:

  • 首屏只加载 / 页面资源。
  • 当用户访问 /profile 时才请求对应的 JS chunk。
  • 提升首屏渲染速度,节省带宽。

四、懒加载与骨架屏(Skeleton)结合

"fallback" 不一定只能是简单文字,也可以是骨架屏组件。

javascript 复制代码
const Dashboard = lazy(() => import("./Dashboard"));

function App() {
  return (
    <Suspense fallback={<SkeletonDashboard />}>
      <Dashboard />
    </Suspense>
  );
}

✅ 优点:

  • 提升用户体验(加载过程有视觉反馈)。
  • 避免「白屏」现象。
什么是骨架屏(Skeleton Screen)?

骨架屏是一种在页面加载过程中用于占位的 UI。 它展示一个简化的「灰色框架」版本页面,让用户感觉加载更平滑。

🧩 示意:

加载中时:

[头像方框] [灰色长条表示用户名] ``[大灰块表示文章内容...]

加载完成后:

[真实头像] [用户名] ``[文章内容]

✅ 优点:

  • 避免"白屏"体验
  • 给用户"正在加载中"的心理反馈
  • 在懒加载 / 数据请求过程中非常常见

React 示例:

javascript 复制代码
import React, { lazy, Suspense } from "react";
import SkeletonProfile from "./SkeletonProfile";

const Profile = lazy(() => import("./Profile"));

export default function App() {
  return (
    <Suspense fallback={<SkeletonProfile />}>
      <Profile />
    </Suspense>
  );
}

🔹 fallback 中放入骨架屏组件 🔹 当 Profile 懒加载未完成时,显示骨架屏

五、与动态 import 的区别

特性 React.lazy import()
功能定位 React 异步组件机制 JS 原生动态导入
返回值 React 组件 Promise
使用场景 懒加载组件 任意模块按需加载
是否支持 Suspense ✅ 支持 ❌ 不直接支持

简单理解: React.lazy 是对 import()React 封装

什么是动态 import()

动态 import()ES 模块( ESM 提供的一种异步导入模块的语法 。 它允许我们在代码运行时(而不是加载时)再去加载某个模块。

javascript 复制代码
// 静态导入(编译时就确定)
import { add } from './math.js';

// 动态导入(运行时才执行)
import('./math.js').then(module => {console.log(module.add(2, 3));
});

import() 返回一个 Promise ✅ 只有执行到这一行代码时,模块才会被加载 ✅ 加载完后才能使用导出的内容

语法与用法

基本语法:

ini 复制代码
import(modulePath)
  .then(module => {// 使用模块导出的内容
    module.default();
  })
  .catch(err => {console.error('模块加载失败', err);
  });

✅ 支持 await

csharp 复制代码
async function load() {
  const { add } = await import('./math.js');
  console.log(add(2, 5));
}
为什么要使用动态导入?

💡 原因:性能优化(按需加载)

  • 静态 import 会在打包时将所有模块一起打包进主文件(首屏加载变慢)
  • 动态 import 会在需要时才加载模块(生成独立 chunk)

👉 比如:

scss 复制代码
// 登录后才加载用户中心
if (isLoggedIn) {import('./UserCenter.js').then(...)
}

这样未登录的用户就不会下载无关代码!

构建工具如何处理动态导入?
构建工具 动态导入结果
Webpack 自动分包为独立的 chunk
Vite / Rollup 基于 ESM 原生语法动态加载模块
Parcel / esbuild 也支持生成独立包,延迟加载

打包后目录示例:

bash 复制代码
dist/
├── index.js          # 主包(入口)
├── math-xxx.js       # 被动态导入的独立 chunk

✅ 当执行到 import('./math.js') 时,浏览器才会发请求加载 math-xxx.js

⭐React.lazy() 与动态 import 的关系

React 懒加载底层其实就是 对动态 import 的封装

java 复制代码
// 原生 import()
import('./About').then(module => module.default());

// React.lazy 封装
const About = React.lazy(() => import('./About'));

所以你可以理解为: React.lazy() = 动态 import() + 自动生成异步组件包装

常见应用场景
场景 示例
路由懒加载 const Page = lazy(() => import('./pages/Page'))
按需加载图表库 if (showChart) import('echarts')
管理后台 登录后再加载管理面板模块
组件库 用户只导入实际使用的组件

import() 是 JavaScript 原生提供的异步模块加载机制 。 它让我们可以实现 按需加载(Code Splitting) , 是 React 懒加载、路由懒加载的底层基础。

六、懒加载和 Tree Shaking

可以!懒加载和 Tree Shaking 是可以同时生效的。 但------它们生效的"阶段"和"粒度"不同 👇

名称 触发时机 优化目标 举例
Tree Shaking 构建阶段(打包时) 删除无用代码 删除未使用的导出函数
懒加载 (Lazy Loading) 运行时(用户交互时) 延迟加载模块 import('./PageA')

当你写:

javascript 复制代码
// 懒加载一个页面组件
const PageA = React.lazy(() => import('./pages/PageA'));

构建工具(比如 Vite / Rollup / Webpack)会做两件事:

1️⃣ 代码分割(Code Splitting)

  • ./pages/PageA 打包成一个独立的 chunk 文件 (例如:PageA.[hash].js
  • 主包不会包含它(提升首屏加载速度)

2️⃣ 对 PageA 模块内部做 Tree Shaking

  • 构建阶段依然会扫描 ./pages/PageA 的 export
  • 删除未使用的函数 / 变量

👉 即使 PageA 是懒加载的,模块内部依然可以被 Tree Shaking!

所以:

懒加载和 Tree Shaking 并不冲突。

懒加载发生在运行时,用于减少首屏体积;

Tree Shaking 发生在打包时,用于删除未使用代码。

对于 import('./xxx') 这种懒加载的模块, 构建工具仍然会对其内部代码执行 Tree Shaking 优化。

七、Vite / Rollup 打包流程中的核心逻辑

Rollup 是一个"静态分析型打包器"。 它在打包时并不会真的"执行代码",而是解析 import/export 语句来构建"依赖图

举个例子👇

javascript 复制代码
// App.js
const Comp = React.lazy(() => import('./Comp'));

当 Rollup 看到 import('./Comp') 这个语句时:

  • 它知道这是一个「动态导入」;
  • 但路径 './Comp' 是一个静态字符串
  • 所以它可以在构建阶段确定 要加载的文件是 Comp.js

于是 Rollup 会在内部记录:

App.js → (动态依赖) → Comp.js

这就叫 把 Comp 模块分析进依赖图

⭐在确定依赖关系后,Rollup 进入三个阶段:

阶段 说明
1️⃣ 构建依赖图 扫描所有 import/export 语句(包括静态和动态的)
2️⃣ 执行 Tree Shaking 删除模块内未被使用的变量/函数/导出
3️⃣ 代码分割(Code Splitting) 对动态导入模块生成独立的 chunk 文件

对比结果直观理解:

javascript 复制代码
// Comp.js
export const used = () => console.log("used");
export const unused = () => console.log("unused");

export default function Comp() {used();
}

打包结果如下:

css 复制代码
main.[hash].js      <-- App.js 主包
Comp.[hash].js      <-- 懒加载 chunk,只在点击时加载

Comp.[hash].js 文件中:

  • unusedTree Shaking 删除
  • 整个模块在首屏时 不会加载(因为是动态 import)。

✅ 所以你可以同时获得:

  • 首屏优化(懒加载)

  • 包体积优化(Tree Shaking)

import() 的路径是 静态字符串 时, Rollup 能把它纳入依赖图,对模块做 Tree Shaking, 并单独输出为一个懒加载 chunk。

这就是:

首屏不加载 Comp 模块(Lazy Loading ✅) Comp 模块内无用代码被删除( Tree Shaking ✅)

八、常见问题

1️⃣ 只能用于默认导出(default export)组件

javascript 复制代码
// ✅ 正确
export default function About() { ... }

// ❌ 错误
export const About = () => { ... }

2️⃣ Suspense 目前只支持懒加载,不支持数据请求等待(除非使用 React 18 的并发特性)。

3️⃣ 多个懒加载组件 应尽量共用一个 Suspense,避免重复渲染 fallback。

相关推荐
10年前端老司机3 小时前
面试官爱问的 Object.defineProperty,90%的人倒在这些细节上!
前端·javascript
庞囧3 小时前
从输入 URL 到开始解析 HTML 之间:浏览器背后发生了什么
前端
少年阿闯~~3 小时前
解决HTML塌陷的方法
前端·html
徐小夕3 小时前
花了4个月时间,我写了一款支持AI的协同Word文档编辑器
前端·vue.js·后端
岁月向前4 小时前
小组件获取主App数据的几种方案
前端
用户47949283569154 小时前
TypeScript 和 JavaScript 的 'use strict' 有啥不同
前端·javascript·typescript
恒创科技HK5 小时前
香港服务器速度快慢受何影响?
运维·服务器·前端
bubiyoushang8885 小时前
MATLAB实现直流电法和大地电磁法的一维正演计算
前端·javascript·matlab
Mintopia5 小时前
🧠 AIGC模型的增量训练技术:Web应用如何低成本迭代能力?
前端·javascript·aigc