前端资源合并的实战优化技巧与性能提升策略
先说结论:我现在基本只用 Vite 的预构建
资源合并这事儿我折腾了好几年,从最早手动 concat JS 文件,到 Grunt、Gulp 配置一堆 task,再到 Webpack 的 code splitting 和 splitChunks,最后到现在的 Vite。说实话,现在回过头看,很多方案都是阶段性产物——能用,但不优雅。
我现在新项目基本直接上 Vite,利用它的 预构建(prebundling) 和 按需加载 + rollup 打包 组合拳,开发体验和构建结果都挺舒服。但这也不是拍脑袋决定的,是踩了太多坑之后的选择。
这篇文章我就把几个主流方案拉出来遛一遛,讲讲它们在真实项目中的表现,尤其是那些文档里不写但你迟早会遇到的坑。
谁更灵活?谁更省事?
我们主要比这几个:
- 纯手工合并(fs + node script)
- Gulp + gulp-concat
- Webpack splitChunks
- Vite 预构建 + 动态导入
先亮态度:手工和 Gulp 我已经淘汰了;Webpack 能用但配置太重;Vite 是我现在的心头好,不是因为它多先进,而是它让我不用操心大多数问题。
手工合并:真有人这么干?有,我还干过
以前接了个老后台系统,没构建工具,所有 JS 直接 script 标签引入,顺序还不能错。上线前要打包成一个文件,怎么办?写个 Node 脚本:
const fs = require('fs');
const files = [
'./src/lib/jquery.js',
'./src/util/helper.js',
'./src/module/user.js',
'./src/app.js'
];
let bundle = '';
files.forEach(file => {
bundle += fs.readFileSync(file, 'utf-8') + 'n';
});
fs.writeFileSync('./dist/bundle.js', bundle);
看着简单吧?但问题一堆:
- 依赖顺序靠人肉维护,改个模块顺序就得重新测试
- 没有压缩,上线还得再走一遍 uglify
- 没法做 HMR,改一行代码得重新跑脚本
- 后期加了 CSS 合并,逻辑越来越复杂,最后变成“发布专用 mini 构建系统”
这个方案唯一的优点是透明——你知道每一步发生了什么。但它不适合超过 3 个页面的项目。我后来把它升级成了 Gulp。
Gulp:比手工强点,但也只是“强点”
上了 Gulp 之后,至少自动化了。这是典型的 concat 配置:
const gulp = require('gulp');
const concat = require('gulp-concat');
const uglify = require('gulp-uglify');
gulp.task('js', () => {
return gulp.src([
'src/js/vendor/*.js',
'src/js/modules/*.js',
'src/js/app.js'
])
.pipe(concat('bundle.min.js'))
.pipe(uglify())
.pipe(gulp.dest('dist/js/'));
});
看起来不错对吧?但实际用起来你会发现:
- 模块之间的依赖关系依然靠文件顺序隐式管理
- 加个第三方库?得手动挪进 vendor 文件夹
- 想拆分公共代码?得额外配
gulp-if或者多个 task - tree-shaking?别想了,concat 就是全量合并
最让我崩溃的一次是:某个模块用了 ES6 模块语法,而另一个用的是 IIFE,concat 完直接报错。折腾了半天发现是作用域污染。这种问题在现代构建工具里早就不是问题了。
所以我的结论是:Gulp 适合做文件处理流水线(比如图片压缩、字体转换),但不适合做 JS 资源合并的核心方案。它太底层了,等于你自己造轮子。
Webpack:功能全,但容易把自己绕进去
Webpack 的 splitChunks 看似强大,实则是个“高风险高回报”的配置项。这是我见过最复杂的默认配置之一。
举个典型场景:你有多个页面,每个页面有自己的 entry,又共享一些库(React、Lodash)。你希望:
- 第三方库打成一个 vendor 包
- 公共工具函数抽成 common 包
- 每个页面保留独立入口
于是你写下这段配置:
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\/]node_modules[\/]/,
name: 'vendors',
priority: 10,
reuseExistingChunk: true,
},
common: {
name: 'common',
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
}
}
}
}
};
初看 OK,但很快你会发现问题:
- 某些小模块也被抽出去了,导致请求数变多
- vendor 包太大,更新一次全缓存失效
- chunk 名字带 hash,但 HTML 插入靠 html-webpack-plugin,部署流程变复杂
- 开发环境启动慢,尤其 node_modules 多的时候
而且一旦项目结构变化,比如新增异步组件,splitChunks 行为可能突变。我曾经因为加了个动态 import,导致原本共用的 util 函数被重复打包进两个 chunk,排查了整整一天。
我不是说 Webpack 不好,它是工业级解决方案。但它的学习成本和维护成本太高了。如果你团队里没人真正吃透它,很容易陷入“改一处崩三处”的境地。
Vite:我为什么现在首选它
Vite 在资源合并这件事上的思路很清晰:开发阶段不做合并,生产阶段用 Rollup 自动优化。
你只需要这样写代码:
// main.js
import { formatUser } from './utils/user';
import { apiClient } from './services/api';
document.getElementById('app').innerHTML =
<h1>Welcome ${formatUser('vite user')}</h1>
;
// 动态加载非关键模块
if (window.location.pathname === '/admin') {
import('./admin/dashboard').then(mod => mod.init());
}
然后 build 的时候,Vite 默认就会:
- 把 node_modules 里的依赖预构建为一个或多个 vendor chunk
- 分析模块图谱,自动分割公共部分
- 给每个输出文件加上 content-hash
- 支持 dynamic import 的 code splitting
你几乎不需要配 anything。当然你可以自定义:
// vite.config.js
export default {
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom', 'lodash'],
ui: ['antd'],
}
}
}
}
}
但大多数情况下,默认就够用了。
最爽的是开发体验:ESM 原生加载,改一个文件 HMR 几乎瞬间完成。不像 Webpack 动不动就要 rebuild 整个 module graph。
唯一需要注意的是:Vite 对 IE11 不支持(毕竟是基于 ESM),如果你还在维护老项目,得三思。但新项目?闭眼上就完事了。
性能对比:差距比我想象的小
我一直以为不同方案打包出来的文件大小会有显著差异。于是我在同一个项目试了三种方案:
- Gulp concat + uglify
- Webpack splitChunks
- Vite 默认 build
结果出乎意料:最终产物 Gzipped 后差距都在 5KB 以内。真正影响首屏性能的反而是:
- 是否开启了 gzip / brotli
- 关键资源是否内联或 preload
- 是否有大量同步阻塞脚本
也就是说,**选哪个构建工具,对最终体积影响不大;但对开发效率和维护成本影响巨大**。
这也是我转向 Vite 的根本原因——我不想把时间花在调配置上。
我的选型逻辑
现在我判断用不用某种资源合并方案,就看三个问题:
- 开发时能不能快速看到修改效果?(HMR 支持)
- 要不要手动维护依赖顺序?
- 配置复杂度是否随项目增长指数上升?
手工和 Gulp 在第一条就挂了;Webpack 第二条勉强过关,第三条经常爆雷;Vite 三项全绿。
当然也有例外:
- 超小型静态页(就两三个文件)——直接手写 script 标签也行
- 必须兼容 IE 的项目——还是乖乖用 Webpack 吧
- 已有成熟 Webpack 体系的大项目——别轻易迁移,成本太高
但只要是新项目,我都推荐 Vite。它不是万能的,但在“省心 + 高效”这个维度上,目前没有对手。
以上是我的对比总结,有更优的实现方式欢迎评论区交流
资源合并本质上是个权衡问题:你要的是极致控制,还是开发效率?我以前追求前者,现在更倾向于后者。毕竟我们是开发者,不是构建工具专家。
如果你还在用 Gulp 做 JS 合并,真的可以考虑升级一下工具链了。不是新技术一定好,而是有些苦没必要吃。
至于未来?我关注的是打包即服务(如 Netlify Build Plugins)和 CDN 边缘构建,也许有一天我们连本地 build 都不需要了。但现在嘛,Vite 已经够我用了。

暂无评论