人人都会AI编程

React.lazy + Suspense 路由级代码分割

更新时间:2026-07-10

在现代单页应用中,所有 JavaScript 代码通常会被打包成一个或多个 bundle。如果不做任何处理,用户首次访问时会下载整个应用的代码,导致首屏加载缓慢,尤其在应用体积较大时更为明显。路由级代码分割就是将不同路由对应的组件拆分成独立的代码块(chunk),仅当用户访问该路由时才动态加载对应的代码。

React 提供了 lazy 函数和 Suspense 组件,使得组件级别的代码分割变得异常简单,无需手动配置复杂的打包逻辑。

基本用法:搭配 React Router

假设有一个包含首页、关于页和用户页的应用,我们可以按路由对页面组件进行懒加载:

import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

// 使用 dynamic import 语法进行懒加载
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const User = lazy(() => import('./pages/User'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/user/:id" element={<User />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

当用户首次访问 / 时,只有 Home 组件的代码会被加载;切换到 /about 时,浏览器才会去下载 About 组件的对应 chunk。Suspensefallback 属性用于指定在组件加载过程中显示的占位界面(例如一个加载动画)。

对于使用旧版 react-router-dom v5 的用户,也可以结合 SwitchRoute 使用:

<Switch>
  <Suspense fallback={<div>Loading...</div>}>
    <Route exact path="/" component={Home} />
    <Route path="/about" component={About} />
  </Suspense>
</Switch>

如何处理加载失败

lazy 组件在加载过程中如果网络出现问题或者 chunk 加载失败,会导致组件渲染错误。我们可以使用 Error Boundary 包裹 Suspense 来统一处理这类异常:

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

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

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

function App() {
  return (
    <ErrorBoundary>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>...</Routes>
      </Suspense>
    </ErrorBoundary>
  );
}

命名 chunk 便于调试

默认情况下,打包工具(如 Vite 或 Webpack)会为懒加载的 chunk 生成数字或哈希文件名,难以识别。可以在 import() 中使用魔法注释指定 chunk 名称:

const About = lazy(() => import(/* webpackChunkName: "about" */ './pages/About'));

使用 Vite 时,可以直接利用动态导入,Vite 会自动根据文件路径生成有意义的名称,一般不需要手动干预。

加载指示器的用户体验优化

路由切换时的 loading 状态如果只是简单显示“Loading...”,体验仍显生硬。通常我们可以:

  • 使用骨架屏(Skeleton)代替简单的文字
  • 使用顶部的进度条(如 NProgress)
  • 利用 Suspense 的边界做更细粒度的控制

例如,在页面布局中,可以仅在内容区域显示加载状态,而保持 Header 和 Sidebar 始终可见:

function App() {
  return (
    <div className="layout">
      <Header />
      <Sidebar />
      <main>
        <Suspense fallback={<PageSkeleton />}>
          <Routes>
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />
          </Routes>
        </Suspense>
      </main>
    </div>
  );
}

延迟加载的时机与预加载

React.lazy按需加载,只有组件真正开始渲染时才会发起网络请求。这可能导致用户点击路由后仍然需要等待 chunk 下载,造成短暂的延迟。对于某些用户大概率会访问的页面,可以提前预加载(Prefetch)对应的 chunk:

// 鼠标悬停时预加载
<Link
  to="/about"
  onMouseEnter={() => import('./pages/About')}
>
  关于
</Link>

当用户鼠标悬停到链接上时,浏览器会在后台下载 About 页面的代码,点击后几乎可以瞬间渲染。

Webpack 和 Vite 的注意事项

  • Webpack:需确保 @babel/plugin-syntax-dynamic-import 已配置(通常 Create React App 和多数脚手架已默认启用)。
  • Vite:天然支持 import() 动态导入,无需额外配置,但注意在开发环境下 lazy 组件仍会触发网络请求,生产构建后才会真正分割。

避免过度分割

路由级代码分割虽然能减小初始包体积,但也不宜分割过细。如果一个路由页面的子组件也被 lazy 拆分,可能会导致页面上出现多次 loading 闪烁。合理的粒度通常是以路由为单元进行分割,对特别大的页面内部组件(如重型图表、富文本编辑器)可以再单独再懒加载。

通过 React.lazySuspense,路由级代码分割几乎零配置即可引入,能显著提升大型应用的首次加载速度和用户体验,是 React 项目性能优化的必选项之一。