前端项目体积膨胀是性能杀手。一个未经优化的单页应用,首次加载可能需要下载数 MB 的 JavaScript 文件,移动端弱网环境动辄加载十几秒。代码分割、Tree Shaking、按需加载,正是针对这一问题的三把手术刀。
代码分割(Code Splitting)
代码分割的核心思想是“不要一次把所有代码都发给用户,而是按需切分成多个小块,用到时再加载”。
Webpack 中,入口配置(entry)的多入口打包是最原始的分割形式,但更常用的是动态导入(import()):
// 点击按钮时才加载对应的模块
button.addEventListener('click', () => {
import('./heavy-chart.js').then(module => {
module.renderChart();
});
});
import() 返回一个 Promise,构建工具会将它及其依赖单独打包成一个 chunk,用户点击时才发起网络请求加载。这避免了初始加载包体积过大,让首次渲染更快。
Vite 同样原生支持动态导入,并且基于 ES Modules 的开发服务器能让模块按需原生加载,开发体验更丝滑。生产构建时由 Rollup 完成代码分割。
Tree Shaking
Tree Shaking 是“摇树优化”的形象说法,指自动剔除未使用的代码。它依赖 ES Modules 的静态结构特性:ESM 的导入导出在编译阶段就能确定依赖关系,而 CommonJS 的 require() 是动态的,难以静态分析。
// math.js
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
// main.js
import { add } from './math.js';
add(1, 2);
构建工具分析后发现 subtract 没有被任何地方引用,便将其从最终产物中剔除。Webpack 在生产模式(mode: 'production')下默认启用 Tree Shaking,需要配合 ES Modules 语法,并且在 package.json 中标记无副作用文件("sideEffects": false)。Vite(Rollup)同样内置 Tree Shaking,效果通常更彻底,因为 Rollup 本身就是为 ES Modules 设计的静态打包器。
注意事项:Tree Shaking 不是万能的。如果代码中有副作用(如修改全局变量、立即执行函数),工具为了保证运行正确,可能不敢切除。这也是为什么很多库会在 package.json 中声明 sideEffects 字段,告诉构建工具“放心删,我没副作用”。
按需加载(Lazy Loading)
按需加载是一种策略,而非具体技术。代码分割是实现按需加载的主要手段,但按需加载的范围更广,包括组件、路由、图片等任何资源的延迟加载。
路由级别的按需加载 是 SPA 优化中最立竿见影的手段:
// Vue Router 示例
const routes = [
{ path: '/dashboard', component: () => import('./views/Dashboard.vue') },
{ path: '/settings', component: () => import('./views/Settings.vue') }
];
只有当用户访问对应路由时,该页面的 JS、CSS 才会被加载。这样一来,首屏只加载当前页面需要的代码,其他页面“沉睡”在服务端或 CDN 上。React 中 React.lazy() + Suspense 同理。
组件级的按需加载 将粒度做得更细。比如一个富文本编辑器组件只在用户聚焦输入框时才加载,大幅降低首屏 JS 体积。配合图片懒加载、非关键 CSS 延迟加载等,整个页面的资源加载顺序被精细管控。
三者关系与协作
它们不是孤立的技术,而是互补的优化维度:
- 代码分割 是基础设施,决定如何切分代码;
- Tree Shaking 是质量优化,减少每个 chunk 内部的冗余代码;
- 按需加载 是策略,决定何时触发这些 chunk 的加载。
一个典型的现代前端项目,会让构建工具自动进行 Tree Shaking,通过动态导入实现路由级和组件级的代码分割,再结合图片懒加载、字体延迟加载等,最终呈现出一个“轻快首屏,流畅后续”的用户体验。这是前端性能优化的基本功,也是面试中衡量工程化能力的关键考点。