懒加载与按需加载
懒加载(Lazy Loading)
✅ 定义
懒加载指的是------在需要时才加载资源,而不是一次性加载所有内容。
⏰ 「用到再加载,用不到就不加载」
📦 常见应用场景
场景 | 示例 | 实现方式 |
---|---|---|
图片懒加载 | 长列表中的图片 | / IntersectionObserver |
路由懒加载 | 某个页面未访问时不加载 | import() 动态加载组件 |
组件懒加载 | UI 组件、弹窗、图表等 | React/Vue 异步组件机制 |
按需加载(Code Splitting)
✅ 定义
按需加载指的是------将代码拆分成多个 bundle,在运行时动态加载需要的模块。
📦 「让 JS 模块也能懒加载」
🚀 常见实现方式
- 动态 import
ini
import('./moduleA').then(module => {module.doSomething();
});
- 👉
import()
会返回一个 Promise,只有执行到这一行才去加载对应模块。 - 路由级拆分
ini
const About = React.lazy(() => import('./pages/About'));
<Route path="/about" element={<About />} />
-
构建工具自动分包
- Webpack:
SplitChunksPlugin
- Vite:基于 Rollup 自动拆包(
import()
会生成独立 chunk)
- Webpack:
⚙️ 构建后的效果
假设你的项目中有三个模块:
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
文件中:
unused
被 Tree 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。