代码分割

代码分割是优化应用性能的重要手段。本章介绍为什么需要代码分割,以及如何通过 Entry 分割、SplitChunks 和动态导入实现代码分割。


📋 学习目标

  • ✅ 理解为什么需要代码分割
  • ✅ 掌握 Entry 分割
  • ✅ 掌握 SplitChunks 配置
  • ✅ 学会使用动态导入
  • ✅ 理解懒加载和预加载

为什么需要代码分割

问题

将所有代码打包到一个文件中会导致:

  • 文件体积过大
  • 首次加载时间长
  • 无法利用浏览器缓存
  • 用户体验差

解决方案

代码分割可以将代码拆分成多个小文件:

  • 按需加载
  • 并行加载
  • 利用浏览器缓存
  • 提升加载速度

Entry 分割

基础分割

module.exports = {
  entry: {
    main: './src/index.js',
    vendor: './src/vendor.js'
  },
  output: {
    filename: '[name].bundle.js'
  }
}

分离第三方库

module.exports = {
  entry: {
    main: './src/index.js',
    vendor: ['react', 'react-dom', 'lodash']
  }
}

问题:手动维护,容易出错


SplitChunks 自动分割

基础配置

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
}

完整配置

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',  // 'initial' | 'async' | 'all'
      minSize: 20000,  // 最小 chunk 大小(字节)
      maxSize: 0,  // 最大 chunk 大小
      minChunks: 1,  // 最小引用次数
      maxAsyncRequests: 30,  // 最大异步请求数
      maxInitialRequests: 30,  // 最大初始请求数
      cacheGroups: {
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        },
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: -10,
          reuseExistingChunk: true
        }
      }
    }
  }
}

chunks 选项

  • 'initial':只分割初始 chunk
  • 'async':只分割异步 chunk(动态导入)
  • 'all':分割所有 chunk(推荐)

cacheGroups 配置

module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        // 默认组
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        },
        
        // 第三方库
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: 10,
          chunks: 'all'
        },
        
        // 公共代码
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true
        },
        
        // React 相关
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'react',
          priority: 20,
          chunks: 'all'
        }
      }
    }
  }
}

动态导入(Dynamic Import)

基础用法

// 动态导入
import('./module').then(module => {
  module.doSomething()
})

React 懒加载

import React, { lazy, Suspense } from 'react'
 
const Home = lazy(() => import('./pages/Home'))
const About = lazy(() => import('./pages/About'))
 
function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Suspense>
  )
}

Vue 懒加载

const routes = [
  {
    path: '/home',
    component: () => import('./pages/Home.vue')
  },
  {
    path: '/about',
    component: () => import('./pages/About.vue')
  }
]

预加载和预获取

预加载(Preload)

// 高优先级,立即加载
import(/* webpackPreload: true */ './module')

预获取(Prefetch)

// 低优先级,空闲时加载
import(/* webpackPrefetch: true */ './module')

使用场景

预加载:关键资源,需要立即使用

// 用户点击按钮后立即需要
button.onclick = () => {
  import(/* webpackPreload: true */ './dialog')
}

预获取:可能需要的资源

// 路由懒加载,预获取下一个页面
const Home = lazy(() => import(/* webpackPrefetch: true */ './pages/Home'))

代码分割策略

策略一:按框架分离

module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        framework: {
          test: /[\\/]node_modules[\\/](react|react-dom|react-router)[\\/]/,
          name: 'framework',
          priority: 40,
          chunks: 'all'
        }
      }
    }
  }
}

策略二:按功能分离

module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        utils: {
          test: /[\\/]src[\\/]utils[\\/]/,
          name: 'utils',
          priority: 10,
          chunks: 'all'
        },
        components: {
          test: /[\\/]src[\\/]components[\\/]/,
          name: 'components',
          priority: 10,
          chunks: 'all'
        }
      }
    }
  }
}

策略三:按页面分离

// 使用动态导入
const Home = lazy(() => import('./pages/Home'))
const About = lazy(() => import('./pages/About'))
const Contact = lazy(() => import('./pages/Contact'))

完整配置示例

module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all',
      minSize: 20000,
      maxSize: 244000,
      cacheGroups: {
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        },
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          priority: -10,
          chunks: 'all'
        },
        common: {
          minChunks: 2,
          priority: 5,
          reuseExistingChunk: true
        },
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'react',
          priority: 20,
          chunks: 'all'
        }
      }
    },
    runtimeChunk: {
      name: 'runtime'
    }
  }
}

最佳实践

1. 分离运行时代码

module.exports = {
  optimization: {
    runtimeChunk: 'single'  // 或 { name: 'runtime' }
  }
}

2. 合理设置 chunk 大小

module.exports = {
  optimization: {
    splitChunks: {
      minSize: 20000,
      maxSize: 244000  // 避免 chunk 过大
    }
  }
}

3. 使用动态导入

// ✅ 推荐:按需加载
const Component = lazy(() => import('./Component'))
 
// ❌ 不推荐:一次性加载所有
import Component from './Component'

4. 配置预获取

// 预获取可能访问的页面
const NextPage = lazy(() => 
  import(/* webpackPrefetch: true */ './pages/NextPage')
)

总结

  • Entry 分割:手动分离入口
  • SplitChunks:自动分割代码
  • 动态导入:按需加载模块
  • 预加载/预获取:优化加载策略
  • 最佳实践:分离运行时代码,合理设置大小

下一步


Webpack 代码分割 SplitChunks 动态导入 懒加载