Skip to content

代码分割策略

课程概述

本章节深入探讨前端应用的代码分割策略,学习如何合理拆分代码以提升应用性能。通过有效的代码分割,可以减少初始加载时间,提升用户体验。

学习目标

  • 理解代码分割的概念和重要性
  • 掌握不同的代码分割策略
  • 学习使用动态导入实现代码分割
  • 了解路由级代码分割
  • 掌握组件级代码分割
  • 学习库和公共代码的分割策略

第一部分:代码分割基础

1.1 什么是代码分割

代码分割(Code Splitting)是将应用代码拆分成多个bundle的技术,按需加载所需代码,而不是一次性加载整个应用。

未分割示例:

javascript
// 单个 bundle,包含所有代码
bundle.js (2MB) → 首次加载全部

分割后示例:

javascript
// 多个 bundle,按需加载
main.js (200KB)     → 首次加载
home.chunk.js (100KB) → 访问首页时加载
about.chunk.js (80KB) → 访问关于页时加载
admin.chunk.js (150KB) → 访问管理页时加载

1.2 为什么需要代码分割

javascript
1. 减少初始加载时间
2. 优化资源利用
3. 提升页面性能
4. 改善用户体验
5. 降低带宽消耗

性能对比:

javascript
// 未分割
首次加载: 2MB
加载时间: 6秒
可交互时间: 7秒

// 分割后
首次加载: 200KB
加载时间: 0.8秒
可交互时间: 1秒

1.3 代码分割策略

javascript
1. 路由级分割  - 按路由拆分
2. 组件级分割  - 按组件拆分
3. 库级分割    - 第三方库单独打包
4. 功能级分割  - 按功能模块拆分
5. 时间分割    - 按加载时机拆分

第二部分:动态导入

2.1 基础动态导入

typescript
// 传统静态导入
import { add } from './math'

// 动态导入
const math = await import('./math')
math.add(1, 2)

// 或使用 Promise
import('./math').then(math => {
  math.add(1, 2)
})

2.2 React中的动态导入

typescript
// src/App.tsx
import { lazy, Suspense } from 'react'

// 懒加载组件
const Home = lazy(() => import('./pages/Home'))
const About = lazy(() => import('./pages/About'))
const Dashboard = lazy(() => import('./pages/Dashboard'))

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/dashboard" element={<Dashboard />} />
      </Routes>
    </Suspense>
  )
}

2.3 带错误处理的动态导入

typescript
// src/components/LazyComponent.tsx
import { lazy, Suspense, ComponentType } from 'react'

function lazyWithRetry<T extends ComponentType<any>>(
  componentImport: () => Promise<{ default: T }>
) {
  return lazy(async () => {
    const pageHasAlreadyBeenForceRefreshed = JSON.parse(
      window.sessionStorage.getItem('page-has-been-force-refreshed') || 'false'
    )
    
    try {
      const component = await componentImport()
      window.sessionStorage.setItem('page-has-been-force-refreshed', 'false')
      return component
    } catch (error) {
      if (!pageHasAlreadyBeenForceRefreshed) {
        window.sessionStorage.setItem('page-has-been-force-refreshed', 'true')
        return window.location.reload()
      }
      throw error
    }
  })
}

// 使用
const Dashboard = lazyWithRetry(() => import('./pages/Dashboard'))

2.4 预加载和预获取

typescript
// 魔法注释
const Dashboard = lazy(() => 
  import(
    /* webpackChunkName: "dashboard" */
    /* webpackPrefetch: true */
    './pages/Dashboard'
  )
)

const AdminPanel = lazy(() => 
  import(
    /* webpackChunkName: "admin" */
    /* webpackPreload: true */
    './pages/AdminPanel'
  )
)

区别:

javascript
// Prefetch - 空闲时加载
<link rel="prefetch" href="dashboard.chunk.js">

// Preload - 立即加载
<link rel="preload" href="admin.chunk.js">

第三部分:路由级代码分割

3.1 React Router配置

typescript
// src/App.tsx
import { lazy, Suspense } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { LoadingSpinner } from './components/LoadingSpinner'

// 路由级懒加载
const Home = lazy(() => import('./pages/Home'))
const Products = lazy(() => import('./pages/Products'))
const ProductDetail = lazy(() => import('./pages/ProductDetail'))
const Cart = lazy(() => import('./pages/Cart'))
const Checkout = lazy(() => import('./pages/Checkout'))
const Profile = lazy(() => import('./pages/Profile'))
const Admin = lazy(() => import('./pages/Admin'))

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<LoadingSpinner />}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/products" element={<Products />} />
          <Route path="/products/:id" element={<ProductDetail />} />
          <Route path="/cart" element={<Cart />} />
          <Route path="/checkout" element={<Checkout />} />
          <Route path="/profile" element={<Profile />} />
          <Route path="/admin/*" element={<Admin />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  )
}

