欢迎光临
我们一直在努力

04-性能优化与最佳实践——16. 代码分割 - 动态导入

16. 代码分割 – 动态导入

概述

代码分割是一种优化技术,将应用代码拆分成多个小块(chunks),按需加载,而不是一次性加载整个应用。这可以显著减少首屏加载时间,提升用户体验。

维度内容
What 使用动态 import() 将代码拆分成多个 chunk
Why 减少首屏加载体积,按需加载
When 大型应用、路由页面、不常用的组件
Where 路由配置、组件导入处
Who 需要优化加载性能的开发者
How const LazyComponent = lazy(() => import('./Component')) + <Suspense>

1. 什么是代码分割

1.1 没有代码分割的问题

// ❌ 传统方式:所有组件打包在一起
import HomePage from './pages/HomePage';
import AboutPage from './pages/AboutPage';
import DashboardPage from './pages/DashboardPage';
import AdminPage from './pages/AdminPage';
import SettingsPage from './pages/SettingsPage';

function App() {
// 所有页面组件都会在首屏加载
return <Router>{/* … */}</Router>;
}

// 打包结果:bundle.js (5MB)
// 用户需要下载 5MB 才能看到任何内容

1.2 使用代码分割

// ✅ 代码分割:按需加载
import { lazy, Suspense } from 'react';

const HomePage = lazy(() => import('./pages/HomePage'));
const AboutPage = lazy(() => import('./pages/AboutPage'));
const DashboardPage = lazy(() => import('./pages/DashboardPage'));
const AdminPage = lazy(() => import('./pages/AdminPage'));
const SettingsPage = lazy(() => import('./pages/SettingsPage'));

function App() {
return (
<Suspense fallback={<LoadingSpinner />}>
<Router>{/* … */}</Router>
</Suspense>
);
}

// 打包结果:
// main.js (200KB)
// home.js (100KB)
// about.js (80KB)
// dashboard.js (150KB)
// …
// 用户只下载当前页面需要的代码


2. React.lazy 和 Suspense

2.1 基础用法

import { lazy, Suspense } from 'react';

// 懒加载组件
const LazyComponent = lazy(() => import('./HeavyComponent'));

function App() {
return (
<div>
<h1>我的应用</h1>
<Suspense fallback={<div>加载中…</div>}>
<LazyComponent />
</Suspense>
</div>
);
}

2.2 命名导出

// Component.js
export const MyComponent = () => <div>Hello</div>;
export const AnotherComponent = () => <div>World</div>;

// 懒加载命名导出
const MyComponent = lazy(() =>
import('./Component').then(module => ({ default: module.MyComponent }))
);

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

2.3 多个 Suspense

function App() {
return (
<div>
<Suspense fallback={<HeaderSkeleton />}>
<LazyHeader />
</Suspense>

<Suspense fallback={<ContentSkeleton />}>
<LazyContent />
</Suspense>

<Suspense fallback={<FooterSkeleton />}>
<LazyFooter />
</Suspense>
</div>
);
}


3. 路由级别代码分割

3.1 React Router 集成

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 Contact = lazy(() => import('./pages/Contact'));
const Blog = lazy(() => import('./pages/Blog'));
const BlogPost = lazy(() => import('./pages/BlogPost'));

