React.lazy 和 Suspense 实现动态加载与优化

2 阅读2分钟

React 版本:18.x 难度:中级

考察要点

  1. React.lazy 和 Suspense 的工作原理
  2. 动态导入和代码分割策略
  3. 加载状态和错误处理
  4. 性能优化和预加载技术

解答:

1. 概念解释

基本定义

  • React.lazy:允许动态导入组件的函数
  • Suspense:在组件加载时显示后备 UI 的组件
  • 代码分割:将应用拆分成多个包,按需加载

工作原理

  • React.lazy 返回一个 Promise
  • Suspense 捕获加载状态
  • 自动处理加载和错误边界

应用场景

  • 路由级别代码分割
  • 大型组件按需加载
  • 条件渲染的复杂组件

2. 代码示例

基础示例:
import React, { Suspense } from 'react';

// 👉 基础的动态导入
const LazyComponent = React.lazy(() => 
  import('./HeavyComponent')
);

// 👉 基础加载组件
const LoadingSpinner: React.FC = () => (
  <div className="loading-spinner">Loading...</div>
);

// 👉 使用 Suspense 包装懒加载组件
export const BasicExample: React.FC = () => {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <LazyComponent />
    </Suspense>
  );
};
进阶示例:
import React, { Suspense, useState, useTransition } from 'react';

// 👉 定义错误边界组件
class ErrorBoundary extends React.Component<
  { children: React.ReactNode },
  { hasError: boolean }
> {
  constructor(props: { children: React.ReactNode }) {
    super(props);
    this.state = { hasError: false };
  }

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

  render() {
    if (this.state.hasError) {
      return <div>Something went wrong. Please try again.</div>;
    }
    return this.props.children;
  }
}

// 👉 懒加载组件包装器
interface LazyComponentProps {
  componentPath: string;
}

const LazyLoadWrapper: React.FC<LazyComponentProps> = ({ componentPath }) => {
  const [isPending, startTransition] = useTransition();
  const LazyComponent = React.lazy(() => import(componentPath));

  return (
    <div>
      {isPending && <LoadingSpinner />}
      <ErrorBoundary>
        <Suspense fallback={<LoadingSpinner />}>
          <LazyComponent />
        </Suspense>
      </ErrorBoundary>
    </div>
  );
};

// 👉 使用示例
export const AdvancedExample: React.FC = () => {
  const [showComponent, setShowComponent] = useState(false);

  const handleClick = () => {
    startTransition(() => {
      setShowComponent(true);
    });
  };

  return (
    <div>
      <button onClick={handleClick}>Load Component</button>
      {showComponent && (
        <LazyLoadWrapper componentPath="./HeavyComponent" />
      )}
    </div>
  );
};

3. 注意事项与最佳实践

❌ 常见错误示例

// ❌ 错误示范:在条件语句中使用 React.lazy
const BadComponent: React.FC<{ condition: boolean }> = ({ condition }) => {
  const DynamicComponent = condition
    ? React.lazy(() => import('./ComponentA'))
    : React.lazy(() => import('./ComponentB'));

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <DynamicComponent />
    </Suspense>
  );
};

// ❌ 错误示范:没有错误处理
const AnotherBadComponent: React.FC = () => {
  const LazyComponent = React.lazy(() => import('./NonExistentComponent'));

  return (
    <Suspense fallback={<div>Loading...</div>}>
      <LazyComponent />
    </Suspense>
  );
};

✅ 正确实现方式

// ✅ 正确示范:预定义懒加载组件
const ComponentA = React.lazy(() => import('./ComponentA'));
const ComponentB = React.lazy(() => import('./ComponentB'));

const GoodComponent: React.FC<{ condition: boolean }> = ({ condition }) => {
  return (
    <ErrorBoundary>
      <Suspense fallback={<div>Loading...</div>}>
        {condition ? <ComponentA /> : <ComponentB />}
      </Suspense>
    </ErrorBoundary>
  );
};

// ✅ 正确示范:使用预加载
const PreloadableComponent = {
  Component: React.lazy(() => import('./HeavyComponent')),
  preload: () => import('./HeavyComponent')
};

const AnotherGoodComponent: React.FC = () => {
  const handleMouseEnter = () => {
    // 鼠标悬停时预加载
    PreloadableComponent.preload();
  };

  return (
    <div onMouseEnter={handleMouseEnter}>
      <Suspense fallback={<LoadingSpinner />}>
        <PreloadableComponent.Component />
      </Suspense>
    </div>
  );
};

4. 性能优化

import React, { Suspense, useCallback, useMemo } from 'react';

// 👉 优化的加载策略
const useOptimizedLazyLoad = (componentPath: string) => {
  // 使用 useMemo 缓存懒加载组件
  const LazyComponent = useMemo(
    () => React.lazy(() => import(componentPath)),
    [componentPath]
  );

  // 预加载函数
  const preload = useCallback(() => {
    import(componentPath);
  }, [componentPath]);

  return { LazyComponent, preload };
};

// 👉 实现组件
export const OptimizedComponent: React.FC = () => {
  const { LazyComponent, preload } = useOptimizedLazyLoad('./HeavyComponent');

  return (
    <div onMouseEnter={preload}>
      <Suspense fallback={<LoadingSpinner />}>
        <LazyComponent />
      </Suspense>
    </div>
  );
};

5. 测试策略

import { render, screen, fireEvent } from '@testing-library/react';
import { act } from 'react-dom/test-utils';

describe('LazyLoadComponent', () => {
  it('should show loading state initially', () => {
    render(<OptimizedComponent />);
    expect(screen.getByText('Loading...')).toBeInTheDocument();
  });

  it('should load component on mouse enter', async () => {
    render(<OptimizedComponent />);
    
    await act(async () => {
      fireEvent.mouseEnter(screen.getByRole('div'));
    });

    // 等待组件加载
    const component = await screen.findByTestId('heavy-component');
    expect(component).toBeInTheDocument();
  });
});

6. 实际应用示例

import React, { Suspense, useTransition } from 'react';
import { Routes, Route } from 'react-router-dom';

// 👉 路由级别的代码分割
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Profile = React.lazy(() => import('./pages/Profile'));
const Settings = React.lazy(() => import('./pages/Settings'));

// 👉 加载状态组件
const PageLoader: React.FC = () => (
  <div className="page-loader">
    <LoadingSpinner />
    <p>Loading page...</p>
  </div>
);

// 👉 路由配置
export const AppRoutes: React.FC = () => {
  const [isPending, startTransition] = useTransition();

  return (
    <>
      {isPending && <PageLoader />}
      <ErrorBoundary>
        <Suspense fallback={<PageLoader />}>
          <Routes>
            <Route path="/dashboard" element={<Dashboard />} />
            <Route path="/profile" element={<Profile />} />
            <Route path="/settings" element={<Settings />} />
          </Routes>
        </Suspense>
      </ErrorBoundary>
    </>
  );
};