Skip to content

代码分割(lazy-import)

第一部分:代码分割基础

1.1 什么是代码分割

代码分割(Code Splitting)是将应用代码拆分成多个bundle的技术,按需加载所需代码,而不是一次性加载整个应用,从而减少初始加载时间,提升性能。

核心概念:

javascript
// 传统打包:单一bundle
import ComponentA from './ComponentA';
import ComponentB from './ComponentB';
import ComponentC from './ComponentC';

function App() {
  return (
    <div>
      <ComponentA />
      <ComponentB />
      <ComponentC />
    </div>
  );
}
// 问题:bundle.js 包含所有代码,体积大

// 代码分割:按需加载
const ComponentA = lazy(() => import('./ComponentA'));
const ComponentB = lazy(() => import('./ComponentB'));
const ComponentC = lazy(() => import('./ComponentC'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      <ComponentA />
      <ComponentB />
      <ComponentC />
    </Suspense>
  );
}
// 优势:拆分成多个小chunk,按需加载

1.2 代码分割的优势

javascript
// 1. 性能提升
// - 减少初始bundle大小
// - 加快首屏加载速度
// - 降低Time to Interactive (TTI)
// - 优化Core Web Vitals

// 2. 用户体验
// - 更快的页面响应
// - 减少白屏时间
// - 流畅的交互体验
// - 节省用户带宽

// 3. 开发效率
// - 模块化开发
// - 独立部署
// - 更好的缓存策略
// - 易于维护

// 示例对比
// 传统方式
// main.js: 500KB (包含所有代码)
// 加载时间: 3-5秒

// 代码分割后
// main.js: 100KB (核心代码)
// chunk-1.js: 150KB (路由A)
// chunk-2.js: 150KB (路由B)
// chunk-3.js: 100KB (路由C)
// 初始加载时间: 1秒

1.3 React.lazy基础

javascript
// React.lazy语法
const LazyComponent = React.lazy(() => import('./Component'));

// 基本使用
import { lazy, Suspense } from 'react';

const About = lazy(() => import('./pages/About'));

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

// 多个lazy组件
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));

function App() {
  return (
    <Suspense fallback={<PageLoader />}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
      </Routes>
    </Suspense>
  );
}

// 命名导出处理
// Component.js
export function MyComponent() {
  return <div>My Component</div>;
}

// App.js
const MyComponent = lazy(() => 
  import('./Component').then(module => ({
    default: module.MyComponent
  }))
);

// 错误处理
const SafeComponent = lazy(() => 
  import('./Component').catch(error => {
    console.error('Failed to load component:', error);
    return { default: () => <div>Failed to load</div> };
  })
);

1.4 动态import()

javascript
// 基础动态导入
button.addEventListener('click', async () => {
  const module = await import('./module.js');
  module.default();
});

// React中使用
function Component() {
  const [Module, setModule] = useState(null);
  
  const loadModule = async () => {
    const { default: LoadedModule } = await import('./Module');
    setModule(() => LoadedModule);
  };
  
  return (
    <div>
      <button onClick={loadModule}>Load Module</button>
      {Module && <Module />}
    </div>
  );
}

// 条件导入
async function loadEditor(type) {
  if (type === 'rich') {
    return import('./RichTextEditor');
  } else if (type === 'markdown') {
    return import('./MarkdownEditor');
  } else {
    return import('./PlainTextEditor');
  }
}

// 并行导入
async function loadMultiple() {
  const [moduleA, moduleB, moduleC] = await Promise.all([
    import('./ModuleA'),
    import('./ModuleB'),
    import('./ModuleC')
  ]);
  
  return {
    A: moduleA.default,
    B: moduleB.default,
    C: moduleC.default
  };
}

第二部分:分割策略

2.1 路由级分割

