代码分割的本质是什么?有什么意义呢?
问题解析
代码分割(Code Splitting)是 Webpack 的核心特性之一,它解决了源代码直接上线和打包成唯一脚本两种极端方案的问题,在服务器性能和用户体验之间找到平衡点。
核心概念
三种部署方案的对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 源代码直接上线 | 按需加载,无冗余 | HTTP 请求多,性能开销大 |
| 打包成唯一脚本 | 请求少,缓存友好 | 页面空白期长,首屏慢 |
| 代码分割 | 平衡两者优势 | 需要合理配置 |
代码分割的本质
代码分割 = 源代码直接上线 和 打包成唯一脚本 之间的中间状态
本质:用可接受的服务器性能压力增加换取更好的用户体验
详细解答
方案对比详解
方案 1:源代码直接上线
项目结构:
src/
├── utils/
│ ├── helper.js -> HTTP 请求 1
│ ├── format.js -> HTTP 请求 2
│ └── validate.js -> HTTP 请求 3
├── components/
│ ├── Button.jsx -> HTTP 请求 4
│ ├── Input.jsx -> HTTP 请求 5
│ └── Modal.jsx -> HTTP 请求 6
└── pages/
├── Home.jsx -> HTTP 请求 7
└── About.jsx -> HTTP 请求 8
问题:
- 8 个 HTTP 请求,建立连接开销大
- 浏览器并发请求数限制(通常 6-8 个)
- 请求排队,加载时间延长
方案 2:打包成唯一脚本
// webpack.config.js
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js' // 所有代码打包成一个文件
}
};
问题:
- 文件体积巨大(可能几 MB)
- 首屏加载时间极长
- 页面空白期严重影响用户体验
- 修改一行代码,整个缓存失效
方案 3:代码分割(推荐)
// webpack.config.js
module.exports = {
entry: {
main: './src/index.js',
vendor: './src/vendor.js'
},
output: {
filename: '[name].[chunkhash:8].js'
},
optimization: {
splitChunks: {
chunks: 'all'
}
}
};
优势:
- 合理的文件数量(通常 3-10 个)
- 首屏加载快,按需加载其他代码
- 缓存利用率高,公共代码单独打包
- 并行加载,充分利用浏览器并发
代码分割的实现方式
1. 入口分割(Entry Splitting)
module.exports = {
entry: {
app: './src/app.js',
admin: './src/admin.js' // 独立的入口
},
output: {
filename: '[name].js'
}
};
2. 动态导入(Dynamic Import)
// 路由懒加载
const Home = () => import(/* webpackChunkName: "home" */ './pages/Home');
const About = () => import(/* webpackChunkName: "about" */ './pages/About');
// 条件加载
if (needChart) {
const Chart = await import(/* webpackChunkName: "chart" */ './components/Chart');
}
// 事件触发加载
button.addEventListener('click', () => {
import(/* webpackChunkName: "modal" */ './components/Modal')
.then(module => {
module.showModal();
});
});
3. SplitChunksPlugin
module.exports = {
optimization: {
splitChunks: {
// 选择哪些 chunk 进行分割
chunks: 'all', // async(异步), initial(同步), all(全部)
// 最小分割大小(字节)
minSize: 20000, // 20KB
// 最大分割大小
maxSize: 0,
// 最小被引用次数
minChunks: 1,
// 最大异步请求数
maxAsyncRequests: 30,
// 最大初始请求数
maxInitialRequests: 30,
// 缓存组
cacheGroups: {
// 第三方库
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 10 // 优先级
},
// 公共代码
common: {
minChunks: 2,
chunks: 'all',
enforce: true
}
}
}
}
};
深入理解
代码分割的加载流程
用户访问页面
|
v
加载 HTML + 关键 CSS/JS (首屏)
|
v
页面可交互(首屏渲染完成)
|
v
按需加载其他模块(懒加载)
|
v
预加载后续可能需要的模块(预加载)
预加载和预获取
// 预获取:可能在将来需要(导航到其他页面)
import(/* webpackPrefetch: true */ './pages/About');
// 预加载:当前页面即将需要
import(/* webpackPreload: true */ './components/Chart');
<!-- prefetch:在浏览器空闲时加载 -->
<link rel="prefetch" href="about.js">
<!-- preload:立即加载,优先级高 -->
<link rel="preload" href="chart.js" as="script">
代码分割与缓存策略
module.exports = {
output: {
// 使用 contenthash 确保缓存有效
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js'
},
optimization: {
splitChunks: {
cacheGroups: {
// 第三方库单独打包,缓存时间长
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
// 获取包名
const packageName = module.context.match(
/[\\/]node_modules[\\/](.*?)([\\/]|$)/
)[1];
return `npm.${packageName.replace('@', '')}`;
}
}
}
},
// 运行时代码单独打包
runtimeChunk: {
name: 'runtime'
}
}
};
最佳实践
React 路由代码分割
import React, { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// 懒加载页面组件
const Home = lazy(() => import(/* webpackChunkName: "home" */ './pages/Home'));
const About = lazy(() => import(/* webpackChunkName: "about" */ './pages/About'));
const Dashboard = lazy(() => import(/* webpackChunkName: "dashboard" */ './pages/Dashboard'));
function App() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
Vue 路由代码分割
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'Home',
component: () => import(/* webpackChunkName: "home" */ '../views/Home.vue')
},
{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
完整的代码分割配置
const path = require('path');
module.exports = {
entry: {
main: './src/index.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/[name].[contenthash:8].js',
chunkFilename: 'js/[name].[contenthash:8].chunk.js',
clean: true
},
optimization: {
splitChunks: {
chunks: 'all',
minSize: 20000,
maxSize: 244000, // 244KB,超过则尝试分割
minChunks: 1,
maxAsyncRequests: 30,
maxInitialRequests: 30,
enforceSizeThreshold: 50000,
cacheGroups: {
// React 相关库
reactVendor: {
test: /[\\/]node_modules[\\/](react|react-dom|react-router-dom)[\\/]/,
name: 'react-vendor',
chunks: 'all',
priority: 20
},
// UI 组件库
uiVendor: {
test: /[\\/]node_modules[\\/](antd|@ant-design)[\\/]/,
name: 'ui-vendor',
chunks: 'all',
priority: 15
},
// 工具库
utilsVendor: {
test: /[\\/]node_modules[\\/](lodash|moment|axios)[\\/]/,
name: 'utils-vendor',
chunks: 'all',
priority: 10
},
// 其他第三方库
defaultVendors: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 5,
reuseExistingChunk: true
},
// 公共代码
common: {
minChunks: 2,
chunks: 'all',
enforce: true,
priority: 1
}
}
},
// 运行时代码单独打包
runtimeChunk: {
name: 'runtime'
}
}
};
性能监控指标
// 监控代码分割效果
window.addEventListener('load', () => {
// 首屏加载时间
const timing = performance.timing;
const loadTime = timing.loadEventEnd - timing.navigationStart;
console.log(`Page load time: ${loadTime}ms`);
// 资源加载情况
const resources = performance.getEntriesByType('resource');
const jsResources = resources.filter(r => r.name.endsWith('.js'));
console.log(`JS files loaded: ${jsResources.length}`);
console.log(`Total JS size: ${jsResources.reduce((sum, r) => sum + r.transferSize, 0)} bytes`);
});
面试要点
-
代码分割的本质:源代码直接上线和打包成唯一脚本之间的中间状态,用可接受的服务器性能压力换取更好的用户体验
-
三种方案的对比:
- 源代码直接上线:HTTP 请求多,性能开销大
- 打包成唯一脚本:页面空白期长,用户体验不好
- 代码分割:平衡两者,合理分割文件
-
实现方式:
- 入口分割(Entry Splitting)
- 动态导入(Dynamic Import)
- SplitChunksPlugin
-
SplitChunksPlugin 配置:
chunks: 选择哪些 chunk 进行分割minSize: 最小分割大小cacheGroups: 缓存组配置
-
预加载策略:
webpackPrefetch: 预获取,空闲时加载webpackPreload: 预加载,高优先级加载
-
最佳实践:
- 路由懒加载
- 第三方库单独打包
- 运行时代码单独打包
- 使用 contenthash 优化缓存