Skip to content
On this page

Webpack 代码分割

代码分割是 Webpack 的核心功能之一,它允许我们将代码拆分成多个 bundle,以便按需加载,从而优化应用程序的性能和加载时间。

代码分割基础概念

代码分割的类型

javascript
// 1. 入口点分割
module.exports = {
  entry: {
    home: './src/home.js',
    about: './src/about.js',
    contact: './src/contact.js'
  }
};

// 2. 动态导入分割
// src/index.js
import('./components/LazyComponent').then(module => {
  const LazyComponent = module.default;
  // 使用组件
});

// 3. 配置分割
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
};

代码分割的好处

javascript
// 减少初始加载时间
// - 用户只加载需要的代码
// - 更小的初始 bundle
// - 并行加载多个小文件
// - 更好的缓存策略

动态导入

基础动态导入

javascript
// 动态导入单个模块
async function loadLodash() {
  const { default: _ } = await import('lodash');
  return _;
}

// 动态导入多个模块
async function loadModules() {
  const [homeModule, aboutModule] = await Promise.all([
    import('./home'),
    import('./about')
  ]);
  
  return {
    home: homeModule.default,
    about: aboutModule.default
  };
}

// 条件导入
async function loadFeature(featureName) {
  switch (featureName) {
    case 'calendar':
      const { Calendar } = await import('./calendar');
      return Calendar;
    case 'chart':
      const { Chart } = await import('./chart');
      return Chart;
    default:
      return null;
  }
}

React 中的动态导入

javascript
import React, { Suspense, lazy } from 'react';

// 懒加载组件
const Home = lazy(() => import('./components/Home'));
const About = lazy(() => import('./components/About'));
const Contact = lazy(() => import('./components/Contact'));

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

Vue 中的动态导入

javascript
// Vue 路由懒加载
const routes = [
  {
    path: '/',
    name: 'Home',
    component: () => import('./views/Home.vue')
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('./views/About.vue')
  },
  {
    path: '/profile',
    name: 'Profile',
    component: () => import('./views/Profile.vue')
  }
];

// 组件懒加载
export default {
  components: {
    LazyComponent: () => import('./LazyComponent.vue')
  }
};

SplitChunksPlugin 配置

基础配置

javascript
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',                      // 'initial' | 'async' | 'all'
      minSize: 20000,                    // 最小大小,小于这个值的不会分割
      maxSize: 244000,                   // 最大大小,超过这个值会被分割
      minChunks: 1,                      // 最少被引用次数
      maxAsyncRequests: 30,               // 按需加载最大请求数
      maxInitialRequests: 30,             // 入口点最大并行请求数
      automaticNameDelimiter: '~',        // 文件名连接符
      cacheGroups: {                     // 缓存组
        default: {
          minChunks: 2,                  // 默认缓存组配置
          priority: -20,
          reuseExistingChunk: true       // 重用已存在的块
        }
      }
    }
  }
};

高级缓存组配置

javascript
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // 第三方库
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10,
          chunks: 'all'
        },
        
        // React 生态
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'react',
          priority: 20,
          chunks: 'all'
        },
        
        // 样式文件
        styles: {
          name: 'styles',
          test: /\.css$/,
          chunks: 'all',
          enforce: true                  // 强制创建 chunk
        },
        
        // 大型库单独分割
        largeLibraries: {
          test: /[\\/]node_modules[\\/](moment|lodash|axios)[\\/]/,
          name: 'large-libraries',
          priority: 15,
          chunks: 'all'
        },
        
        // 公共代码
        common: {
          name: 'common',
          minChunks: 2,                  // 至少被 2 个 chunk 使用
          priority: 5,
          chunks: 'all',
          minSize: 0                     // 即使很小也要分割
        }
      }
    }
  }
};

按功能分割

javascript
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // 认证相关
        auth: {
          test: /[\\/]src[\\/]auth[\\/]/,
          name: 'auth',
          priority: 8,
          chunks: 'all'
        },
        
        // UI 组件
        ui: {
          test: /[\\/]src[\\/]components[\\/]/,
          name: 'ui',
          priority: 8,
          chunks: 'all'
        },
        
        // 工具函数
        utils: {
          test: /[\\/]src[\\/]utils[\\/]/,
          name: 'utils',
          priority: 8,
          chunks: 'all'
        }
      }
    }
  }
};

预加载和预获取

Webpack 魔法注释

javascript
// 预加载 - 立即加载
const component = import(
  /* webpackPreload: true */
  './CriticalComponent'
);

// 预获取 - 空闲时加载
const component = import(
  /* webpackPrefetch: true */
  './FutureComponent'
);

// 自定义 chunk 名称
const component = import(
  /* webpackChunkName: "my-chunk" */
  './MyComponent'
);

// 预加载和自定义名称
const component = import(
  /* webpackPreload: true */
  /* webpackChunkName: "critical" */
  './CriticalComponent'
);

实际应用示例

javascript
// 路由级别的预加载
const routes = [
  {
    path: '/dashboard',
    component: () => import(
      /* webpackPreload: true */
      './Dashboard'
    )
  },
  {
    path: '/profile',
    component: () => import(
      /* webpackPrefetch: true */
      './Profile'
    )
  }
];

