前端资源合并的实战优化技巧与性能提升策略

UI俊荣 优化 阅读 2,395
赞 19 收藏
二维码
手机扫码查看
反馈

先说结论:我现在基本只用 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 的根本原因——我不想把时间花在调配置上。

我的选型逻辑

现在我判断用不用某种资源合并方案,就看三个问题:

  1. 开发时能不能快速看到修改效果?(HMR 支持)
  2. 要不要手动维护依赖顺序?
  3. 配置复杂度是否随项目增长指数上升?

手工和 Gulp 在第一条就挂了;Webpack 第二条勉强过关,第三条经常爆雷;Vite 三项全绿。

当然也有例外:

  • 超小型静态页(就两三个文件)——直接手写 script 标签也行
  • 必须兼容 IE 的项目——还是乖乖用 Webpack 吧
  • 已有成熟 Webpack 体系的大项目——别轻易迁移,成本太高

但只要是新项目,我都推荐 Vite。它不是万能的,但在“省心 + 高效”这个维度上,目前没有对手。

以上是我的对比总结,有更优的实现方式欢迎评论区交流

资源合并本质上是个权衡问题:你要的是极致控制,还是开发效率?我以前追求前者,现在更倾向于后者。毕竟我们是开发者,不是构建工具专家。

如果你还在用 Gulp 做 JS 合并,真的可以考虑升级一下工具链了。不是新技术一定好,而是有些苦没必要吃。

至于未来?我关注的是打包即服务(如 Netlify Build Plugins)和 CDN 边缘构建,也许有一天我们连本地 build 都不需要了。但现在嘛,Vite 已经够我用了。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论