lazy-loading-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseLazy Loading Patterns
懒加载模式
Code splitting and lazy loading patterns for React 19 applications using , , route-based splitting, and intersection observer strategies.
React.lazySuspense针对React 19应用,使用、、基于路由的分割和交叉观察器策略实现代码分割与懒加载的方案。
React.lazySuspenseOverview
概述
- Reducing initial bundle size for faster page loads
- Route-based code splitting in SPAs
- Lazy loading heavy components (charts, editors, modals)
- Below-the-fold content loading
- Conditional feature loading based on user permissions
- Progressive image and media loading
- 减小初始包大小,实现更快的页面加载
- 单页应用(SPA)中基于路由的代码分割
- 懒加载大型组件(图表、编辑器、模态框)
- 可视区域外内容的加载
- 根据用户权限实现条件化功能加载
- 图片与媒体资源的渐进式加载
Core Patterns
核心模式
1. React.lazy + Suspense (Standard Pattern)
1. React.lazy + Suspense(标准模式)
tsx
import { lazy, Suspense } from 'react';
// Lazy load component - code split at this boundary
const HeavyEditor = lazy(() => import('./HeavyEditor'));
function EditorPage() {
return (
<Suspense fallback={<EditorSkeleton />}>
<HeavyEditor />
</Suspense>
);
}
// With named exports (requires intermediate module)
const Chart = lazy(() =>
import('./charts').then(module => ({ default: module.LineChart }))
);tsx
import { lazy, Suspense } from 'react';
// 懒加载组件 - 在此边界处进行代码分割
const HeavyEditor = lazy(() => import('./HeavyEditor'));
function EditorPage() {
return (
<Suspense fallback={<EditorSkeleton />}>
<HeavyEditor />
</Suspense>
);
}
// 针对具名导出(需要中间模块)
const Chart = lazy(() =>
import('./charts').then(module => ({ default: module.LineChart }))
);2. React 19 use()
Hook (Modern Pattern)
use()2. React 19 use()
Hook(现代模式)
use()tsx
import { use, Suspense } from 'react';
// Create promise outside component
const dataPromise = fetchData();
function DataDisplay() {
// Suspense-aware promise unwrapping
const data = use(dataPromise);
return <div>{data.title}</div>;
}
// Usage with Suspense
<Suspense fallback={<Skeleton />}>
<DataDisplay />
</Suspense>tsx
import { use, Suspense } from 'react';
// 在组件外部创建Promise
const dataPromise = fetchData();
function DataDisplay() {
// 支持Suspense的Promise解析
const data = use(dataPromise);
return <div>{data.title}</div>;
}
// 结合Suspense使用
<Suspense fallback={<Skeleton />}>
<DataDisplay />
</Suspense>3. Route-Based Code Splitting (React Router 7.x)
3. 基于路由的代码分割(React Router 7.x)
tsx
import { lazy } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router';
// Lazy load route components
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics'));
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ path: 'dashboard', element: <Dashboard /> },
{ path: 'settings', element: <Settings /> },
{ path: 'analytics', element: <Analytics /> },
],
},
]);
// Root with Suspense boundary
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<RouterProvider router={router} />
</Suspense>
);
}tsx
import { lazy } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router';
// 懒加载路由组件
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics'));
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
children: [
{ path: 'dashboard', element: <Dashboard /> },
{ path: 'settings', element: <Settings /> },
{ path: 'analytics', element: <Analytics /> },
],
},
]);
// 带Suspense边界的根组件
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<RouterProvider router={router} />
</Suspense>
);
}4. Intersection Observer Lazy Loading
4. 交叉观察器懒加载
tsx
import { useRef, useState, useEffect, lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function LazyOnScroll({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ rootMargin: '100px' } // Load 100px before visible
);
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return (
<div ref={ref}>
{isVisible ? children : <Placeholder />}
</div>
);
}
// Usage
<LazyOnScroll>
<Suspense fallback={<ChartSkeleton />}>
<HeavyComponent />
</Suspense>
</LazyOnScroll>tsx
import { useRef, useState, useEffect, lazy, Suspense } from 'react';
const HeavyComponent = lazy(() => import('./HeavyComponent'));
function LazyOnScroll({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ rootMargin: '100px' } // 在元素进入可视区域前100px开始加载
);
if (ref.current) observer.observe(ref.current);
return () => observer.disconnect();
}, []);
return (
<div ref={ref}>
{isVisible ? children : <Placeholder />}
</div>
);
}
// 使用方式
<LazyOnScroll>
<Suspense fallback={<ChartSkeleton />}>
<HeavyComponent />
</Suspense>
</LazyOnScroll>5. Prefetching on Hover/Focus
5. 悬停/聚焦时预加载
tsx
import { useQueryClient } from '@tanstack/react-query';
import { Link } from 'react-router';
function NavLink({ to, children }: { to: string; children: React.ReactNode }) {
const queryClient = useQueryClient();
const prefetchRoute = () => {
// Prefetch data for the route
queryClient.prefetchQuery({
queryKey: ['page', to],
queryFn: () => fetchPageData(to),
});
// Prefetch the component chunk
import(`./pages/${to}`);
};
return (
<Link
to={to}
onMouseEnter={prefetchRoute}
onFocus={prefetchRoute}
preload="intent" // React Router preloading
>
{children}
</Link>
);
}tsx
import { useQueryClient } from '@tanstack/react-query';
import { Link } from 'react-router';
function NavLink({ to, children }: { to: string; children: React.ReactNode }) {
const queryClient = useQueryClient();
const prefetchRoute = () => {
// 预加载路由数据
queryClient.prefetchQuery({
queryKey: ['page', to],
queryFn: () => fetchPageData(to),
});
// 预加载组件代码块
import(`./pages/${to}`);
};
return (
<Link
to={to}
onMouseEnter={prefetchRoute}
onFocus={prefetchRoute}
preload="intent" // React Router预加载
>
{children}
</Link>
);
}6. Module Preload Hints
6. 模块预加载提示
html
<!-- In index.html or via helmet -->
<link rel="modulepreload" href="/assets/dashboard-chunk.js" />
<link rel="modulepreload" href="/assets/vendor-react.js" />
<!-- Prefetch for likely next navigation -->
<link rel="prefetch" href="/assets/settings-chunk.js" />tsx
// Programmatic preloading
function preloadComponent(importFn: () => Promise<any>) {
const link = document.createElement('link');
link.rel = 'modulepreload';
link.href = importFn.toString().match(/import\("(.+?)"\)/)?.[1] || '';
document.head.appendChild(link);
}html
<!-- 在index.html中或通过helmet添加 -->
<link rel="modulepreload" href="/assets/dashboard-chunk.js" />
<link rel="modulepreload" href="/assets/vendor-react.js" />
<!-- 预加载可能的下一个导航目标 -->
<link rel="prefetch" href="/assets/settings-chunk.js" />tsx
// 程序化预加载
function preloadComponent(importFn: () => Promise<any>) {
const link = document.createElement('link');
link.rel = 'modulepreload';
link.href = importFn.toString().match(/import\("(.+?)"\)/)?.[1] || '';
document.head.appendChild(link);
}7. Conditional Loading with Feature Flags
7. 结合功能标志的条件加载
tsx
import { lazy, Suspense } from 'react';
import { useFeatureFlag } from '@/hooks/useFeatureFlag';
const NewDashboard = lazy(() => import('./NewDashboard'));
const LegacyDashboard = lazy(() => import('./LegacyDashboard'));
function Dashboard() {
const useNewDashboard = useFeatureFlag('new-dashboard');
return (
<Suspense fallback={<DashboardSkeleton />}>
{useNewDashboard ? <NewDashboard /> : <LegacyDashboard />}
</Suspense>
);
}tsx
import { lazy, Suspense } from 'react';
import { useFeatureFlag } from '@/hooks/useFeatureFlag';
const NewDashboard = lazy(() => import('./NewDashboard'));
const LegacyDashboard = lazy(() => import('./LegacyDashboard'));
function Dashboard() {
const useNewDashboard = useFeatureFlag('new-dashboard');
return (
<Suspense fallback={<DashboardSkeleton />}>
{useNewDashboard ? <NewDashboard /> : <LegacyDashboard />}
</Suspense>
);
}Suspense Boundaries Strategy
Suspense边界策略
tsx
// ✅ CORRECT: Granular Suspense boundaries
function Dashboard() {
return (
<div className="grid grid-cols-3 gap-4">
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<UsersChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
</div>
);
}
// ❌ WRONG: Single boundary blocks entire UI
function Dashboard() {
return (
<Suspense fallback={<FullPageSkeleton />}>
<RevenueChart />
<UsersChart />
<RecentOrders />
</Suspense>
);
}tsx
// ✅ 正确:细粒度Suspense边界
function Dashboard() {
return (
<div className="grid grid-cols-3 gap-4">
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
<Suspense fallback={<ChartSkeleton />}>
<UsersChart />
</Suspense>
<Suspense fallback={<TableSkeleton />}>
<RecentOrders />
</Suspense>
</div>
);
}
// ❌ 错误:单一边界阻塞整个UI
function Dashboard() {
return (
<Suspense fallback={<FullPageSkeleton />}>
<RevenueChart />
<UsersChart />
<RecentOrders />
</Suspense>
);
}Error Boundaries with Lazy Components
懒加载组件的错误边界
tsx
import { Component, ErrorInfo, ReactNode } from 'react';
class LazyErrorBoundary extends Component<
{ children: ReactNode; fallback: ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Lazy load failed:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
// Usage
<LazyErrorBoundary fallback={<ErrorFallback />}>
<Suspense fallback={<Skeleton />}>
<LazyComponent />
</Suspense>
</LazyErrorBoundary>tsx
import { Component, ErrorInfo, ReactNode } from 'react';
class LazyErrorBoundary extends Component<
{ children: ReactNode; fallback: ReactNode },
{ hasError: boolean }
> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('懒加载失败:', error, errorInfo);
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
// 使用方式
<LazyErrorBoundary fallback={<ErrorFallback />}>
<Suspense fallback={<Skeleton />}>
<LazyComponent />
</Suspense>
</LazyErrorBoundary>Bundle Analysis Integration
包分析集成
typescript
// vite.config.ts
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// Vendor splitting
'vendor-react': ['react', 'react-dom'],
'vendor-router': ['react-router'],
'vendor-query': ['@tanstack/react-query'],
// Feature splitting
'feature-charts': ['recharts', 'd3'],
'feature-editor': ['@tiptap/react', '@tiptap/starter-kit'],
},
},
},
},
plugins: [
visualizer({
filename: 'dist/bundle-analysis.html',
open: true,
gzipSize: true,
}),
],
});typescript
// vite.config.ts
import { defineConfig } from 'vite';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// 第三方依赖分割
'vendor-react': ['react', 'react-dom'],
'vendor-router': ['react-router'],
'vendor-query': ['@tanstack/react-query'],
// 功能模块分割
'feature-charts': ['recharts', 'd3'],
'feature-editor': ['@tiptap/react', '@tiptap/starter-kit'],
},
},
},
},
plugins: [
visualizer({
filename: 'dist/bundle-analysis.html',
open: true,
gzipSize: true,
}),
],
});Performance Budgets
性能预算
json
// package.json
{
"bundlesize": [
{ "path": "dist/assets/index-*.js", "maxSize": "80kb" },
{ "path": "dist/assets/vendor-react-*.js", "maxSize": "50kb" },
{ "path": "dist/assets/feature-*-*.js", "maxSize": "100kb" }
]
}json
// package.json
{
"bundlesize": [
{ "path": "dist/assets/index-*.js", "maxSize": "80kb" },
{ "path": "dist/assets/vendor-react-*.js", "maxSize": "50kb" },
{ "path": "dist/assets/feature-*-*.js", "maxSize": "100kb" }
]
}Anti-Patterns (FORBIDDEN)
反模式(禁止使用)
tsx
// ❌ NEVER: Lazy load small components (< 5KB)
const Button = lazy(() => import('./Button')); // Overhead > savings
// ❌ NEVER: Missing Suspense boundary
function App() {
const Chart = lazy(() => import('./Chart'));
return <Chart />; // Will throw!
}
// ❌ NEVER: Lazy inside render (creates new component each render)
function App() {
const Component = lazy(() => import('./Component')); // ❌
return <Component />;
}
// ❌ NEVER: Lazy loading critical above-fold content
const Hero = lazy(() => import('./Hero')); // Delays LCP!
// ❌ NEVER: Over-splitting (too many small chunks)
// Each chunk = 1 HTTP request = latency overhead
// ❌ NEVER: Missing error boundary for network failures
<Suspense fallback={<Skeleton />}>
<LazyComponent /> {/* What if import fails? */}
</Suspense>tsx
// ❌ 绝对不要:懒加载小型组件(<5KB)
const Button = lazy(() => import('./Button')); // 开销大于收益
// ❌ 绝对不要:缺少Suspense边界
function App() {
const Chart = lazy(() => import('./Chart'));
return <Chart />; // 会抛出错误!
}
// ❌ 绝对不要:在render函数内使用懒加载(每次渲染都会创建新组件)
function App() {
const Component = lazy(() => import('./Component')); // ❌
return <Component />;
}
// ❌ 绝对不要:懒加载可视区域内的关键内容
const Hero = lazy(() => import('./Hero')); // 会延迟最大内容绘制(LCP)!
// ❌ 绝对不要:过度分割(过多小型代码块)
// 每个代码块对应一次HTTP请求 = 额外的延迟开销
// ❌ 绝对不要:缺少处理网络失败的错误边界
<Suspense fallback={<Skeleton />}>
<LazyComponent /> {/* 如果加载失败怎么办? */}
</Suspense>Key Decisions
关键决策
| Decision | Option A | Option B | Recommendation |
|---|---|---|---|
| Splitting granularity | Per-component | Per-route | Per-route for most apps, per-component for heavy widgets |
| Prefetch strategy | On hover | On viewport | On hover for nav links, viewport for content |
| Suspense placement | Single root | Granular | Granular for independent loading |
| Skeleton vs spinner | Skeleton | Spinner | Skeleton for content, spinner for actions |
| Chunk naming | Auto-generated | Manual | Manual for debugging, auto for production |
| 决策项 | 选项A | 选项B | 推荐方案 |
|---|---|---|---|
| 分割粒度 | 按组件分割 | 按路由分割 | 按路由分割适用于大多数应用,大型组件可按组件分割 |
| 预加载策略 | 悬停时预加载 | 进入可视区域时预加载 | 导航链接推荐悬停时预加载,内容推荐进入可视区域时预加载 |
| Suspense边界位置 | 单一根边界 | 细粒度边界 | 细粒度边界用于独立加载的模块 |
| 骨架屏 vs 加载动画 | 骨架屏 | 加载动画 | 内容区域推荐骨架屏,操作按钮推荐加载动画 |
| 代码块命名 | 自动生成 | 手动命名 | 调试阶段推荐手动命名,生产环境可使用自动命名 |
Related Skills
相关技能
- - LCP optimization through lazy loading
core-web-vitals - - Vite code splitting configuration
vite-advanced - - React render performance
render-optimization - - Server-side code splitting
react-server-components-framework
- - 通过懒加载优化最大内容绘制(LCP)
core-web-vitals - - Vite代码分割配置
vite-advanced - - React渲染性能优化
render-optimization - - 服务端代码分割
react-server-components-framework
Capability Details
能力详情
component-lazy-loading
component-lazy-loading
Keywords: React.lazy, dynamic import, Suspense, code splitting
Solves: How to lazy load React components, reduce bundle size
关键词: React.lazy, dynamic import, Suspense, code splitting
解决问题: 如何懒加载React组件,减小包体积
route-splitting
route-splitting
Keywords: route, code splitting, React Router, lazy routes
Solves: Route-based code splitting, per-page bundles
关键词: route, code splitting, React Router, lazy routes
解决问题: 基于路由的代码分割,实现按页面打包
intersection-observer
intersection-observer
Keywords: scroll, viewport, lazy, IntersectionObserver, below-fold
Solves: Load components when scrolled into view
关键词: scroll, viewport, lazy, IntersectionObserver, below-fold
解决问题: 当组件滚动进入可视区域时加载
suspense-patterns
suspense-patterns
Keywords: Suspense, fallback, boundary, skeleton, loading
Solves: Proper Suspense boundary placement, skeleton loading
关键词: Suspense, fallback, boundary, skeleton, loading
解决问题: 正确的Suspense边界设置,骨架屏加载
preloading
preloading
Keywords: prefetch, preload, modulepreload, hover, intent
Solves: Preload on hover, prefetch likely navigation
关键词: prefetch, preload, modulepreload, hover, intent
解决问题: 悬停时预加载,预加载可能的导航目标
bundle-optimization
bundle-optimization
Keywords: bundle, chunks, splitting, manualChunks, vendor
Solves: Optimize bundle splitting strategy, vendor chunks
关键词: bundle, chunks, splitting, manualChunks, vendor
解决问题: 优化代码分割策略,第三方依赖打包
References
参考资料
- - Route-based code splitting patterns
references/route-splitting.md - - Scroll-triggered lazy loading
references/intersection-observer.md - - Lazy component template
scripts/lazy-component.tsx
- - 基于路由的代码分割模式
references/route-splitting.md - - 滚动触发的懒加载
references/intersection-observer.md - - 懒加载组件模板
scripts/lazy-component.tsx