3.2 嵌套路由分割

typescript
// src/pages/Admin.tsx
import { lazy, Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'

const AdminDashboard = lazy(() => import('./AdminDashboard'))
const AdminUsers = lazy(() => import('./AdminUsers'))
const AdminProducts = lazy(() => import('./AdminProducts'))
const AdminSettings = lazy(() => import('./AdminSettings'))

export default function Admin() {
  return (
    <div className="admin-layout">
      <AdminSidebar />
      <div className="admin-content">
        <Suspense fallback={<div>Loading...</div>}>
          <Routes>
            <Route path="/" element={<AdminDashboard />} />
            <Route path="/users" element={<AdminUsers />} />
            <Route path="/products" element={<AdminProducts />} />
            <Route path="/settings" element={<AdminSettings />} />
          </Routes>
        </Suspense>
      </div>
    </div>
  )
}

3.3 路由预加载

typescript
// src/utils/routePreload.ts
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'

const routePreloadMap: Record<string, () => Promise<any>> = {
  '/products': () => import('./pages/Products'),
  '/cart': () => import('./pages/Cart'),
  '/checkout': () => import('./pages/Checkout'),
}

export function useRoutePreload() {
  const location = useLocation()
  
  useEffect(() => {
    // 预加载相关路由
    const preloadRoutes = getRelatedRoutes(location.pathname)
    
    preloadRoutes.forEach(route => {
      const preload = routePreloadMap[route]
      if (preload) {
        setTimeout(() => preload(), 2000) // 2秒后预加载
      }
    })
  }, [location])
}

function getRelatedRoutes(currentPath: string): string[] {
  const routeMap: Record<string, string[]> = {
    '/': ['/products', '/cart'],
    '/products': ['/cart'],
    '/cart': ['/checkout'],
  }
  
  return routeMap[currentPath] || []
}

第四部分:组件级代码分割

4.1 大型组件分割

typescript
// src/pages/Dashboard.tsx
import { lazy, Suspense } from 'react'

const Chart = lazy(() => import('@/components/Chart'))
const DataTable = lazy(() => import('@/components/DataTable'))
const ReportGenerator = lazy(() => import('@/components/ReportGenerator'))

export default function Dashboard() {
  return (
    <div className="dashboard">
      <h1>Dashboard</h1>
      
      <Suspense fallback={<div>Loading chart...</div>}>
        <Chart />
      </Suspense>
      
      <Suspense fallback={<div>Loading table...</div>}>
        <DataTable />
      </Suspense>
      
      <Suspense fallback={<div>Loading report...</div>}>
        <ReportGenerator />
      </Suspense>
    </div>
  )
}

4.2 条件组件分割

typescript
// src/components/Editor.tsx
import { lazy, Suspense, useState } from 'react'

const RichTextEditor = lazy(() => import('./RichTextEditor'))
const MarkdownEditor = lazy(() => import('./MarkdownEditor'))

export function Editor() {
  const [editorType, setEditorType] = useState<'rich' | 'markdown'>('rich')
  
  return (
    <div>
      <select value={editorType} onChange={(e) => setEditorType(e.target.value as any)}>
        <option value="rich">Rich Text</option>
        <option value="markdown">Markdown</option>
      </select>
      
      <Suspense fallback={<div>Loading editor...</div>}>
        {editorType === 'rich' ? <RichTextEditor /> : <MarkdownEditor />}
      </Suspense>
    </div>
  )
}

4.3 Modal/Dialog分割

typescript
// src/components/UserProfile.tsx
import { lazy, Suspense, useState } from 'react'

const EditProfileModal = lazy(() => import('./EditProfileModal'))

export function UserProfile() {
  const [showModal, setShowModal] = useState(false)
  
  return (
    <div>
      <button onClick={() => setShowModal(true)}>
        Edit Profile
      </button>
      
      {showModal && (
        <Suspense fallback={<div>Loading...</div>}>
          <EditProfileModal onClose={() => setShowModal(false)} />
        </Suspense>
      )}
    </div>
  )
}

第五部分:库和vendor分割

5.1 Vite分包配置

typescript
// vite.config.ts
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // React生态
          'react-vendor': ['react', 'react-dom', 'react-router-dom'],
          
          // UI库
          'ui-vendor': ['@mui/material', '@mui/icons-material'],
          
          // 工具库
          'utils-vendor': ['lodash-es', 'date-fns', 'axios'],
          
          // 图表库
          'chart-vendor': ['chart.js', 'react-chartjs-2'],
        }
      }
    }
  }
})

