WinddSnow

React-Performance-Optimization-Comprehensive-Code-Splitting-React-Lazy-Suspen...

字数统计: 5.2k阅读时长: 22 min
2026/08/01

第102课:React 性能优化综合——代码分割、React.lazySuspense、虚拟列表

前几课我们学习了 memouseMemouseCallback 等组件级别的性能优化手段。但真正影响用户体验的往往是首屏加载速度长列表渲染性能。首屏加载慢,用户会离开;长列表渲染卡顿,用户会烦躁。React 提供了两个层面的解决方案:代码分割(Code Splitting) 结合 React.lazy<Suspense> 将代码拆分为小块并按需加载,大幅减少首屏 JS 体积;虚拟列表(Virtualization) 则仅渲染可视区域内的少量 DOM 节点,使得即使数据量达到万级也能保持 60fps 的流畅度。本节课将系统讲解这两种技术的原理、配置、实战陷阱和最佳实践,并通过一个完整的产品列表应用将两者融合。


1. 代码分割与 React.lazy

1.1 问题:庞大的 bundle 阻塞首屏

默认情况下,React 应用的全部 JavaScript 代码(业务逻辑、第三方库、组件)会被打包为一个或少数几个巨大的文件。当用户首次访问时,浏览器必须下载、解析并执行整个 bundle 才能渲染出任何内容。随着应用增长,这个 bundle 的体积可能达到数 MB,在网络较慢的移动设备上造成数秒的白屏。

代码分割 是解决这一问题的核心技术:将 bundle 拆分为多个较小的 chunk,首屏仅加载当前页面必需的代码,其余代码在用户导航到对应页面时才按需加载。Webpack、Vite 等打包工具都原生支持通过动态 import() 实现代码分割。React 则通过 React.lazy 提供了组件级别的懒加载封装。

1.2 React.lazy:组件级懒加载

React.lazy 接收一个返回 Promise 的函数,该函数的返回值必须是一个 import() 动态导入的模块,且该模块必须默认导出一个 React 组件。React.lazy 返回一个新的组件,该组件在首次渲染时自动触发动态导入,并在加载完成后渲染导入的组件。

1
2
3
4
5
6
7
import { lazy } from 'react';

// 静态导入:所有代码打包进主 bundle
// import HeavyChart from './HeavyChart';

// 动态懒加载:代码拆分为独立 chunk
const HeavyChart = lazy(() => import('./HeavyChart'));

关键约束

  • React.lazy 接受的函数必须返回 Promise<{ default: ComponentType }>。这意味着被导入的模块必须使用 export default 导出组件。如果是命名导出,需要在 import() 后手动映射:lazy(() => import('./Module').then(m => ({ default: m.NamedExport })))
  • React.lazy 只能用于客户端渲染。在服务端渲染(SSR)中,需要使用 @loadable/component 等替代方案。
  • React.lazy 返回的组件必须被 <Suspense> 包裹,否则在加载过程中会抛出错误。

1.3 错误边界:处理加载失败

如果 import() 因为网络问题或路径错误而失败,React 会抛出错误。你可以使用 错误边界(Error Boundary) 捕获这一错误并提供降级 UI:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { Component, lazy, Suspense } from 'react';

const LazyComponent = lazy(() => import('./RiskyModule'));

class ErrorBoundary extends Component {
state = { hasError: false };

static getDerivedStateFromError() {
return { hasError: true };
}

render() {
if (this.state.hasError) {
return <div>⚠️ 组件加载失败,请刷新页面重试。</div>;
}
return this.props.children;
}
}

// 使用
<ErrorBoundary>
<Suspense fallback={<div>加载中...</div>}>
<LazyComponent />
</Suspense>
</ErrorBoundary>

2. <Suspense>:优雅的加载状态管理

<Suspense> 是 React 提供的声明式加载状态组件。它在子组件尚未准备好(如懒加载未完成、数据未获取)时,渲染你指定的 fallback UI。<Suspense>React.lazy 是天然的搭档。

2.1 基础用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { Suspense, lazy } from 'react';

