首页 / React 19 入门教程 / 代码分割

React 19 入门教程

代码分割

本教程共 50 篇 · 第 42 篇 · 更新于 2026-07-29 · 约 6 分钟阅读

React代码分割lazy动态import性能优化首屏加载

42. 代码分割

本节目标:学会用 React.lazy 把组件拆成按需加载的 chunk,减少首屏下载体积,提升页面加载速度。

为什么要代码分割

默认情况下,Vite 会把所有组件打包成一个 index.js。组件少的时候没问题,一旦上了规模——图表库、富文本编辑器、地图组件——这个文件就膨胀了。

用户打开首页,得下载整个应用的 JS,包括他可能永远不会看的”设置页面”和”数据报表”。浪费带宽,首屏还慢。

代码分割的思路:只下载当前页面需要的代码,其他部分等用到时再下载。

React.lazy:声明式懒加载

React.lazy 接受一个返回动态 import() 的函数,返回一个可以正常渲染的组件:

import { lazy, Suspense } from 'react';

// 静态 import:打包进主文件
// import Chart from './Chart.js';

// 动态 import:拆成单独文件,按需加载
const Chart = lazy(() => import('./Chart.js'));

function Dashboard() {
  return (
    <Suspense fallback={<div>图表加载中……</div>}>
      <Chart />
    </Suspense>
  );
}

Vite 看到 import('./Chart.js') 会自动把 Chart 拆成单独的 JS 文件。只有 Chart 第一次被渲染时才会下载。

Note

lazyimport() 返回的模块必须有 default 导出。所以被懒加载的组件要用 export default function Chart() 这种写法。

按路由分割

最常见的用法是按路由分割代码:

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

const Home = lazy(() => import('./pages/Home.js'));
const Dashboard = lazy(() => import('./pages/Dashboard.js'));
const Settings = lazy(() => import('./pages/Settings.js'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<p>页面加载中……</p>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/settings" element={<Settings />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

用户访问 / 只下载 Home 的代码。点进 /dashboard 才下载 Dashboard 的代码。每个路由都是独立的 chunk。

条件加载

不只是路由,任何”不立即需要”的组件都可以懒加载:

import { lazy, Suspense, useState } from 'react';

const HeavyModal = lazy(() => import('./HeavyModal.js'));

function Page() {
  const [showModal, setShowModal] = useState(false);

  return (
    <div>
      <button onClick={() => setShowModal(true)}>打开弹窗</button>
      {showModal && (
        <Suspense fallback={null}>
          <HeavyModal onClose={() => setShowModal(false)} />
        </Suspense>
      )}
    </div>
  );
}

弹窗的 JS 只有用户点击”打开”时才下载。不点就不下载。

预加载:提前下载

懒加载有个问题——用户点击后要等代码下载,体验不好。解决办法是预加载:在用户可能操作之前就开始下载。

const Chart = lazy(() => import('./Chart.js'));

function Dashboard() {
  return (
    <div>
      <button
        onMouseEnter={() => import('./Chart.js')}  // 鼠标悬停时开始下载
        onClick={() => setShowChart(true)}
      >
        显示图表
      </button>
      {showChart && (
        <Suspense fallback={<div>加载中……</div>}>
          <Chart />
        </Suspense>
      )}
    </div>
  );
}

鼠标悬停到按钮上时就开始下载 Chart 的代码。等用户真正点击下载早就完成了,零等待。

也可以用 useEffect 在空闲时预加载:

useEffect(() => {
  // 页面加载完成后,空闲时预加载
  requestIdleCallback(() => import('./Chart.js'));
}, []);
Tip

预加载不是银弹。别一次性预加载太多文件,否则和没分割一样。只预加载用户最可能访问的 1-2 个模块。

命名 chunk

默认情况下,Vite 给 chunk 起数字名 0.js1.js。调试时看不出哪个是哪个。可以用魔法注释命名:

const Chart = lazy(() => import(/* webpackChunkName: "chart" */ './Chart.js'));

Vite 也支持这个注释,会生成 chart.js 而不是 0.js

常见坑

lazy 不能在组件内部声明

如 §37 所述,lazy 必须在模块顶层声明。在组件内部声明会导致每次渲染创建新的组件类型,所有 state 都丢了。

lazy 组件必须用 Suspense 包裹

// ❌ 错误:没有 Suspense,lazy 组件会报错
function App() {
  return <Chart />;
}

// ✅ 正确
function App() {
  return (
    <Suspense fallback={<div>加载中</div>}>
      <Chart />
    </Suspense>
  );
}

Chart 的代码还没下载完时,渲染会”挂起”(suspend)。没有 Suspense 边界捕获这个挂起,React 就会报错。

服务端渲染的兼容

React.lazy 不支持服务端渲染(SSR)。如果你的应用用了 SSR(比如 Next.js),需要用 @loadable/component 这类库替代。

分割策略

不是所有组件都该懒加载。分割太细反而不好——每个 chunk 都有请求开销。

好的分割策略:

  1. 按路由分割:每个路由页面一个 chunk,最基本也最有效
  2. 按功能分割:图表、编辑器、地图这些重组件单独分割
  3. 按条件分割:弹窗、折叠面板、Tab 内容这些不立即显示的分割

不该分割的:

  • 首屏必须显示的组件
  • 很小的组件(几十字节,分割了反而多一个请求)
  • 频繁切换的组件(每次切换都重新下载体验差)
Tip

vite-bundle-visualizer 看看你的打包结果,找出最大的 chunk,针对性分割。