5.2 自动分包策略

typescript
// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          // node_modules 分包
          if (id.includes('node_modules')) {
            // React 相关
            if (id.includes('react') || id.includes('react-dom')) {
              return 'react'
            }
            
            // UI 库
            if (id.includes('@mui')) {
              return 'mui'
            }
            
            // 图表
            if (id.includes('chart') || id.includes('echarts')) {
              return 'charts'
            }
            
            // 其他第三方库
            return 'vendor'
          }
          
          // 按目录分包
          if (id.includes('/src/components/')) {
            return 'components'
          }
          
          if (id.includes('/src/utils/')) {
            return 'utils'
          }
        }
      }
    }
  }
})

5.3 Webpack分包配置

javascript
// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // React
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
          name: 'react',
          priority: 20
        },
        
        // UI库
        antd: {
          test: /[\\/]node_modules[\\/]antd[\\/]/,
          name: 'antd',
          priority: 15
        },
        
        // 图表
        charts: {
          test: /[\\/]node_modules[\\/](chart\.js|echarts)[\\/]/,
          name: 'charts',
          priority: 15
        },
        
        // 其他vendor
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10
        },
        
        // 公共代码
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true
        }
      }
    }
  }
}

第六部分:分割优化策略

6.1 分包大小控制

typescript
// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            const packageName = id.split('node_modules/')[1].split('/')[0]
            
            // 大型库单独分包
            if (['@mui/material', 'antd', 'echarts'].includes(packageName)) {
              return packageName.replace('@', '')
            }
            
            return 'vendor'
          }
        }
      }
    },
    
    chunkSizeWarningLimit: 500, // 500KB警告
  }
})

6.2 首屏优化

typescript
// src/App.tsx
import { lazy, Suspense } from 'react'
import { Routes, Route } from 'react-router-dom'

// 首页不懒加载
import Home from './pages/Home'

// 其他页面懒加载
const About = lazy(() => import('./pages/About'))
const Products = lazy(() => import('./pages/Products'))
const Contact = lazy(() => import('./pages/Contact'))

function App() {
  return (
    <Routes>
      {/* 首页直接加载 */}
      <Route path="/" element={<Home />} />
      
      {/* 其他页面懒加载 */}
      <Route 
        path="/about" 
        element={
          <Suspense fallback={<div>Loading...</div>}>
            <About />
          </Suspense>
        } 
      />
      <Route 
        path="/products" 
        element={
          <Suspense fallback={<div>Loading...</div>}>
            <Products />
          </Suspense>
        } 
      />
      <Route 
        path="/contact" 
        element={
          <Suspense fallback={<div>Loading...</div>}>
            <Contact />
          </Suspense>
        } 
      />
    </Routes>
  )
}

6.3 关键路径优化

typescript
// src/utils/critical.ts
// 关键资源不分割
export { default as Header } from '@/components/Header'
export { default as Footer } from '@/components/Footer'
export { default as Navigation } from '@/components/Navigation'

// 非关键资源懒加载
export const UserMenu = lazy(() => import('@/components/UserMenu'))
export const SearchModal = lazy(() => import('@/components/SearchModal'))
export const NotificationPanel = lazy(() => import('@/components/NotificationPanel'))

第七部分:加载状态优化

7.1 加载占位符

typescript
// src/components/LoadingPlaceholder.tsx
export function LoadingPlaceholder({ type }: { type: 'page' | 'component' | 'modal' }) {
  if (type === 'page') {
    return (
      <div className="page-skeleton">
        <div className="skeleton-header" />
        <div className="skeleton-content" />
      </div>
    )
  }
  
  if (type === 'component') {
    return <div className="component-skeleton" />
  }
  
  return <div className="modal-skeleton" />
}
typescript
// 使用
const Dashboard = lazy(() => import('./pages/Dashboard'))

function App() {
  return (
    <Suspense fallback={<LoadingPlaceholder type="page" />}>
      <Dashboard />
    </Suspense>
  )
}

7.2 渐进式加载

typescript
// src/components/ProgressiveImage.tsx
import { useState, useEffect } from 'react'

export function ProgressiveImage({ src, placeholder }: { src: string, placeholder: string }) {
  const [currentSrc, setCurrentSrc] = useState(placeholder)
  
  useEffect(() => {
    const img = new Image()
    img.src = src
    img.onload = () => setCurrentSrc(src)
  }, [src])
  
  return (
    <img 
      src={currentSrc} 
      className={currentSrc === placeholder ? 'loading' : 'loaded'}
    />
  )
}

