前端性能优化——针对打包和启动速度优化经验

77 阅读11分钟

在入职的时候项目前端框架十分老旧

  • 主要使用的还是vue3.2+vuecli4+webpack4+js的组合
  • 项目本地启动速度在32000ms以上
  • jenkins构建时间在3-5分钟以上,
  • 项目的DOMContentLoaded和Load速度都在3s以上
  • 首屏加载文件更是离谱得堆叠在一起,全部放在chunk-vendors,没有做任何分片操作,导致chunk-vendors文件有12MB,本地启动加载该文件耗时1.5s左右(受网络波动影响)
  • chrome的performance评分在28
  • 打包的总体积在17M左右

虽然只是一个iframe搭建的微前端项目,但是项目内冗余代码过多,经常出现一个vue文件长达4000多行,并且项目虽然是vue3搭建的,但是代码大部分都是由vue2编写,小部分混着vue3,导致无论是维护还是重构都十分困难

先看看优化之后的结果:

  • 主要使用的还是vue3.3+vuecli4+webpack5js的组合
  • 项目本地启动速度在10000ms左右
  • jenkins构建时间在2-3分钟左右
  • 项目的DOMContentLoaded和Load速度都缩短到了1.5s左右
  • 首屏加载文件chunk-vendors文件下降到3.3M,本地启动加载该文件耗时减小到700ms左右(受网络波动影响)
  • chrome的performance评分在40
  • 打包的总体积在14M左右

工具:

  • webpack-bundle-analyzer 包 用于观察打包热力图
  • chrome-network最直观看到DOMContentLoaded和Load速度
  • chrome-performance 观察整个流程所涉及的执行文件以及耗时长度
  • chrome-lighthouse 用于优化分析
方案一: 将webpack4替换为vite (放弃)

综合考虑还是放弃了,后面遇到了一个可以自己全权设计新起的微前端项目,那个项目我直接上的vue3.6+vite6+pinia+ts,在没有针对性执行优化,单纯实践业务的场景下:

  • 启动耗时只有600-800ms
  • jenkins构建20-40s(主要是项目小+都是es模块,天生比webpack打包体积小20%)
  • 项目的DOMContentLoaded和Load速度都在200ms
  • chrome的performance评分在55
方案二: webpack4升级到webpack5, vue3.2升级到vue3.3 (应用)

主要还是起一个铺垫作用,现在主流比较新的库都需要webpack5 + vue3.3,而这个升级操作的难度远低于将vuecli4+webpack4替换成vite6,而且webpack5本身对比webpack4也具备显著的优点

以下webpack5的优点复制于豆包;

  1. 显著的构建性能提升
  • 持久化缓存(Persistent Caching) :支持将构建过程中的中间结果(如模块解析、编译结果)缓存到硬盘,第二次构建时可直接复用缓存,大幅减少重复计算,尤其在大型项目中,二次构建速度提升 50%+
  • 并发处理优化:对 loader 执行、代码生成等步骤的并发调度进行了优化,充分利用多核 CPU 资源,缩短构建耗时

实践结果的确在二次构建的时候,项目本地启动速度大幅下降,大概下降到14000ms

  • 更高效的模块解析:优化了模块查找算法,减少不必要的文件系统访问,并通过缓存模块依赖关系进一步提升解析速度。

实践上没太深的优化感受

  1. 长期缓存能力增强

长期缓存的核心是:文件内容不变时,输出文件名的哈希值(hash)不变,从而让浏览器 / CDN 长期缓存资源,减少重复下载。

Webpack 5 通过以下方式优化长期缓存:

  • 稳定的 chunkId 和 moduleId:默认使用deterministic算法生成chunkId和moduleId,替代 Webpack 4 中不稳定的数字 ID。即使新增 / 删除模块,其他模块的 ID 也不会变化,避免哈希值无效化。
  • 更精确的 contenthash:contenthash仅基于文件内容计算,确保内容不变则哈希不变。例如,CSS 抽离后的文件哈希,不会因 JS 代码变化而改变。

实践上感受不深,可能是没有遇到相关的问题case

  1. 内置模块联邦

这是 Webpack 5 最具革命性的特性,允许不同 Webpack 构建(如多个应用)之间共享模块,无需将代码打包到同一个 bundle 中。

  • 适用场景:微前端架构(如多个子应用共享组件库)、跨应用代码复用(如共享工具函数)。
  • 优势:避免代码重复打包,减少整体体积;实现应用间的 “动态依赖”(A 应用可实时使用 B 应用的最新模块),简化多团队协作。
  1. 增强的 Tree Shaking

Tree Shaking 用于移除未使用的代码(dead code),减小 bundle 体积。Webpack 5 在此方面做了深度优化:

  • 对 ES 模块的严格处理:更精准地识别未引用的导出,并支持对嵌套模块的摇树。
  • 支持 CommonJS 模块的摇树:通过 terser-webpack-plugin 配合,可对部分 CommonJS 模块进行静态分析,移除未使用的代码

webpack4升级到webpack5最开始是为了引入tailwindcss,所以没有对比webpack4的时候的打包体积,但体感这块儿webpack的确对于打包文件做了一定性的treeshaking和压缩

  1. 内置静态资源处理

Webpack 5 内置了对图片、字体、二进制文件等静态资源的处理能力,无需再依赖 url-loaderfile-loader,简化配置:

  • 支持资源的缓存哈希(如 image.[contenthash].png),配合长期缓存使用。

实践的确无需再引入file-loader,url-loader等静态资源处理器

  1. 移除 Node.js 内置模块 polyfill

Webpack 4 及之前版本会自动为浏览器注入 Node.js 内置模块(如 fspath)的 polyfill,导致 bundle 中包含大量无用代码。