const Dashboard = lazy(() => import('./Dashboard'));
const Settings = lazy(() => import('./Settings'));

function App() {
return (
<div>
<h1>管理后台</h1>
<Suspense fallback={<LoadingSpinner />}>
<Dashboard />
</Suspense>
<Suspense fallback={<div>加载设置中...</div>}>
<Settings />
</Suspense>
</div>
);
}

每个 <Suspense> 边界独立管理其子组件的加载状态。上例中,DashboardSettings 各自拥有独立的 fallback,互不阻塞。

2.2 嵌套 Suspense:逐步揭示内容

你可以将多个 <Suspense> 嵌套,实现逐步揭示(Progressive Disclosure) 的用户体验:先展示页面框架,再逐步加载各部分内容。

1
2
3
4
5
6
7
8
9
<Suspense fallback={<PageSkeleton />}>       {/* 外层:整体骨架屏 */}
<PageHeader /> {/* 立即渲染 */}
<Suspense fallback={<ChartPlaceholder />}> {/* 内层:图表占位 */}
<HeavyChart />
</Suspense>
<Suspense fallback={<TablePlaceholder />}> {/* 内层:表格占位 */}
<DataTable />
</Suspense>
</Suspense>

PageHeader 可以立即渲染(它没有懒加载),而 HeavyChartDataTable 各自懒加载时,用户会先看到页头,然后看到图表和表格的占位符,最终看到完整内容。这种渐进的体验远比一个全局加载器让人感觉“快”得多。

2.3 <Suspense> 与路由的集成

路由级别的代码分割是最常见的优化手段——每个页面独立为一个 chunk,仅在用户访问时才加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

const HomePage = lazy(() => import('./pages/Home'));
const ProductsPage = lazy(() => import('./pages/Products'));
const ProductDetailPage = lazy(() => import('./pages/ProductDetail'));