7.3 错误边界

typescript
// src/components/LazyErrorBoundary.tsx
import { Component, ReactNode } from 'react'

interface Props {
  children: ReactNode
  fallback?: ReactNode
}

interface State {
  hasError: boolean
}

export class LazyErrorBoundary extends Component<Props, State> {
  state = { hasError: false }
  
  static getDerivedStateFromError() {
    return { hasError: true }
  }
  
  componentDidCatch(error: Error) {
    console.error('Lazy loading error:', error)
  }
  
  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div>
          <h2>加载失败</h2>
          <button onClick={() => window.location.reload()}>
            重新加载
          </button>
        </div>
      )
    }
    
    return this.props.children
  }
}
typescript
// 使用
<LazyErrorBoundary>
  <Suspense fallback={<Loading />}>
    <Dashboard />
  </Suspense>
</LazyErrorBoundary>

第八部分:性能监控

8.1 加载性能追踪

typescript
// src/utils/performance.ts
export function trackChunkLoad(chunkName: string) {
  const startTime = performance.now()
  
  return () => {
    const endTime = performance.now()
    const loadTime = endTime - startTime
    
    // 发送到分析服务
    analytics.track('chunk_load', {
      chunk: chunkName,
      loadTime,
    })
    
    console.log(`Chunk ${chunkName} loaded in ${loadTime}ms`)
  }
}
typescript
// 使用
const Dashboard = lazy(() => {
  const track = trackChunkLoad('dashboard')
  return import('./pages/Dashboard').finally(track)
})

8.2 Bundle分析

bash
# 安装分析工具
npm install -D rollup-plugin-visualizer
typescript
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
    })
  ]
})

8.3 性能指标

typescript
// src/utils/metrics.ts
export function measureBundleSize() {
  if ('performance' in window) {
    const resources = performance.getEntriesByType('resource')
    
    const jsResources = resources.filter(r => r.name.endsWith('.js'))
    const totalSize = jsResources.reduce((sum, r) => sum + (r.transferSize || 0), 0)
    
    console.log('Total JS Size:', (totalSize / 1024).toFixed(2), 'KB')
    
    jsResources.forEach(r => {
      console.log(`${r.name}: ${(r.transferSize / 1024).toFixed(2)} KB`)
    })
  }
}

第九部分:完整示例

9.1 完整应用示例

typescript
// src/App.tsx
import { lazy, Suspense } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
import { LazyErrorBoundary } from './components/LazyErrorBoundary'
import { LoadingPlaceholder } from './components/LoadingPlaceholder'

// 关键组件直接导入
import Header from './components/Header'
import Footer from './components/Footer'

// 页面懒加载
const Home = lazy(() => import('./pages/Home'))
const Products = lazy(() => import('./pages/Products'))
const ProductDetail = lazy(() => import('./pages/ProductDetail'))
const Cart = lazy(() => import('./pages/Cart'))
const Checkout = lazy(() => import('./pages/Checkout'))
const Profile = lazy(() => import('./pages/Profile'))
const Admin = lazy(() => import('./pages/Admin'))

function App() {
  return (
    <BrowserRouter>
      <div className="app">
        <Header />
        
        <main>
          <LazyErrorBoundary>
            <Suspense fallback={<LoadingPlaceholder type="page" />}>
              <Routes>
                <Route path="/" element={<Home />} />
                <Route path="/products" element={<Products />} />
                <Route path="/products/:id" element={<ProductDetail />} />
                <Route path="/cart" element={<Cart />} />
                <Route path="/checkout" element={<Checkout />} />
                <Route path="/profile" element={<Profile />} />
                <Route path="/admin/*" element={<Admin />} />
              </Routes>
            </Suspense>
          </LazyErrorBoundary>
        </main>
        
        <Footer />
      </div>
    </BrowserRouter>
  )
}

export default App

9.2 Vite完整配置

typescript
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import { visualizer } from 'rollup-plugin-visualizer'

export default defineConfig({
  plugins: [
    react(),
    visualizer({
      open: true,
      gzipSize: true,
    })
  ],
  
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'react-vendor': ['react', 'react-dom', 'react-router-dom'],
          'ui-vendor': ['@mui/material'],
          'utils-vendor': ['lodash-es', 'axios'],
        },
        
        chunkFileNames: 'js/[name]-[hash].js',
        entryFileNames: 'js/[name]-[hash].js',
        assetFileNames: 'assets/[name]-[hash][extname]',
      }
    },
    
    chunkSizeWarningLimit: 500,
  }
})

