人人都会AI编程

12.5 路由懒加载与代码分割

更新时间:2026-07-11

单页应用(SPA)随着功能增多,打包后的 JavaScript 文件会越来越庞大。如果用户首次加载时就下载整个应用的所有代码,会导致白屏时间长、首屏体验差。路由懒加载与代码分割正是解决这一问题的核心手段:仅加载当前页面所需的代码,其他页面的代码按需加载

为什么需要路由懒加载

假设你的应用包含首页、用户管理、报表分析、系统设置四个模块,每个模块有自己独立的组件树和依赖。如果不做代码分割:

  • 打包后的 bundle.js 可能超过 1MB。
  • 用户访问首页时,却要强制下载报表分析、系统设置等暂时用不到的代码。
  • 在弱网环境下,加载时间可能长达数秒,严重影响用户体验。

路由懒加载让每个页面成为独立的代码块(chunk),当用户导航到某个页面时,才动态加载该页面对应的 JavaScript 文件。这样做带来的直接好处:

  • 首屏加载更快:初始只加载必要代码。
  • 按需加载:用户访问得越深,才加载相应模块,节省带宽。
  • 缓存更有效:某个页面的代码更新,只影响对应 chunk,其他 chunk 仍可被浏览器缓存。

实现方式:React.lazy + Suspense + 动态 import

React 提供了 React.lazy() 函数用于定义懒加载组件,配合 Suspense 处理加载中的状态。结合 React Router v6,可以轻松实现路由级别的代码分割。

基础示例

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

// 使用动态 import() 懒加载页面组件
const HomePage = lazy(() => import('./pages/HomePage'));
const UserManagePage = lazy(() => import('./pages/UserManagePage'));
const ReportPage = lazy(() => import('./pages/ReportPage'));
const SettingsPage = lazy(() => import('./pages/SettingsPage'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>页面加载中...</div>}>
        <Routes>
          <Route path="/" element={<HomePage />} />
          <Route path="/users" element={<UserManagePage />} />
          <Route path="/reports" element={<ReportPage />} />
          <Route path="/settings" element={<SettingsPage />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

当用户访问 /users 时,浏览器才会请求 UserManagePage 对应的 JS 文件。在加载期间,Suspensefallback 属性指定的内容(如加载提示、骨架屏)会显示出来,直到组件加载完成后自动替换为实际内容。

在实际项目中的最佳实践

1. 统一管理路由配置

通常会将路由配置抽离成一个数组,方便维护和批量生成:

import { lazy } from 'react';

const routes = [
  {
    path: '/',
    element: lazy(() => import('./pages/HomePage')),
  },
  {
    path: '/users',
    element: lazy(() => import('./pages/UserManagePage')),
    children: [
      {
        path: ':id',
        element: lazy(() => import('./pages/UserDetailPage')),
      },
    ],
  },
  // ...其他路由
];

在渲染时遍历 routes 生成 <Route> 元素。这种方式让路由管理集中在单一文件,增删路由一目了然。

2. 使用更友好的 fallback 界面

简单的 "加载中..." 文字可能让用户感觉生硬,建议使用骨架屏或品牌化的加载动画:

function LoadingSpinner() {
  return (
    <div className="loading-container">
      <Spin size="large" tip="正在努力加载页面..." />
    </div>
  );
}

// 在 Suspense 中使用
<Suspense fallback={<LoadingSpinner />}>
  <Routes>{/* ... */}</Routes>
</Suspense>

对于企业级应用,保持加载态的品牌统一性可以提升整体体验。

3. 错误边界兜底

懒加载也可能因为网络错误、chunk 文件丢失等原因加载失败。仅用 Suspense 无法捕获这类错误,需要配合错误边界(Error Boundary)

class ChunkErrorBoundary extends React.Component {
  state = { hasError: false, error: null };

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

  handleRetry = () => {
    this.setState({ hasError: false, error: null });
  };

  render() {
    if (this.state.hasError) {
      return (
        <ErrorResult
          title="页面加载失败"
          description="请检查网络连接或刷新重试"
          onRetry={this.handleRetry}
        />
      );
    }
    return this.props.children;
  }
}

// 包裹 Suspense
<ChunkErrorBoundary>
  <Suspense fallback={<LoadingSpinner />}>
    <Routes>{/* ... */}</Routes>
  </Suspense>
</ChunkErrorBoundary>

这样即使加载失败,用户也能看到友好的提示并提供重试按钮,而不是面对一个白屏。

4. 嵌套路由的懒加载

React Router v6 支持嵌套路由,子路由也可以懒加载。注意,父组件加载后才能匹配子路由,因此父组件本身也需要被 lazy 包裹,或者使用 Suspense 包裹嵌套的 <Outlet />

// UserLayout.tsx
import { Outlet } from 'react-router-dom';

export default function UserLayout() {
  return (
    <div>
      <h2>用户管理</h2>
      <Suspense fallback={<p>加载用户详情...</p>}>
        <Outlet />
      </Suspense>
    </div>
  );
}

这种方式能进一步精细控制加载粒度,避免加载一个父页面时连带下载所有子页面。

与构建工具的配合

代码分割依赖于构建工具(如 Vite、Webpack)对动态 import() 语法的支持。当你使用 lazy(() => import('./pages/UserManagePage')) 时,构建工具会自动将该文件及其依赖提取为独立的 chunk 文件。

Vite 的代码分割默认基于动态导入,无需额外配置。若想手动控制 chunk 名称,可以使用魔法注释:

const UserPage = lazy(() => import(/* webpackChunkName: "user-page" */ './pages/UserPage'));

Vite 则通过输出配置来控制:

// vite.config.js
export default {
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            return 'vendor';
          }
        }
      }
    }
  }
};

合理拆分 vendor(第三方库)和应用代码,可以进一步优化缓存和加载性能。

什么时候不需要懒加载

路由懒加载虽然好,但并非所有页面都值得拆分:

  • 首屏关键页面:比如首页本身就是用户最先看到的,懒加载会增加一次额外的请求,反而可能拖慢首屏。这类页面可与主入口打包在一起,或使用预加载策略。
  • 极小页面:几 KB 的组件拆分后的请求开销可能比代码本身还大,可以保留在同一个 bundle 中。
  • 高频访问且体积不大的页面:频繁的下载请求反而增加网络负担,可以结合预加载(preload/prefetch)优化。

使用预加载提升体验

对于可能在后续流程中访问的页面,可以在用户悬停或空闲时提前加载其 chunk:

import { lazy } from 'react';
const SettingsPage = lazy(() => import('./pages/SettingsPage'));

// 在用户鼠标移入导航菜单时预加载
<Link
  to="/settings"
  onMouseEnter={() => {
    import(/* webpackPrefetch: true */ './pages/SettingsPage');
  }}
>
  系统设置
</Link>

Webpack 的 / webpackPrefetch: true / 注释会让浏览器在空闲时预加载该资源,这样当用户真正点击时,组件几乎瞬间可用。在 Vite 中,你可以使用 <link rel="prefetch">import() 提前触发请求。

总结

路由懒加载与代码分割是现代前端应用性能优化的标配手段。核心要点:

  • 使用 React.lazy 和动态 import() 定义懒加载组件。
  • Suspense 提供加载态 UI。
  • 配合错误边界处理加载异常,保证应用健壮性。
  • 结合构建工具合理拆分 chunk,平衡加载颗粒度。
  • 对于关键页面考虑预加载,避免影响导航体验。

实施路由懒加载后,你的应用将具备“按需分配”的能力,让用户只为他们当前看到的内容买单,显著提升首屏加载速度和整体性能。