lazy-loading-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Lazy Loading Patterns

懒加载模式

Code splitting and lazy loading patterns for React 19 applications using
React.lazy
,
Suspense
, route-based splitting, and intersection observer strategies.
针对React 19应用,使用
React.lazy
Suspense
、基于路由的分割和交叉观察器策略实现代码分割与懒加载的方案。

Overview

概述

  • 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)

2. React 19
use()
Hook(现代模式)

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

关键决策

DecisionOption AOption BRecommendation
Splitting granularityPer-componentPer-routePer-route for most apps, per-component for heavy widgets
Prefetch strategyOn hoverOn viewportOn hover for nav links, viewport for content
Suspense placementSingle rootGranularGranular for independent loading
Skeleton vs spinnerSkeletonSpinnerSkeleton for content, spinner for actions
Chunk namingAuto-generatedManualManual for debugging, auto for production
决策项选项A选项B推荐方案
分割粒度按组件分割按路由分割按路由分割适用于大多数应用,大型组件可按组件分割
预加载策略悬停时预加载进入可视区域时预加载导航链接推荐悬停时预加载,内容推荐进入可视区域时预加载
Suspense边界位置单一根边界细粒度边界细粒度边界用于独立加载的模块
骨架屏 vs 加载动画骨架屏加载动画内容区域推荐骨架屏,操作按钮推荐加载动画
代码块命名自动生成手动命名调试阶段推荐手动命名,生产环境可使用自动命名

Related Skills

相关技能

  • core-web-vitals
    - LCP optimization through lazy loading
  • vite-advanced
    - Vite code splitting configuration
  • render-optimization
    - React render performance
  • react-server-components-framework
    - Server-side code splitting
  • core-web-vitals
    - 通过懒加载优化最大内容绘制(LCP)
  • vite-advanced
    - Vite代码分割配置
  • render-optimization
    - React渲染性能优化
  • 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

参考资料

  • references/route-splitting.md
    - Route-based code splitting patterns
  • references/intersection-observer.md
    - Scroll-triggered lazy loading
  • scripts/lazy-component.tsx
    - Lazy component template
  • references/route-splitting.md
    - 基于路由的代码分割模式
  • references/intersection-observer.md
    - 滚动触发的懒加载
  • scripts/lazy-component.tsx
    - 懒加载组件模板