javascript
// React Router + lazy
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { lazy, Suspense } from 'react';

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

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<PageLoader />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/dashboard" element={<Dashboard />} />
          <Route path="/profile" element={<Profile />} />
          <Route path="/settings" element={<Settings />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

// 嵌套路由分割
const UserRoutes = lazy(() => import('./routes/UserRoutes'));
const AdminRoutes = lazy(() => import('./routes/AdminRoutes'));

function App() {
  return (
    <Suspense fallback={<RouteLoader />}>
      <Routes>
        <Route path="/user/*" element={<UserRoutes />} />
        <Route path="/admin/*" element={<AdminRoutes />} />
      </Routes>
    </Suspense>
  );
}

// 每个路由独立fallback
function App() {
  return (
    <Routes>
      <Route
        path="/"
        element={
          <Suspense fallback={<HomeLoader />}>
            <Home />
          </Suspense>
        }
      />
      <Route
        path="/dashboard"
        element={
          <Suspense fallback={<DashboardLoader />}>
            <Dashboard />
          </Suspense>
        }
      />
    </Routes>
  );
}

// 路由预加载
const routes = [
  { path: '/', component: lazy(() => import('./Home')) },
  { path: '/about', component: lazy(() => import('./About')) }
];

// 添加preload方法
function lazyWithPreload(importFunc) {
  const LazyComponent = lazy(importFunc);
  LazyComponent.preload = importFunc;
  return LazyComponent;
}

const About = lazyWithPreload(() => import('./About'));

// 预加载使用
function NavLink({ to }) {
  const handleMouseEnter = () => {
    About.preload();
  };
  
  return (
    <Link to={to} onMouseEnter={handleMouseEnter}>
      About
    </Link>
  );
}

2.2 组件级分割

javascript
// 按功能分割
const Header = lazy(() => import('./components/Header'));
const Footer = lazy(() => import('./components/Footer'));
const Sidebar = lazy(() => import('./components/Sidebar'));

function Layout() {
  return (
    <div>
      <Suspense fallback={<HeaderSkeleton />}>
        <Header />
      </Suspense>
      
      <main>
        <Suspense fallback={<SidebarSkeleton />}>
          <Sidebar />
        </Suspense>
        
        <Content />
      </main>
      
      <Suspense fallback={<FooterSkeleton />}>
        <Footer />
      </Suspense>
    </div>
  );
}

// 重量级组件分割
const Chart = lazy(() => import('./components/Chart'));
const DataTable = lazy(() => import('./components/DataTable'));
const Editor = lazy(() => import('./components/Editor'));

function Dashboard() {
  return (
    <div className="dashboard">
      <Suspense fallback={<ChartSkeleton />}>
        <Chart data={chartData} />
      </Suspense>
      
      <Suspense fallback={<TableSkeleton />}>
        <DataTable data={tableData} />
      </Suspense>
      
      <Suspense fallback={<EditorSkeleton />}>
        <Editor content={content} />
      </Suspense>
    </div>
  );
}

// 模态框分割
const Modal = lazy(() => import('./components/Modal'));

function App() {
  const [showModal, setShowModal] = useState(false);
  
  return (
    <div>
      <button onClick={() => setShowModal(true)}>
        Open Modal
      </button>
      
      {showModal && (
        <Suspense fallback={<ModalLoader />}>
          <Modal onClose={() => setShowModal(false)} />
        </Suspense>
      )}
    </div>
  );
}

// 条件加载
function ConditionalSplit({ userType }) {
  const AdminPanel = lazy(() => import('./AdminPanel'));
  const UserPanel = lazy(() => import('./UserPanel'));
  
  return (
    <Suspense fallback={<PanelLoader />}>
      {userType === 'admin' ? <AdminPanel /> : <UserPanel />}
    </Suspense>
  );
}

2.3 第三方库分割

javascript
// 重量级库按需加载
// Moment.js示例
function DateComponent() {
  const [moment, setMoment] = useState(null);
  
  useEffect(() => {
    import('moment').then(mod => {
      setMoment(() => mod.default);
    });
  }, []);
  
  if (!moment) return <div>Loading...</div>;
  
  return <div>{moment().format('YYYY-MM-DD')}</div>;
}

// Chart.js懒加载
const ChartComponent = lazy(() => 
  import('react-chartjs-2').then(module => ({
    default: module.Line
  }))
);

function Analytics() {
  return (
    <Suspense fallback={<ChartSkeleton />}>
      <ChartComponent data={chartData} />
    </Suspense>
  );
}

// Lodash函数级导入
button.addEventListener('click', async () => {
  const { debounce } = await import('lodash-es');
  const debouncedFn = debounce(myFunction, 300);
  debouncedFn();
});

// 图标库按需加载
const iconModules = {
  home: () => import('@icons/home'),
  user: () => import('@icons/user'),
  settings: () => import('@icons/settings')
};

function Icon({ name }) {
  const [IconComponent, setIconComponent] = useState(null);
  
  useEffect(() => {
    iconModules[name]().then(mod => {
      setIconComponent(() => mod.default);
    });
  }, [name]);
  
  if (!IconComponent) return null;
  
  return <IconComponent />;
}

// 国际化文件分割
async function loadLocale(locale) {
  const messages = await import(`./locales/${locale}.json`);
  return messages.default;
}

function I18nProvider({ locale, children }) {
  const [messages, setMessages] = useState(null);
  
  useEffect(() => {
    loadLocale(locale).then(setMessages);
  }, [locale]);
  
  if (!messages) return <Loading />;
  
  return (
    <IntlProvider locale={locale} messages={messages}>
      {children}
    </IntlProvider>
  );
}

2.4 Webpack魔法注释

javascript
// webpackChunkName: 命名chunk
const About = lazy(() => 
  import(/* webpackChunkName: "about" */ './pages/About')
);
// 生成: about.chunk.js

// webpackPrefetch: 预获取
const Dashboard = lazy(() =>
  import(
    /* webpackChunkName: "dashboard" */
    /* webpackPrefetch: true */
    './pages/Dashboard'
  )
);
// 浏览器空闲时预获取

// webpackPreload: 预加载
const Critical = lazy(() =>
  import(
    /* webpackChunkName: "critical" */
    /* webpackPreload: true */
    './components/Critical'
  )
);
// 父chunk加载时并行预加载

// webpackMode: 导入模式
const DynamicComponent = lazy(() =>
  import(
    /* webpackMode: "lazy" */
    `./components/${componentName}`
  )
);

// 组合使用
const AdminPanel = lazy(() =>
  import(
    /* webpackChunkName: "admin-panel" */
    /* webpackPrefetch: true */
    /* webpackPreload: true */
    './pages/AdminPanel'
  )
);

// 按组分割
const UserRoutes = lazy(() =>
  import(/* webpackChunkName: "user-routes" */ './routes/UserRoutes')
);

const AdminRoutes = lazy(() =>
  import(/* webpackChunkName: "admin-routes" */ './routes/AdminRoutes')
);

const PublicRoutes = lazy(() =>
  import(/* webpackChunkName: "public-routes" */ './routes/PublicRoutes')
);

第三部分:高级技巧

3.1 预加载策略

javascript
// 路由预加载
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

const routeComponents = {
  '/': () => import('./pages/Home'),
  '/about': () => import('./pages/About'),
  '/dashboard': () => import('./pages/Dashboard')
};

function RoutePreloader() {
  const location = useLocation();
  
  useEffect(() => {
    // 预加载相关路由
    const relatedRoutes = getRelatedRoutes(location.pathname);
    
    relatedRoutes.forEach(route => {
      if (routeComponents[route]) {
        routeComponents[route]();
      }
    });
  }, [location]);
  
  return null;
}

function getRelatedRoutes(currentPath) {
  const routeMap = {
    '/': ['/about', '/contact'],
    '/products': ['/products/details', '/cart'],
    '/dashboard': ['/profile', '/settings']
  };
  
  return routeMap[currentPath] || [];
}

// 交互预加载
function NavLink({ to, children }) {
  const handleMouseEnter = () => {
    if (routeComponents[to]) {
      routeComponents[to]();
    }
  };
  
  return (
    <Link to={to} onMouseEnter={handleMouseEnter}>
      {children}
    </Link>
  );
}

// 智能预加载
function SmartPreloader() {
  useEffect(() => {
    // 网络空闲时预加载
    if ('requestIdleCallback' in window) {
      requestIdleCallback(() => {
        Object.values(routeComponents).forEach(loader => {
          loader();
        });
      });
    }
  }, []);
  
  return null;
}

// 基于用户行为预加载
function BehaviorBasedPreload() {
  const [userIntent, setUserIntent] = useState(null);
  
  useEffect(() => {
    const handleMouseMove = (e) => {
      // 检测鼠标移向导航区域
      if (e.clientY < 100) {
        setUserIntent('navigation');
      }
    };
    
    document.addEventListener('mousemove', handleMouseMove);
    
    return () => {
      document.removeEventListener('mousemove', handleMouseMove);
    };
  }, []);
  
  useEffect(() => {
    if (userIntent === 'navigation') {
      // 预加载所有路由
      Object.values(routeComponents).forEach(loader => loader());
    }
  }, [userIntent]);
  
  return null;
}

3.2 错误处理

javascript
// 错误边界 + Suspense
class LazyErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    if (error.name === 'ChunkLoadError') {
      console.error('Chunk load failed, reloading page...');
      window.location.reload();
    }
  }
  
  render() {
    if (this.state.hasError) {
      return (
        <div>
          <h2>加载失败</h2>
          <button onClick={() => window.location.reload()}>
            刷新页面
          </button>
        </div>
      );
    }
    
    return this.props.children;
  }
}