9.3 测试和验证

typescript
// 代码分割效果测试
describe('Code Splitting', () => {
  it('should load chunks on demand', async () => {
    const { getByText } = render(<App />);
    
    // 初始不加载Dashboard
    expect(window.loadedChunks).not.toContain('dashboard');
    
    // 点击导航
    fireEvent.click(getByText('Dashboard'));
    
    // 等待加载
    await waitFor(() => {
      expect(window.loadedChunks).toContain('dashboard');
    });
  });
  
  it('should show loading state', async () => {
    const { getByText } = render(<App />);
    
    fireEvent.click(getByText('Dashboard'));
    
    // 显示loading
    expect(getByText('Loading...')).toBeInTheDocument();
    
    // 加载完成
    await waitFor(() => {
      expect(getByText('Dashboard Content')).toBeInTheDocument();
    });
  });
});

第十部分:高级优化技巧

10.1 预加载优先级

typescript
// 根据用户行为预加载
function useSmartPreload() {
  const location = useLocation();
  const [userBehavior, setUserBehavior] = useState({
    visitedRoutes: [],
    clickPatterns: []
  });
  
  useEffect(() => {
    // 分析用户行为
    const patterns = analyzeUserBehavior(userBehavior);
    
    // 预加载可能访问的路由
    patterns.likelyNextRoutes.forEach(route => {
      const component = routeComponents[route];
      if (component?.preload) {
        component.preload();
      }
    });
  }, [userBehavior]);
  
  return userBehavior;
}

// 机器学习预测
function predictNextRoute(currentRoute, history) {
  const patterns = {
    '/': ['/products', '/about'],
    '/products': ['/products/:id', '/cart'],
    '/products/:id': ['/cart', '/products'],
    '/cart': ['/checkout']
  };
  
  return patterns[currentRoute] || [];
}

10.2 动态chunk大小控制

typescript
// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          // 根据模块大小决定分包策略
          const moduleInfo = this.getModuleInfo(id);
          
          if (moduleInfo && moduleInfo.importedBindings) {
            const bindings = Object.keys(moduleInfo.importedBindings);
            
            // 大模块单独分包
            if (bindings.length > 50) {
              return `large-${id.split('/').pop()}`;
            }
          }
          
          // 默认分包逻辑
          if (id.includes('node_modules')) {
            return 'vendor';
          }
        },
        
        // 限制chunk大小
        chunkSizeWarningLimit: 500,
        
        // 最小chunk大小
        minSize: 20000,
      }
    }
  }
});

10.3 Critical CSS提取

bash
npm install -D critters
typescript
// vite.config.ts
import { critters } from 'vite-plugin-critters';

export default defineConfig({
  plugins: [
    critters({
      // 内联关键CSS
      inlineFonts: true,
      preload: 'swap',
      pruneSource: true
    })
  ]
});

// 生成结果
// index.html包含内联的关键CSS
// 非关键CSS延迟加载

10.4 Module Federation

typescript
// Webpack Module Federation
// app1/webpack.config.js
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/components/Button',
        './Header': './src/components/Header'
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true }
      }
    })
  ]
};

// app2/webpack.config.js
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: 'app2',
      remotes: {
        app1: 'app1@http://localhost:3001/remoteEntry.js'
      },
      shared: {
        react: { singleton: true },
        'react-dom': { singleton: true }
      }
    })
  ]
};

// 使用远程组件
const RemoteButton = lazy(() => import('app1/Button'));

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

第十一部分:性能监控和分析

11.1 Chunk加载监控

typescript
// 监控chunk加载性能
class ChunkLoadMonitor {
  private metrics: Map<string, number> = new Map();
  
  trackChunkLoad(chunkName: string, startTime: number) {
    return () => {
      const loadTime = performance.now() - startTime;
      this.metrics.set(chunkName, loadTime);
      
      // 发送到分析服务
      analytics.track('chunk_load', {
        chunk: chunkName,
        loadTime,
        url: window.location.href
      });
      
      // 性能告警
      if (loadTime > 3000) {
        console.warn(`Slow chunk load: ${chunkName} took ${loadTime}ms`);
      }
    };
  }
  
  getMetrics() {
    const chunks = Array.from(this.metrics.entries());
    
    return {
      total: chunks.length,
      average: chunks.reduce((sum, [, time]) => sum + time, 0) / chunks.length,
      max: Math.max(...chunks.map(([, time]) => time)),
      min: Math.min(...chunks.map(([, time]) => time)),
      details: Object.fromEntries(chunks)
    };
  }
}