// 事件驱动的预获取
function onHover() {
  import(
    /* webpackPrefetch: true */
    './HeavyComponent'
  );
}

// 条件预加载
if (window.Connection && navigator.connection.effectiveType === '4g') {
  import(
    /* webpackPreload: true */
    './HighQualityAssets'
  );
}

高级代码分割策略

按页面分割

javascript
// 多页面应用配置
module.exports = {
  entry: {
    home: './src/pages/Home/index.js',
    about: './src/pages/About/index.js',
    contact: './src/pages/Contact/index.js'
  },
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // 公共页面代码
        pageCommon: {
          test: /[\\/]src[\\/]pages[\\/]/,
          minChunks: 2,
          name: 'page-common',
          priority: 10,
          chunks: 'all'
        },
        
        // 第三方库
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 20,
          chunks: 'all'
        }
      }
    }
  }
};

按功能分割

javascript
// 按功能模块分割
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        // 认证功能
        auth: {
          test: module => {
            return (
              module.resource &&
              (
                module.resource.includes('auth') ||
                module.resource.includes('login') ||
                module.resource.includes('register')
              )
            );
          },
          name: 'auth',
          priority: 15
        },
        
        // 数据处理功能
        data: {
          test: module => {
            return (
              module.resource &&
              (
                module.resource.includes('api') ||
                module.resource.includes('services') ||
                module.resource.includes('store')
              )
            );
          },
          name: 'data',
          priority: 15
        },
        
        // UI 组件
        ui: {
          test: module => {
            return (
              module.resource &&
              module.resource.includes('components')
            );
          },
          name: 'ui',
          priority: 10
        }
      }
    }
  }
};

按使用频率分割

javascript
// 高频使用组件
const FrequentlyUsedComponent = lazy(() => import('./FrequentlyUsedComponent'));

// 低频使用组件
const RarelyUsedComponent = lazy(() => 
  import(
    /* webpackPrefetch: true */
    './RarelyUsedComponent'
  )
);

// 一次性使用组件
const OneTimeComponent = lazy(() => 
  import(
    /* webpackChunkName: "one-time" */
    './OneTimeComponent'
  )
);

代码分割最佳实践

1. 合理的分割粒度

javascript
// 不要分割得太细
// ❌ 太细的分割
const Button = lazy(() => import('./Button'));
const Input = lazy(() => import('./Input'));
const Modal = lazy(() => import('./Modal'));

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

2. 考虑加载时机

javascript
// 立即需要的功能 - 预加载
const Dashboard = lazy(() => import(
  /* webpackPreload: true */
  './Dashboard'
));

// 将来可能需要的功能 - 预获取
const Settings = lazy(() => import(
  /* webpackPrefetch: true */
  './Settings'
));

3. 网络状况感知

javascript
// 根据网络状况调整策略
function getNetworkAwareImport(componentPath, options = {}) {
  const connection = navigator.connection;
  
  if (connection) {
    const { effectiveType } = connection;
    
    if (effectiveType === 'slow-2g' || effectiveType === '2g') {
      // 慢网络下不使用预加载
      return import(componentPath);
    } else if (effectiveType === '3g') {
      // 3G 网络下谨慎使用预加载
      return import(
        options.prefetch ? 
        /* webpackPrefetch: true */ componentPath : 
        componentPath
      );
    } else {
      // 快速网络下可以预加载
      return import(
        /* webpackPreload: true */ componentPath
      );
    }
  }
  
  // 默认情况
  return import(componentPath);
}

监控和分析

代码分割分析

javascript
// webpack-bundle-analyzer 配置
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

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

加载性能监控

javascript
// 监控动态导入性能
async function loadWithMonitoring(path, name) {
  const startTime = performance.now();
  
  try {
    const module = await import(path);
    
    const endTime = performance.now();
    console.log(`${name} loaded in ${endTime - startTime}ms`);
    
    return module;
  } catch (error) {
    console.error(`Failed to load ${name}:`, error);
    throw error;
  }
}

// 使用示例
const component = await loadWithMonitoring('./MyComponent', 'MyComponent');

代码分割配置示例

javascript
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      maxInitialRequests: 10,
      maxAsyncRequests: 10,
      cacheGroups: {
        // 第三方库
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 20,
          chunks: 'all',
          maxSize: 244000
        },
        
        // React 生态
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom|react-router)[\\/]/,
          name: 'react',
          priority: 30,
          chunks: 'all'
        },
        
        // 公共代码
        common: {
          name: 'common',
          minChunks: 2,
          priority: 10,
          chunks: 'all',
          minSize: 0
        },
        
        // 样式
        styles: {
          name: 'styles',
          test: /\.css$/,
          chunks: 'all',
          enforce: true
        }
      }
    },
    
    // 运行时代码分离
    runtimeChunk: {
      name: 'runtime'
    }
  }
};

小结

代码分割是优化 Webpack 构建结果的重要手段。通过合理的分割策略,可以显著提升应用的加载性能。关键是要根据应用的特点选择合适的分割方式,并结合预加载和预获取技术,为用户提供最佳的加载体验。