// 使用
function App() {
  return (
    <LazyErrorBoundary>
      <Suspense fallback={<Loading />}>
        <LazyComponent />
      </Suspense>
    </LazyErrorBoundary>
  );
}

// 重试机制
function lazyWithRetry(importFunc, retries = 3) {
  return lazy(() => {
    return new Promise((resolve, reject) => {
      const attempt = (retriesLeft) => {
        importFunc()
          .then(resolve)
          .catch((error) => {
            if (retriesLeft === 0) {
              reject(error);
              return;
            }
            
            setTimeout(() => {
              attempt(retriesLeft - 1);
            }, 1000);
          });
      };
      
      attempt(retries);
    });
  });
}

// 使用
const RobustComponent = lazyWithRetry(
  () => import('./Component'),
  3
);

// 降级处理
function LazyWithFallback() {
  const [Component, setComponent] = useState(null);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    import('./Component')
      .then(mod => setComponent(() => mod.default))
      .catch(err => {
        setError(err);
        // 降级到简单组件
        setComponent(() => SimpleFallbackComponent);
      });
  }, []);
  
  if (error) {
    console.error('Failed to load component:', error);
  }
  
  if (!Component) return <Loading />;
  
  return <Component />;
}

3.3 性能优化

javascript
// bundle分析
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      reportFilename: 'bundle-report.html'
    })
  ]
};