function App() {
return (
<BrowserRouter>
<Suspense fallback={<GlobalLoadingSpinner />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/contact" element={<Contact />} />
<Route path="/blog" element={<Blog />} />
<Route path="/blog/:id" element={<BlogPost />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}

3.2 嵌套路由

// DashboardLayout.jsx
import { Outlet } from 'react-router-dom';
import { lazy, Suspense } from 'react';

const Sidebar = lazy(() => import('./Sidebar'));
const Header = lazy(() => import('./Header'));

function DashboardLayout() {
return (
<div className="dashboard">
<Suspense fallback={<HeaderSkeleton />}>
<Header />
</Suspense>

<div className="dashboard-content">
<Suspense fallback={<SidebarSkeleton />}>
<Sidebar />
</Suspense>

<main>
<Outlet /> {/* 子路由在此渲染 */}
</main>
</div>
</div>
);
}

// 路由配置
const routes = [
{
path: '/dashboard',
element: <DashboardLayout />,
children: [
{
index: true,
element: lazy(() => import('./pages/Overview')),
},
{
path: 'analytics',
element: lazy(() => import('./pages/Analytics')),
},
{
path: 'settings',
element: lazy(() => import('./pages/Settings')),
},
],
},
];


4. 组件级代码分割

4.1 条件加载

function App() {
const [showModal, setShowModal] = useState(false);
const [showChart, setShowChart] = useState(false);

return (
<div>
<button onClick={() => setShowModal(true)}>打开模态框</button>

{showModal && (
<Suspense fallback={<ModalSkeleton />}>
<LazyModal onClose={() => setShowModal(false)} />
</Suspense>
)}

<button onClick={() => setShowChart(true)}>显示图表</button>

{showChart && (
<Suspense fallback={<ChartSkeleton />}>
<LazyChart data={data} />
</Suspense>
)}
</div>
);
}

4.2 预加载

import { lazy, useState } from 'react';

// 创建懒加载组件
const LazyComponent = lazy(() => import('./HeavyComponent'));

// 预加载函数
const preloadLazyComponent = () => {
// 触发预加载
import('./HeavyComponent');
};

function App() {
const [show, setShow] = useState(false);

return (
<div>
{/* 鼠标悬停时预加载 */}
<button
onMouseEnter={preloadLazyComponent}
onClick={() => setShow(true)}
>
显示组件
</button>

{show && (
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
)}
</div>
);
}

4.3 优先级加载

// 高优先级组件(立即加载)
const CriticalComponent = lazy(() => import('./CriticalComponent'));

// 低优先级组件(延迟加载)
const LowPriorityComponent = lazy(() => {
return new Promise(resolve => {
// 延迟 2 秒后加载
setTimeout(() => resolve(import('./LowPriorityComponent')), 2000);
});
});

// 空闲时加载
const IdleComponent = lazy(() => {
return new Promise(resolve => {
if ('requestIdleCallback' in window) {
requestIdleCallback(() => resolve(import('./IdleComponent')));
} else {
setTimeout(() => resolve(import('./IdleComponent')), 100);
}
});
});


5. 错误处理和加载状态

5.1 带错误边界的懒加载

import { lazy, Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';

const LazyComponent = lazy(() => import('./HeavyComponent'));

function LazyWrapper() {
return (
<ErrorBoundary fallback={<div>组件加载失败</div>}>
<Suspense fallback={<LoadingSpinner />}>
<LazyComponent />
</Suspense>
</ErrorBoundary>
);
}

5.2 自定义加载组件

function LoadingSpinner({ delay = 200 }) {
const [show, setShow] = useState(false);

useEffect(() => {
const timer = setTimeout(() => setShow(true), delay);
return () => clearTimeout(timer);
}, [delay]);

if (!show) return null;

return (
<div className="loading-spinner">
<div className="spinner"></div>
<p>加载中…</p>
</div>
);
}

// 使用
<Suspense fallback={<LoadingSpinner />}>
<LazyComponent />
</Suspense>

5.3 骨架屏

function PageSkeleton() {
return (
<div className="page-skeleton">
<div className="skeleton-header"></div>
<div className="skeleton-content">
<div className="skeleton-line"></div>
<div className="skeleton-line"></div>
<div className="skeleton-line"></div>
</div>
</div>
);
}

// 使用
<Suspense fallback={<PageSkeleton />}>
<LazyPage />
</Suspense>


6. Webpack 代码分割配置

6.1 魔法注释

// 自定义 chunk 名称
const Home = lazy(() =>
import(/* webpackChunkName: "home-page" */ './pages/Home')
);

// 预加载
const About = lazy(() =>
import(/* webpackPrefetch: true */ './pages/About')
);

// 预获取
const Contact = lazy(() =>
import(/* webpackPreload: true */ './pages/Contact')
);

// 指定加载模式(lazy/ eager)
const Admin = lazy(() =>
import(/* webpackMode: "lazy" */ './pages/Admin')
);

6.2 分包策略

// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\\\/]node_modules[\\\\/]/,
name: 'vendors',
chunks: 'all',
},
react: {
test: /[\\\\/]node_modules[\\\\/](react|react-dom)[\\\\/]/,
name: 'react-vendor',
chunks: 'all',
},
ui: {
test: /[\\\\/]node_modules[\\\\/](@mui|antd)[\\\\/]/,
name: 'ui-vendor',
chunks: 'all',
},
},
},
},
};