const monitor = new ChunkLoadMonitor();

// 使用
const Dashboard = lazy(() => {
  const track = monitor.trackChunkLoad('dashboard', performance.now());
  return import('./Dashboard').finally(track);
});

11.2 Bundle分析报告

bash
# 生成分析报告
npm run build -- --analyze

# 或使用rollup-plugin-visualizer
npm install -D rollup-plugin-visualizer
typescript
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    visualizer({
      open: true,
      gzipSize: true,
      brotliSize: true,
      filename: 'dist/stats.html',
      template: 'treemap' // sunburst, treemap, network
    })
  ]
});

// 生成的报告显示:
// - 每个chunk的大小
// - 模块依赖关系
// - 压缩后大小
// - 优化建议

11.3 性能指标收集

typescript
// 收集加载性能指标
function collectLoadingMetrics() {
  if ('PerformanceObserver' in window) {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.entryType === 'resource') {
          const resource = entry as PerformanceResourceTiming;
          
          // 只关注chunk文件
          if (resource.name.includes('/js/') || resource.name.includes('.chunk.')) {
            const metrics = {
              name: resource.name,
              duration: resource.duration,
              transferSize: resource.transferSize,
              encodedBodySize: resource.encodedBodySize,
              decodedBodySize: resource.decodedBodySize,
              compressionRatio: resource.encodedBodySize 
                ? (1 - resource.transferSize / resource.encodedBodySize) * 100 
                : 0
            };
            
            console.log('Chunk loaded:', metrics);
            
            // 发送到分析服务
            sendMetrics(metrics);
          }
        }
      }
    });
    
    observer.observe({ entryTypes: ['resource'] });
  }
}

// 页面加载时执行
window.addEventListener('load', collectLoadingMetrics);

第十二部分:错误处理和重试

12.1 Chunk加载失败处理

typescript
// 完整的错误处理方案
function lazyWithErrorHandling(
  importFn: () => Promise<any>,
  componentName: string
) {
  return lazy(async () => {
    try {
      return await importFn();
    } catch (error) {
      console.error(`Failed to load ${componentName}:`, error);
      
      // 检查是否是网络错误
      if (error instanceof Error && error.message.includes('Failed to fetch')) {
        // 显示重试UI
        return {
          default: () => (
            <div className="chunk-error">
              <h3>加载失败</h3>
              <p>组件 {componentName} 加载失败</p>
              <button onClick={() => window.location.reload()}>
                重新加载页面
              </button>
            </div>
          )
        };
      }
      
      throw error;
    }
  });
}

// 使用
const Dashboard = lazyWithErrorHandling(
  () => import('./Dashboard'),
  'Dashboard'
);

12.2 自动重试机制

typescript
// 带指数退避的重试
function lazyWithRetry(
  importFn: () => Promise<any>,
  options = { maxRetries: 3, retryDelay: 1000 }
) {
  return lazy(() => {
    return new Promise((resolve, reject) => {
      let retries = 0;
      
      const attemptLoad = () => {
        importFn()
          .then(resolve)
          .catch((error) => {
            if (retries < options.maxRetries) {
              retries++;
              const delay = options.retryDelay * Math.pow(2, retries - 1);
              
              console.log(`Retry ${retries}/${options.maxRetries} in ${delay}ms`);
              
              setTimeout(attemptLoad, delay);
            } else {
              console.error('Max retries reached');
              reject(error);
            }
          });
      };
      
      attemptLoad();
    });
  });
}

// 使用
const Dashboard = lazyWithRetry(
  () => import('./Dashboard'),
  { maxRetries: 3, retryDelay: 1000 }
);

12.3 降级策略

typescript
// Chunk加载失败时的降级方案
function lazyWithFallback(
  importFn: () => Promise<any>,
  FallbackComponent: React.ComponentType
) {
  return lazy(async () => {
    try {
      return await importFn();
    } catch (error) {
      console.error('Chunk load failed, using fallback:', error);
      
      return {
        default: FallbackComponent
      };
    }
  });
}

// 简单降级组件
const SimpleDashboard = () => (
  <div>
    <h2>Dashboard (简化版)</h2>
    <p>完整版本加载失败,这是简化版本</p>
  </div>
);

// 使用
const Dashboard = lazyWithFallback(
  () => import('./Dashboard'),
  SimpleDashboard
);

第十三部分:实战优化案例

13.1 电商网站优化