// 动态chunk大小优化
// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 20000,
      maxSize: 244000,
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10
        },
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true
        }
      }
    }
  }
};

// 代码压缩
// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');

module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true,
            pure_funcs: ['console.log']
          }
        }
      })
    ]
  }
};

// 缓存策略
// webpack.config.js
module.exports = {
  output: {
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].chunk.js'
  },
  optimization: {
    moduleIds: 'deterministic',
    runtimeChunk: 'single',
    splitChunks: {
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
};

3.4 SSR代码分割

javascript
// loadable-components (SSR支持)
import loadable from '@loadable/component';

const OtherComponent = loadable(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <OtherComponent fallback={<div>Loading...</div>} />
    </div>
  );
}

// 服务端
import { ChunkExtractor } from '@loadable/server';

app.get('*', (req, res) => {
  const extractor = new ChunkExtractor({ 
    statsFile: path.resolve('build/loadable-stats.json') 
  });
  
  const jsx = extractor.collectChunks(<App />);
  const html = renderToString(jsx);
  
  res.send(`
    <!DOCTYPE html>
    <html>
      <head>
        ${extractor.getStyleTags()}
        ${extractor.getLinkTags()}
      </head>
      <body>
        <div id="root">${html}</div>
        ${extractor.getScriptTags()}
      </body>
    </html>
  `);
});

// Next.js动态导入
import dynamic from 'next/dynamic';

const DynamicComponent = dynamic(() => import('./Component'), {
  loading: () => <p>Loading...</p>,
  ssr: true
});

function Page() {
  return (
    <div>
      <DynamicComponent />
    </div>
  );
}

// 禁用SSR
const NoSSR = dynamic(() => import('./NoSSR'), {
  ssr: false
});

第四部分:实战案例

4.1 大型应用分割

javascript
// 分层分割策略
// 1. 路由层
const routes = {
  public: lazy(() => import('./routes/PublicRoutes')),
  user: lazy(() => import('./routes/UserRoutes')),
  admin: lazy(() => import('./routes/AdminRoutes'))
};

// 2. 功能模块层
const features = {
  dashboard: lazy(() => import('./features/Dashboard')),
  analytics: lazy(() => import('./features/Analytics')),
  settings: lazy(() => import('./features/Settings'))
};

// 3. 组件层
const components = {
  chart: lazy(() => import('./components/Chart')),
  table: lazy(() => import('./components/DataTable')),
  editor: lazy(() => import('./components/Editor'))
};

// 4. 工具库层
const utils = {
  date: () => import('date-fns'),
  chart: () => import('chart.js'),
  validation: () => import('yup')
};