7. Vite 代码分割

7.1 自动分割

// Vite 自动处理代码分割
const LazyComponent = lazy(() => import('./HeavyComponent'));
// Vite 会自动创建单独的 chunk

7.2 手动配置

// vite.config.js
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
'react-vendor': ['react', 'react-dom', 'react-router-dom'],
'ui-vendor': ['@mui/material', '@emotion/react'],
'utils-vendor': ['lodash', 'date-fns'],
},
},
},
},
};


8. 完整示例:电商应用

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

// 路由级代码分割
const HomePage = lazy(() => import('./pages/HomePage'));
const ProductsPage = lazy(() => import('./pages/ProductsPage'));
const ProductDetailPage = lazy(() => import('./pages/ProductDetailPage'));
const CartPage = lazy(() => import('./pages/CartPage'));
const CheckoutPage = lazy(() => import('./pages/CheckoutPage'));
const ProfilePage = lazy(() => import('./pages/ProfilePage'));

// 组件级代码分割
const NewsletterSignup = lazy(() => import('./components/NewsletterSignup'));
const ProductRecommendations = lazy(() => import('./components/ProductRecommendations'));
const ChatSupport = lazy(() => import('./components/ChatSupport'));

// 预加载函数
const preloadPage = (path) => {
switch (path) {
case '/products':
import('./pages/ProductsPage');
break;
case '/cart':
import('./pages/CartPage');
break;
// …
}
};

function App() {
const [showChat, setShowChat] = useState(false);

return (
<BrowserRouter>
<div>
<nav>
<Link to="/" onMouseEnter={() => preloadPage('/')}>首页</Link>
<Link to="/products" onMouseEnter={() => preloadPage('/products')}>
商品
</Link>
<Link to="/cart" onMouseEnter={() => preloadPage('/cart')}>
购物车
</Link>
</nav>

<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/products" element={<ProductsPage />} />
<Route path="/product/:id" element={<ProductDetailPage />} />
<Route path="/cart" element={<CartPage />} />
<Route path="/checkout" element={<CheckoutPage />} />
<Route path="/profile" element={<ProfilePage />} />
</Routes>
</Suspense>

{/* 首屏不重要的组件 */}
<Suspense fallback={null}>
<NewsletterSignup />
</Suspense>

<Suspense fallback={null}>
<ProductRecommendations />
</Suspense>

{/* 条件加载 */}
{showChat && (
<Suspense fallback={<div>加载客服中…</div>}>
<ChatSupport onClose={() => setShowChat(false)} />
</Suspense>
)}

<button onClick={() => setShowChat(true)}>
在线客服
</button>
</div>
</BrowserRouter>
);
}

function PageSkeleton() {
return (
<div className="page-skeleton">
<div className="skeleton-banner"></div>
<div className="skeleton-grid">
{[…Array(8)].map((_, i) => (
<div key={i} className="skeleton-card"></div>
))}
</div>
</div>
);
}

export default App;


9. 总结

核心要点

要点说明
核心价值 减少首屏加载时间,按需加载
主要工具 React.lazy + Suspense
适用场景 路由页面、大型组件、条件渲染
最佳实践 路由级分割 + 组件级分割 + 预加载

代码分割策略

级别说明示例
路由级 每个路由单独打包 页面组件
组件级 大型组件单独打包 图表、编辑器
条件级 条件渲染的组件 模态框、抽屉
库级 第三方库单独打包 vendors chunk

记忆口诀

代码分割懒加载,首屏体积降下来 路由组件单独包,大型组件按需来 Suspense 加 fallback,用户体验不崩坏


10. 相关资源

  • React.lazy 文档
  • Webpack 代码分割
  • Vite 构建优化

赞(0)
未经允许不得转载:171主机测评 » 04-性能优化与最佳实践——16. 代码分割 - 动态导入
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址