typescript
// 优化前: 单个bundle 2.5MB
// src/App.tsx
import Header from './components/Header';
import Footer from './components/Footer';
import Home from './pages/Home';
import Products from './pages/Products';
import ProductDetail from './pages/ProductDetail';
import Cart from './pages/Cart';
import Checkout from './pages/Checkout';
import UserProfile from './pages/UserProfile';
import OrderHistory from './pages/OrderHistory';

// Bundle分析:
// main.js: 2.5MB (太大!)

// 优化后: 多个chunks
// src/App.tsx
import { lazy, Suspense } from 'react';

// 关键组件直接导入
import Header from './components/Header';
import Footer from './components/Footer';

// 页面懒加载
const Home = lazy(() => import('./pages/Home'));
const Products = lazy(() => import(
  /* webpackChunkName: "products" */
  /* webpackPrefetch: true */
  './pages/Products'
));
const ProductDetail = lazy(() => import(
  /* webpackChunkName: "product-detail" */
  './pages/ProductDetail'
));
const Cart = lazy(() => import(
  /* webpackChunkName: "cart" */
  /* webpackPrefetch: true */
  './pages/Cart'
));
const Checkout = lazy(() => import(
  /* webpackChunkName: "checkout" */
  './pages/Checkout'
));
const UserProfile = lazy(() => import(
  /* webpackChunkName: "user" */
  './pages/UserProfile'
));
const OrderHistory = lazy(() => import(
  /* webpackChunkName: "orders" */
  './pages/OrderHistory'
));

// Bundle结果:
// main.js: 150KB (↓94%)
// products.chunk.js: 200KB
// product-detail.chunk.js: 150KB
// cart.chunk.js: 100KB
// checkout.chunk.js: 180KB
// user.chunk.js: 120KB
// orders.chunk.js: 140KB
// vendor.chunk.js: 800KB

// 性能提升:
// 首次加载: 2.5MB -> 950KB (↓62%)
// FCP: 4.5s -> 1.8s (↓60%)
// TTI: 6.2s -> 2.5s (↓60%)

13.2 SaaS后台系统优化

typescript
// 后台系统特点:
// - 功能模块多
// - 图表库大
// - 编辑器大
// - 不是所有用户都用所有功能

// 优化策略
// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          // 图表库单独打包
          if (id.includes('echarts') || id.includes('chart.js')) {
            return 'charts';
          }
          
          // 编辑器单独打包
          if (id.includes('monaco-editor') || id.includes('codemirror')) {
            return 'editor';
          }
          
          // 表格库单独打包
          if (id.includes('ag-grid') || id.includes('react-table')) {
            return 'tables';
          }
          
          // UI库
          if (id.includes('@mui') || id.includes('antd')) {
            return 'ui';
          }
          
          // 按功能模块分包
          if (id.includes('/src/features/dashboard')) {
            return 'dashboard';
          }
          if (id.includes('/src/features/users')) {
            return 'users';
          }
          if (id.includes('/src/features/analytics')) {
            return 'analytics';
          }
          
          // 公共代码
          if (id.includes('/src/components') || id.includes('/src/utils')) {
            return 'common';
          }
          
          return 'vendor';
        }
      }
    }
  }
});

// 结果:
// 初始加载: 只加载必需的chunks
// 访问功能: 按需加载对应chunk
// 总体积不变,但初始加载减少80%

13.3 移动端优化

typescript
// 移动端特殊考虑
const mobileOptimization = {
  更激进的分割: `
    // 移动端网络慢,分割更细
    const Header = lazy(() => import('./Header'));
    const Footer = lazy(() => import('./Footer'));
    const Sidebar = lazy(() => import('./Sidebar'));
    
    // 甚至大组件内部也分割
    const ProductImage = lazy(() => import('./ProductImage'));
    const ProductInfo = lazy(() => import('./ProductInfo'));
    const ProductReviews = lazy(() => import('./ProductReviews'));
  `,
  
  预加载策略: `
    // 在WiFi下预加载
    if (navigator.connection?.effectiveType === '4g') {
      // 预加载常用chunks
      preloadChunks(['products', 'cart']);
    }
  `,
  
  渐进式加载: `
    // 先加载关键内容
    <Suspense fallback={<Skeleton />}>
      <ProductImage />  {/* 优先 */}
      <ProductInfo />   {/* 优先 */}
    </Suspense>
    
    // 延迟加载次要内容
    <Suspense fallback={null}>
      <ProductReviews />  {/* 延迟 */}
    </Suspense>
  `
};

第十四部分:测试和CI/CD集成

14.1 性能回归测试

typescript
// tests/performance.test.ts
import { analyzeBundle } from './utils/bundleAnalyzer';