// 应用结构
function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<AppLoader />}>
        <Routes>
          <Route path="/" element={<routes.public />} />
          <Route path="/user/*" element={<routes.user />} />
          <Route path="/admin/*" element={<routes.admin />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

4.2 条件加载实战

javascript
// 基于权限的分割
function ProtectedRoute({ permission }) {
  const { hasPermission } = useAuth();
  
  if (!hasPermission(permission)) {
    return <Redirect to="/login" />;
  }
  
  const Component = lazy(() => 
    import(`./pages/${permission}Page`)
  );
  
  return (
    <Suspense fallback={<PageLoader />}>
      <Component />
    </Suspense>
  );
}

// 基于功能开关的分割
function FeatureRoute({ feature }) {
  const { isEnabled } = useFeatureFlags();
  
  if (!isEnabled(feature)) {
    return <NotFound />;
  }
  
  const Component = lazy(() =>
    import(`./features/${feature}/Component`)
  );
  
  return (
    <Suspense fallback={<Loading />}>
      <Component />
    </Suspense>
  );
}

// 基于A/B测试的分割
function ABTestComponent({ experiment }) {
  const { variant } = useABTest(experiment);
  
  const Component = lazy(() =>
    import(`./variants/${variant}Component`)
  );
  
  return (
    <Suspense fallback={<Loading />}>
      <Component />
    </Suspense>
  );
}

4.3 渐进式迁移

javascript
// 旧代码逐步迁移到lazy
// Step 1: 识别大型组件
const OldHeavyComponent = require('./OldHeavyComponent');

// Step 2: 创建lazy版本
const NewLazyComponent = lazy(() => import('./OldHeavyComponent'));

// Step 3: 逐步替换
function App() {
  const [useLazy, setUseLazy] = useState(false);
  
  return (
    <div>
      {useLazy ? (
        <Suspense fallback={<Loading />}>
          <NewLazyComponent />
        </Suspense>
      ) : (
        <OldHeavyComponent />
      )}
    </div>
  );
}

// Step 4: 特性开关控制
function GradualMigration() {
  const { enableLazyLoading } = useFeatureFlags();
  
  const Component = enableLazyLoading
    ? lazy(() => import('./NewComponent'))
    : OldComponent;
  
  return enableLazyLoading ? (
    <Suspense fallback={<Loading />}>
      <Component />
    </Suspense>
  ) : (
    <Component />
  );
}

注意事项

1. 避免过度分割

javascript
// ❌ 过度分割
const Button = lazy(() => import('./Button'));
const Input = lazy(() => import('./Input'));
const Text = lazy(() => import('./Text'));

// ✅ 合理分割
const FormComponents = lazy(() => import('./FormComponents'));

2. Suspense边界

javascript
// ❌ 缺少Suspense
const LazyComp = lazy(() => import('./Comp'));
<LazyComp />  // 错误!

// ✅ 正确使用
<Suspense fallback={<Loading />}>
  <LazyComp />
</Suspense>

3. 错误处理

javascript
// ✅ 完善的错误处理
<ErrorBoundary>
  <Suspense fallback={<Loading />}>
    <LazyComponent />
  </Suspense>
</ErrorBoundary>

常见问题

Q1: lazy和动态import的区别?

A: lazy是React组件专用,动态import是通用的模块导入。

Q2: 代码分割影响SEO吗?

A: SSR时需特殊处理,使用loadable-components。

Q3: 如何调试分割后的代码?

A: 使用source map和webpack bundle analyzer。

Q4: chunk加载失败怎么办?

A: 实现错误边界和重试机制。

Q5: 如何优化chunk大小?

A: 配置webpack splitChunks,分析bundle。

Q6: 预加载和懒加载冲突吗?

A: 不冲突,可以智能预加载即将需要的代码。

Q7: 所有组件都应该lazy吗?

A: 不应该,只对大型或非首屏组件使用。

Q8: 如何测试代码分割?

A: 使用Lighthouse和Network面板分析。

Q9: Suspense可以嵌套吗?

A: 可以,实现渐进式加载。

Q10: React 19对代码分割有改进吗?

A: 更好的Suspense支持和服务器组件。

总结

核心要点

1. 代码分割优势
   ✅ 减少初始bundle
   ✅ 按需加载
   ✅ 提升性能
   ✅ 优化用户体验

2. 分割策略
   ✅ 路由级分割
   ✅ 组件级分割
   ✅ 库级分割
   ✅ 条件分割

3. 实现方案
   ✅ React.lazy
   ✅ 动态import
   ✅ Webpack配置
   ✅ SSR支持

最佳实践

1. 分割原则
   ✅ 优先路由分割
   ✅ 大型组件分割
   ✅ 第三方库按需
   ✅ 避免过度分割

2. 性能优化
   ✅ 智能预加载
   ✅ 合理chunk大小
   ✅ 缓存策略
   ✅ 错误处理

3. 用户体验
   ✅ 优质fallback
   ✅ 渐进式加载
   ✅ 错误恢复
   ✅ 加载指示

代码分割是现代React应用必备的性能优化手段,合理使用能显著提升应用加载速度和用户体验。