前端资源合并优化实战:减少请求提升加载性能

博主佳怡 优化 阅读 2,364
赞 26 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

资源合并这事儿,说简单也简单,说坑也真不少。我最早搞前端那会儿,项目里一堆 CSS 和 JS 文件,首屏加载慢得像蜗牛爬。后来开始搞合并,但一开始也是乱合并一通——把所有 JS 全塞进一个 bundle.js,结果用户打开首页还得下载后台管理页面的代码,纯属自找麻烦。

前端资源合并优化实战:减少请求提升加载性能

折腾了几个项目后,我现在基本固定了一套做法:按路由/页面粒度做拆分,核心依赖单独抽出来,再配合 HTTP/2 的多路复用(别迷信 HTTP/2 就不用合并了,小文件太多照样慢)。下面是我现在项目的典型配置(基于 Webpack):

// webpack.config.js
module.exports = {
  entry: {
    // 核心库单独打包,长期缓存
    vendor: ['react', 'react-dom', 'lodash'],
    // 首页独立入口
    home: './src/pages/home/index.js',
    // 后台页面独立入口
    admin: './src/pages/admin/index.js'
  },
  optimization: {
    splitChunks: {
      chunks: 'all',
      cacheGroups: {
        vendor: {
          name: 'vendor',
          chunks: 'all',
          test: /[\/]node_modules[\/]/,
          priority: 10
        },
        // 公共组件提取
        common: {
          name: 'common',
          minChunks: 2,
          chunks: 'all',
          enforce: true
        }
      }
    }
  }
};

这么干的好处很明显:首页只加载 home.js + vendor.js + common.js,体积可控;用户切到后台页面才加载 admin.js。而且 vendor.js 内容基本不变,CDN 缓存命中率高。实测首屏 JS 体积从 800KB+ 降到 200KB 左右,FCP 时间直接砍半。

这几种错误写法,别再踩坑了

我见过最离谱的合并方式,是把所有静态资源无脑 concat 成一个文件。比如这样:

# 错误示范:粗暴合并
cat header.css footer.css sidebar.css > all.css
cat lib1.js lib2.js page1.js page2.js > all.js

这种写法在小型静态站可能凑合能用,但一旦项目复杂起来就完蛋。问题在哪?

  • 冗余加载:用户看首页却要下载整站 CSS,移动端流量伤不起
  • 缓存失效:改一行页脚 CSS 导致整个 all.css 缓存失效
  • 阻塞渲染:CSS 合并后体积大,解析时间变长,白屏时间增加

还有人喜欢在 HTML 里手动拼接 script 标签,美其名曰“精细控制”:

<!-- 反面教材 -->
<script src="/js/lib/react.min.js"></script>
<script src="/js/lib/lodash.min.js"></script>
<script src="/js/components/header.js"></script>
<script src="/js/components/sidebar.js"></script>
<script src="/js/pages/home.js"></script>

表面看是按需加载,实际每个请求都有 TCP 握手开销(尤其 HTTP/1.1 下)。我曾经接手一个老项目就这么干,首页光 JS 请求就有 15 个,TTI(可交互时间)高达 8 秒。后来改成按路由合并后,降到 3 秒内。

另外注意别过度拆分!有些同学听说“代码分割好”,就把每个组件都拆成动态 import,结果产生几十个小 chunk。浏览器并发请求数有限(Chrome 同域名 6 个),反而拖慢加载。我的经验是:公共部分抽 common,页面级 chunk 控制在 50-150KB(gzip 后),超过 200KB 就考虑再拆。

实际项目中的坑

资源合并不是配完构建工具就万事大吉,线上环境会冒出各种奇怪问题。分享几个我踩过的坑:

坑 1:CSS 合并顺序错乱

用 Webpack 的 MiniCssExtractPlugin 时,如果多个入口引用了相同 CSS 文件,合并后的顺序可能和预期不符。比如 reset.css 被覆盖了。解决办法是在入口文件里显式控制引入顺序:

// home.js
import '../styles/reset.css'; // 先引入 reset
import '../styles/base.css';
import './home.css'; // 最后引入页面特有样式

坑 2:动态导入路径包含变量

为了懒加载组件,很多人这么写:

// 危险写法!
const page = getUrlParam('page');
import(./pages/${page}.js);

Webpack 会把整个 ./pages 目录打包进去,完全失去按需加载意义。正确做法是枚举所有可能路径:

// 安全写法
const loadPage = (pageName) => {
  switch(pageName) {
    case 'home': return import('./pages/home.js');
    case 'about': return import('./pages/about.js');
    default: throw new Error('Unknown page');
  }
};

坑 3:忽略第三方库的副作用

有些老库(比如某些 jQuery 插件)依赖全局变量,合并后作用域污染会导致报错。这时候得用 ProvidePlugin 显式注入:

// webpack.config.js
plugins: [
  new webpack.ProvidePlugin({
    $: 'jquery',
    jQuery: 'jquery'
  })
]

另外,API 地址千万别硬编码在合并后的文件里!我见过有人把测试环境 URL 写死在 JS 里,上线后疯狂调错接口。正确做法是通过环境变量注入:

// 构建时替换
const API_URL = process.env.NODE_ENV === 'production' 
  ? 'https://jztheme.com/api' 
  : 'http://localhost:3000/api';

最后的小建议

资源合并没有银弹。我的原则是:先保证功能正确,再优化加载性能。有时候为了快速上线,我会先粗略合并(比如只分 vendor 和 main),等监控数据出来再精细化调整。毕竟用户不会为“完美的构建配置”买单,但会因为页面打不开而离开。

对了,记得用 Lighthouse 或 WebPageTest 跑分验证效果。有次我以为合并后变快了,结果发现 TTI 反而升高——因为没处理好关键请求链。工具不会骗人。

以上是我踩坑后的总结,希望对你有帮助。有更好的方案欢迎评论区交流,比如你们怎么处理 SVG 图标合并的?我还在用雪碧图,感觉有点过时了…

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

暂无评论