describe('Bundle Performance', () => {
  it('should not exceed size limits', async () => {
    const analysis = await analyzeBundle('dist');
    
    // 检查初始bundle大小
    expect(analysis.initialBundle).toBeLessThan(200 * 1024); // 200KB
    
    // 检查最大chunk大小
    const maxChunk = Math.max(...analysis.chunks.map(c => c.size));
    expect(maxChunk).toBeLessThan(500 * 1024); // 500KB
    
    // 检查总大小
    expect(analysis.totalSize).toBeLessThan(3 * 1024 * 1024); // 3MB
  });
  
  it('should have proper code splitting', () => {
    const analysis = await analyzeBundle('dist');
    
    // 检查chunk数量
    expect(analysis.chunks.length).toBeGreaterThan(3);
    
    // 检查vendor分离
    const hasVendorChunk = analysis.chunks.some(c => c.name.includes('vendor'));
    expect(hasVendorChunk).toBe(true);
    
    // 检查路由分割
    const routeChunks = analysis.chunks.filter(c => 
      c.name.includes('route') || c.name.includes('page')
    );
    expect(routeChunks.length).toBeGreaterThan(2);
  });
});

14.2 CI/CD集成

yaml
# .github/workflows/bundle-check.yml
name: Bundle Size Check

on: [pull_request]

jobs:
  bundle-check:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Setup Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '20'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Build
        run: npm run build
      
      - name: Analyze bundle
        run: npm run analyze
      
      - name: Check bundle size
        run: |
          node scripts/check-bundle-size.js
      
      - name: Comment PR
        uses: actions/github-script@v6
        with:
          script: |
            const fs = require('fs');
            const stats = JSON.parse(fs.readFileSync('dist/stats.json'));
            
            const comment = `
            ## Bundle Size Report
            
            | Chunk | Size | Gzipped |
            |-------|------|---------|
            ${stats.chunks.map(c => 
              `| ${c.name} | ${c.size}KB | ${c.gzipped}KB |`
            ).join('\n')}
            
            Total: ${stats.total}KB
            `;
            
            github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: comment
            });

14.3 性能预算

javascript
// performance-budget.json
{
  "bundles": [
    {
      "name": "main",
      "maxSize": "200KB",
      "maxSizeGzip": "70KB"
    },
    {
      "name": "vendor",
      "maxSize": "800KB",
      "maxSizeGzip": "250KB"
    },
    {
      "name": "*.chunk",
      "maxSize": "500KB",
      "maxSizeGzip": "150KB"
    }
  ],
  "total": {
    "maxSize": "3MB",
    "maxSizeGzip": "1MB"
  }
}

// scripts/check-bundle-size.js
const fs = require('fs');
const path = require('path');
const budget = require('../performance-budget.json');

function checkBundleSize() {
  const distPath = path.join(__dirname, '../dist');
  const files = fs.readdirSync(distPath);
  
  const violations = [];
  
  files.forEach(file => {
    const filePath = path.join(distPath, file);
    const stats = fs.statSync(filePath);
    const sizeKB = stats.size / 1024;
    
    const budgetRule = budget.bundles.find(b => 
      file.match(new RegExp(b.name))
    );
    
    if (budgetRule) {
      const maxSizeKB = parseInt(budgetRule.maxSize);
      
      if (sizeKB > maxSizeKB) {
        violations.push({
          file,
          actual: sizeKB,
          budget: maxSizeKB,
          over: sizeKB - maxSizeKB
        });
      }
    }
  });
  
  if (violations.length > 0) {
    console.error('Bundle size budget exceeded:');
    violations.forEach(v => {
      console.error(`${v.file}: ${v.actual}KB > ${v.budget}KB (+${v.over}KB)`);
    });
    process.exit(1);
  }
  
  console.log('✓ All bundles within budget');
}

checkBundleSize();

总结

本章全面介绍了代码分割策略:

  1. 基础概念 - 理解代码分割的重要性和原理
  2. 动态导入 - 使用import()实现按需加载
  3. 路由分割 - 按页面路由拆分代码
  4. 组件分割 - 组件级别的懒加载优化
  5. 库分割 - 第三方库的合理分包
  6. 优化策略 - 分包大小控制和预加载
  7. 加载状态 - 优化用户体验的Loading处理
  8. 性能监控 - 追踪和分析chunk加载性能
  9. 高级技巧 - 智能预加载、动态优化
  10. 错误处理 - 完善的失败重试机制
  11. 实战案例 - 真实项目优化经验
  12. CI/CD集成 - 自动化性能检查

合理的代码分割是提升应用性能的关键技术,通过系统化的分割策略可以显著改善用户体验。

扩展阅读