Webpack 5 彻底移除了默认 polyfill,仅在代码显式引用时才需手动添加,显著减小非必要代码体积

实践生效,但是可惜没有记录下来webpack4时候的打包体积

  1. 更好的开发体验
  • 更清晰的错误提示:错误信息中包含更具体的模块路径、依赖关系,甚至修复建议,降低调试成本。

这点比vite好用,也可能是我漏配了

  • 热模块替换(HMR)优化:HMR 更新速度更快,尤其在大型项目中,模块变更后能更精准地局部更新,避免全量刷新。
  1. 对现代技术的更好支持
  • 原生支持 WebAssembly(Wasm) :可直接将 Wasm 模块作为入口或依赖,简化高性能二进制代码的集成。

目前项目没接触到,希望未来能接触

  • 支持 ES 模块动态导入(import())的优化:对 import() 分割的 chunk 进行更智能的命名和依赖管理,提升代码分割效率。

实际最重要的优点,现在大部分的技术最低都要webpack5

方案三: 引入tailwindcss (应用)

优点:

  • 代码简洁,css编码速度快,支持快速开发
  • 原子化实现,使得css共用一个类,体积减小
  • 可以项目迁移,一键复制配置文件

不过还是得搭配less或者sass,因为tailwindcss的优先级有点低,对于需要修改外部组件样式的场景无法覆盖对应css代码

方案四: 减少console.log等无用信息的打包 (应用)

最开始是因为有个bug只在dev环境上出现,在本地没办法复现,所以打了很多console.log在项目中,然后上了生产环境,但是其实本质上是不希望这些信息暴露给用户的,而且打包也占了100KB左右的体积,所以开启了webpack的optimization的terser配置,为了防止危险把debugger也一起禁止打包进生产

方案五: webpack分块打包 (应用)

分块的思路主要是观察bundle打包热力图,因为我们项目使用的是http1.1,所以没办法把分块分得太多,因为http1.1一次最多加载6个文件,最好就是把chunk-vendor的拆分维持在5-8个左右,主要将非首屏加载的比较大体积的第三方库拆出来。

这个需要根据实际项目拆,我最后拆出来了五个,然后发现基本还是会在首屏加载出来。

方案六: 取消全局组件,实现按需引用 (应用)

最开始怀疑是首屏文件引用该第三方库,按照这个思路去找到该第三方库所牵扯的文件,采用按需引用或者异步加载,最后五个首屏加载js文件只剩下三个。

观察剩下三个代码,最后发现是相关应用组件被注册为global组件,导致整个库会在项目启动时候被加载,于是一个个把全局组件拆分为按需引用。

按照这个按需引用的思路,继续库的体积都减下来

最后打算把global.js文件全部干掉,要是因为观察chrome的performance的项目执行耗时之后,发现global.js的代码执行耗时是可以缩减的,(而且也是整个app.js执行耗时里面最容易缩减达到成效的文件)

同时对于一些库可能会应用一些不必要的文件,比如有个库引用了国际化的locale文件,这个文件是做多语言n18适配的库,我们项目目前短期内完全用不到多语言的配置,对此在webpack配置将这个库里面的这个文件不要打包进来就行

方案七:轻量库替换功能繁重的库

评估了代码库后发现项目使用的lodash和momentjs库整体功能虽然齐全但是体积过大,很繁重,实际上我们项目只用到了最基本的功能,既然如此本身没必要使用这种大型库,所以将其替换成了lodash-es和momentjs

如果有些第三方之间有牵连,但是评估后类似lodash-es和lodash这种可以直接替换的,如果因为第三方库的原因没办法把aaa库去除掉,可以将alias配置,手动实现库的重定向

方案八:文件压缩
  • css压缩:
  1. 因为我们项目之前默认用的sass,即使后面引入tailwindcss但是实际上并没有太多的人使用,而webpack5是会对于sass文件自己进行压缩,我是直接上了CssMinimizerPlugin,但是实际测试因为webpack5自己压缩的问题,并没有太大成效。
  2. taiwindcss在一定程度上也是符合这个问题
  • 图片压缩:
  1. 本来打算采用图片压缩库,但是发现有个文件被墙了,但是我们项目不能使用淘宝镜像,公司镜像没有处理这块的问题,于是最后我采用了最简单粗暴的办法……直接自己手动压缩然后放进项目里面……
方案九:懒加载
  • 开启图片懒加载,和IntersectionObserver结合,因为我们表格直接上分页了,不涉及长表格的场景,不然表格也一个法子直接懒加载就行
  • 组件懒加载,一般项目都会覆盖
方案十:整理package.json的devdependency和dependency的依赖

很多项目因为是多人合作的关系,很多时候会出现把不需要出现在dependency的库安装在dependency,导致最后打包的体积掺和进了只在开发/构建时候需要的库,因此我对我们的项目整理,将大约打包后1.1M体积的库从dependency移到了devdependency

方案十一:打包生成的output文件使用contentHash值

是前端优化缓存,提升部署效率的核心手段

  1. 能利用浏览器缓存,减少代码的重复下载,提升页面的加载速度
  2. 具备更优的版本监控策略,最小化部署成本
 configureWebpack: {
    output: {
      filename: `js/[name].[contenthash:8].js`,
      chunkFilename: `js/[name].[contenthash:8].js`
    }
  }
一些额外的杂谈
  • 很多场景存在卡顿感,主要来源于接口耗时,很难改动,只能优化体验感
  1. 骨架屏
  2. loading提示
  3. 或者特定场景下可以预加载

至于其他的cdn分配资源,方便快速加载,因为我们项目比较小,无法涉及到这种场景应用,所以没办法亲身尝试,个人还是很希望后续有个实践机会的