function App() {
return (
<BrowserRouter>
<Suspense fallback={<div className="page-loader">页面加载中...</div>}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/products" element={<ProductsPage />} />
<Route path="/products/:id" element={<ProductDetailPage />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}

2.4 useTransition 与 Suspense 的协同

React 18 引入了 useTransition Hook,允许你将某些状态更新标记为低优先级过渡。当结合 Suspense 时,useTransition 可以在新页面内容加载期间保持当前页面的响应(不隐藏当前内容,只显示一个小的加载指示器)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { useState, useTransition, Suspense, lazy } from 'react';

const HeavyPage = lazy(() => import('./HeavyPage'));

function App() {
const [tab, setTab] = useState('home');
const [isPending, startTransition] = useTransition();

function switchTab(nextTab) {
startTransition(() => {
setTab(nextTab); // 标记为低优先级更新
});
}

return (
<div>
<button onClick={() => switchTab('home')}>首页</button>
<button onClick={() => switchTab('heavy')}>重型页面</button>
{isPending && <small>(加载中...)</small>}
<Suspense fallback={<div>加载页面...</div>}>
{tab === 'home' ? <Home /> : <HeavyPage />}
</Suspense>
</div>
);
}

当用户点击“重型页面”按钮时,startTransition 告诉 React:“这个状态更新不重要,如果加载需要时间,先保持旧内容显示,不要立即显示 fallback”。用户仍然可以与当前页面交互,不会感受到“冻结”。


3. 虚拟列表:万级数据的流畅渲染

3.1 问题:全量 DOM 渲染的性能灾难

假设你需要渲染一个包含 10,000 条 数据的列表。如果使用常规的 .map() 渲染,React 会创建 10,000 个 DOM 节点,以及更多的样式计算和布局重排。这会导致:

  • 首次渲染极慢(数百毫秒到数秒)。
  • 滚动卡顿(浏览器需要计算所有节点的布局)。
  • 内存占用巨大(每个 DOM 节点约消耗 50-100 字节,总计可能超过 10MB)。

虚拟列表(Virtualization) 的核心思想是:无论数据总量多大,仅在 DOM 中渲染可视区域内的少量元素(通常 10-30 个)。当用户滚动时,动态替换这些元素的内容和位置,创造出“无限列表”的视觉效果。这通过以下计算实现:

  • 总高度 = 数据总量 × 每项高度(告知滚动条应该多长)。
  • 起始索引 = Math.floor(scrollTop / itemHeight)(第一个可见项的索引)。
  • 可见项数 = Math.ceil(containerHeight / itemHeight) + overscan(多渲染几个作为缓冲)。
  • 偏移量 = startIndex × itemHeight(将可见项通过 translateY 推到正确位置)。

3.2 自实现简易虚拟列表

理解原理的最好方式是手写一个。以下是一个 80 行代码的简化版虚拟列表组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import { useState, useRef, useEffect, useCallback } from 'react';

function VirtualList({ items, itemHeight, containerHeight, renderItem, overscan = 3 }) {
const [scrollTop, setScrollTop] = useState(0);
const containerRef = useRef(null);

// 处理滚动(使用 passive 模式提升性能)
const handleScroll = useCallback(() => {
if (containerRef.current) {
setScrollTop(containerRef.current.scrollTop);
}
}, []);

useEffect(() => {
const el = containerRef.current;
if (!el) return;
el.addEventListener('scroll', handleScroll, { passive: true });
return () => el.removeEventListener('scroll', handleScroll);
}, [handleScroll]);

// 计算可见范围
const totalHeight = items.length * itemHeight;
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan);
const visibleCount = Math.ceil(containerHeight / itemHeight) + overscan * 2;
const endIndex = Math.min(items.length - 1, startIndex + visibleCount);

// 提取可见项
const visibleItems = [];
for (let i = startIndex; i <= endIndex; i++) {
visibleItems.push(items[i]);
}

return (
<div
ref={containerRef}
style={{ height: containerHeight, overflow: 'auto', position: 'relative' }}
>
{/* 撑开滚动条 */}
<div style={{ height: totalHeight, position: 'relative' }}>
{/* 可见项区域:通过 translateY 定位 */}
<div
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${startIndex * itemHeight}px)`,
}}
>
{visibleItems.map((item, index) => (
<div key={item.id ?? startIndex + index} style={{ height: itemHeight }}>
{renderItem(item)}
</div>
))}
</div>
</div>
</div>
);
}

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const items = Array.from({ length: 100000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
value: Math.random(),
}));

<VirtualList
items={items}
itemHeight={50}
containerHeight={400}
renderItem={(item) => (
<div style={{ padding: '8px', borderBottom: '1px solid #eee' }}>
{item.name}: {item.value.toFixed(4)}
</div>
)}
/>

关键参数

  • overscan:在可见区域之外额外渲染的项数(默认 3)。用于减少快速滚动时的空白闪烁。值越大越平滑,但占用的 DOM 节点越多。
  • itemHeight:每项的固定高度(像素)。这是虚拟列表计算的基础——所有项必须有相同的高度。对于不定高的项,需要使用 react-windowVariableSizeListreact-virtuoso 等库。

3.3 使用 react-windowreact-virtuoso

在实际项目中,推荐使用经过充分测试和性能优化的开源虚拟列表库:

**react-window**(轻量,约 3KB gzipped):

1
npm install react-window
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { FixedSizeList as List } from 'react-window';

function Row({ index, style, data }) {
const item = data[index];
return (
<div style={style}>
{item.name}: {item.value.toFixed(4)}
</div>
);
}

function MyList({ items }) {
return (
<List
height={400}
itemCount={items.length}
itemSize={50}
width="100%"
itemData={items}
>
{Row}
</List>
);
}

**react-virtuoso**(功能更全,支持不定高、自动测量、分组、粘性头等):

1
npm install react-virtuoso
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { Virtuoso } from 'react-virtuoso';

function MyList({ items }) {
return (
<Virtuoso
style={{ height: 400 }}
totalCount={items.length}
itemContent={(index) => (
<div style={{ padding: '8px' }}>
{items[index].name}: {items[index].value.toFixed(4)}
</div>
)}
/>
);
}

选型建议

  • **react-window**:列表项高度固定,追求极致轻量和性能。是大多数后台管理系统的首选。
  • **react-virtuoso**:列表项高度不固定(如包含自动换行的文本)、需要分组、粘性标题、滚动到指定项等功能。开箱即用,配置更少。
  • **@tanstack/react-virtual**:如果你已经在使用 TanStack Table 或需要更底层的控制(如配合自己的滚动容器)。

3.4 虚拟列表与性能指标

引入虚拟列表后,性能的提升是数量级的:

  • DOM 节点数:从 10,000 降至约 30。
  • 首次渲染时间:从数百毫秒降至 10ms 以内。
  • 滚动帧率:从 10-20fps 提升至 60fps。
  • 内存占用:从数十 MB 降至 1-2MB。

这种优化对于无限滚动(Infinite Scroll)——追加数据而非替换——尤其关键:没有虚拟化,持续追加数据会导致 DOM 无限增长,最终浏览器崩溃。


4. 综合实战:代码分割 + 虚拟列表的产品列表页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import { lazy, Suspense, useState, useMemo } from 'react';

// 路由级代码分割:产品详情页面是重型组件
const ProductDetailModal = lazy(() => import('./ProductDetailModal'));

// 模拟 10 万条数据
const allProducts = Array.from({ length: 100000 }, (_, i) => ({
id: i,
name: `Product ${i}`,
price: (Math.random() * 1000).toFixed(2),
category: ['电子产品', '图书', '服装'][i % 3],
}));

function VirtualProductList({ products, onProductClick }) {
// 使用 react-window 渲染虚拟列表
const Row = ({ index, style }) => {
const p = products[index];
return (
<div
style={{ ...style, display: 'flex', alignItems: 'center', padding: '0 16px', borderBottom: '1px solid #eee', cursor: 'pointer' }}
onClick={() => onProductClick(p)}
>
<span style={{ flex: 1 }}>{p.name}</span>
<span style={{ width: 100, textAlign: 'right' }}>¥{p.price}</span>
<span style={{ width: 100, textAlign: 'right', color: '#888' }}>{p.category}</span>
</div>
);
};

return (
<FixedSizeList height={500} itemCount={products.length} itemSize={48} width="100%">
{Row}
</FixedSizeList>
);
}

function App() {
const [search, setSearch] = useState('');
const [selectedProduct, setSelectedProduct] = useState(null);

// useMemo 缓存过滤结果,避免每次渲染都重新过滤 10 万条数据
const filtered = useMemo(() => {
if (!search.trim()) return allProducts;
const lower = search.toLowerCase();
return allProducts.filter(p => p.name.toLowerCase().includes(lower));
}, [search]);

return (
<div style={{ maxWidth: 800, margin: '0 auto', padding: 24 }}>
<h1>产品列表({filtered.length} 条)</h1>
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="搜索产品..."
style={{ width: '100%', padding: '8px 12px', marginBottom: 16, fontSize: 16 }}
/>
<VirtualProductList
products={filtered}
onProductClick={setSelectedProduct}
/>
{selectedProduct && (
<Suspense fallback={<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.3)' }}>加载详情...</div>}>
<ProductDetailModal
product={selectedProduct}
onClose={() => setSelectedProduct(null)}
/>
</Suspense>
)}
</div>
);
}

优化点总结

  • 代码分割ProductDetailModal 仅在用户点击产品时才加载其 JS 和 CSS,首屏不加载详情页代码。
  • 虚拟列表:即使搜索结果为 10 万条,DOM 中仅渲染约 15 个 <div>(视口约 10 个 + overscan 5 个)。
  • useMemo 缓存过滤:输入搜索词时,仅重新过滤数据,不触发虚拟列表的重建。
  • Suspense 边界:详情弹窗加载时显示半透明遮罩,而非全局白屏。

5. 性能优化的决策框架

问题现象 可能原因 推荐工具/手段
首屏加载慢(白屏时间长) JS bundle 过大 代码分割、React.lazy、路由懒加载
页面切换卡顿 新页面加载阻塞渲染 Suspense + useTransition
长列表滚动掉帧 DOM 节点过多 虚拟列表(react-window
输入框输入反应迟钝 每次输入触发重渲染 useMemo 缓存过滤结果
频繁的重新渲染 State 设计不当或引用变化 memouseCallbackuseMemo(见第93课)

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) React.lazy 接收的函数必须返回什么类型的值?
    A. 一个 React 元素。
    B. 一个 Promise,解析为一个包含 React 组件的模块({ default: Component })。
    C. 一个字符串。
    D. 一个数组。

  2. (单选) 虚拟列表提高长列表渲染性能的核心原理是什么?
    A. 使用 GPU 加速渲染所有列表项。
    B. 仅在 DOM 中渲染可视区域内的少量元素。
    C. 将所有列表项缓存到 localStorage
    D. 使用 Web Worker 并行渲染。

  3. (填空) 使用 React.lazy 懒加载的组件必须被 ______ 组件包裹,否则加载过程中会抛出错误。

  4. (多选) 以下哪些库提供了 React 虚拟列表实现?
    A. react-window
    B. react-virtuoso
    C. redux
    D. @tanstack/react-virtual

二、AI 编程任务:编写面向 AI 的提示词

场景:你需要为一个 Dashboard 应用实现以下性能优化:

  • 四个页面组件(DashboardHomeAnalyticsReportsSettings)都使用 React.lazy 懒加载,配合 <Suspense> 显示加载骨架屏。
  • Analytics 页面内有一个包含 50,000 行数据的日志列表,使用 react-windowFixedSizeList 实现虚拟滚动,每行高度 40px。
  • 搜索框实时过滤日志列表(根据关键词筛选日志内容),使用 useMemo 避免每次输入都重建虚拟列表数据。
  • 根组件使用 BrowserRouter<Routes> 配置路由。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成完整的 React 应用代码(包含所有页面组件、路由配置、虚拟列表和搜索逻辑)。提示词中需明确指定 lazySuspenseFixedSizeListuseMemo 的使用位置和方式。

三、Agent 模式下的提示词示例

你是一个资深前端开发 Agent。请创建一个带有性能优化的 React Dashboard 应用。需要创建以下文件:

  1. src/pages/DashboardHome.jsxsrc/pages/Analytics.jsxsrc/pages/Reports.jsxsrc/pages/Settings.jsx:每个文件导出一个默认组件。Analytics 组件内部使用 useState 管理搜索关键词,使用 useMemo 过滤 50,000 行模拟日志数据({ id, level, message, timestamp }),使用 react-windowFixedSizeList 渲染虚拟日志列表(height={600}itemSize={40},Row 组件显示日志级别彩色标签、消息和时间戳)。
  2. src/App.jsx:使用 BrowserRouter。使用 React.lazy 懒加载所有四个页面组件。用一个公共的 <Suspense fallback={<PageSkeleton />}> 包裹 <Routes>PageSkeleton 显示占位方框。
  3. src/components/PageSkeleton.jsx:导出一个骨架屏组件(灰色占位块,模拟页面内容)。
  4. 确保所有代码可直接运行(无需真实 API)。完成后列出所有文件内容。

四、面试真题与参考答案

题目(腾讯前端面试题):

请解释 React 中代码分割(Code Splitting)的原理,以及 React.lazy<Suspense> 在其中的角色。如果懒加载的组件加载失败(如网络问题),React 会如何处理?如何优雅地降级?另外,虚拟列表相比直接渲染列表,在 DOM 结构和滚动条实现上有何本质区别?为什么它能做到“万级数据不卡顿”?

参考答案

代码分割是通过动态 import() 将代码拆分为多个较小的 chunk,浏览器仅在实际需要时才加载对应 chunk,从而减少首屏 JavaScript 体积。React.lazy 封装了动态导入,返回一个“可被 Suspense 暂停”的组件——该组件首次渲染时触发导入,React 在等待期间暂停渲染并向上查找最近的 <Suspense> 边界,渲染其 fallback 内容。导入完成后,React 替换 fallback 为真实组件。

如果懒加载失败(import() reject),React 会抛出错误。可以配合错误边界捕获这一错误,在 getDerivedStateFromErrorcomponentDidCatch 中提供降级 UI(如“加载失败,请刷新”)。两者配合(Suspense 处理加载中,ErrorBoundary 处理加载失败)构成了完整的异步加载容错方案。

虚拟列表的本质区别在于它创建了一个假的高度来撑开滚动条(总高度 = 数据量 × 单项高度),但在 DOM 中仅渲染可视区域内的少量节点。滚动时,通过动态计算偏移量(translateYpaddingTop)将这些可见节点推到正确的位置。因此无论数据总量是 1 万还是 100 万,DOM 节点数始终恒定(约 20-50 个),滚动性能仅取决于可见项数的渲染开销,与数据总量无关。这解释了它“万级数据不卡顿”的原因——浏览器只处理和绘制几十个元素,而非上万个。


课后练习答案

一、概念自测答案

  1. B

    • 解析:React.lazy 要求返回 Promise<{ default: Component }>,即动态导入的模块必须默认导出组件。
  2. B

    • 解析:虚拟列表通过仅渲染可视区域内的少量 DOM 节点实现高性能。A 错误(虚拟列表不依赖 GPU);C、D 不相关。
  3. <Suspense>

    • 解析:React.lazy 组件必须在 <Suspense> 内部使用,否则加载中状态会抛出错误被错误边界捕获。
  4. A、B、D

    • 解析:react-windowreact-virtuoso@tanstack/react-virtual 均为 React 虚拟列表库。redux 是状态管理库。

二、AI 编程任务参考答案(提示词示例)

示例提示词
“请创建一个 React Dashboard 应用,实现性能优化。要求:

  • 四个页面组件(DashboardHomeAnalyticsReportsSettings)使用 React.lazy 懒加载,<Suspense fallback={<PageSkeleton />}> 包裹。
  • Analytics 包含虚拟日志列表:useMemo 过滤 50000 行模拟数据,react-windowFixedSizeList 渲染。
  • 搜索框使用 useState 管理关键词,实时过滤。
  • BrowserRouter<Routes> 配置路由。
  • 输出所有文件完整代码。”
CATALOG
  1. 1. 第102课:React 性能优化综合——代码分割、React.lazy、Suspense、虚拟列表
    1. 1.1. 1. 代码分割与 React.lazy
      1. 1.1.1. 1.1 问题:庞大的 bundle 阻塞首屏
      2. 1.1.2. 1.2 React.lazy:组件级懒加载
      3. 1.1.3. 1.3 错误边界:处理加载失败
    2. 1.2. 2. <Suspense>:优雅的加载状态管理
      1. 1.2.1. 2.1 基础用法
      2. 1.2.2. 2.2 嵌套 Suspense:逐步揭示内容
      3. 1.2.3. 2.3 <Suspense> 与路由的集成
      4. 1.2.4. 2.4 useTransition 与 Suspense 的协同
    3. 1.3. 3. 虚拟列表:万级数据的流畅渲染
      1. 1.3.1. 3.1 问题:全量 DOM 渲染的性能灾难
      2. 1.3.2. 3.2 自实现简易虚拟列表
      3. 1.3.3. 3.3 使用 react-window 和 react-virtuoso
      4. 1.3.4. 3.4 虚拟列表与性能指标
    4. 1.4. 4. 综合实战:代码分割 + 虚拟列表的产品列表页
    5. 1.5. 5. 性能优化的决策框架
    6. 1.6. 课后练习
      1. 1.6.1. 一、概念自测(选择题 / 填空题)
      2. 1.6.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.6.3. 三、Agent 模式下的提示词示例
      4. 1.6.4. 四、面试真题与参考答案
    7. 1.7. 课后练习答案
      1. 1.7.1. 一、概念自测答案
      2. 1.7.2. 二、AI 编程任务参考答案(提示词示例)