Lex's Blog

前端性能优化

TL;DR

  • 缓存:空间利用
  • 并行化:时间利用
  • 压缩:缩短传输时间

从前端应用的生命周期梳理:构建 -> 资源请求/加载 -> 渲染 -> 运行

构建阶段

「构建」这个行为本身的提速

缓存

各种链路的缓存

  • loader 缓存
  • webpack 缓存
  • yarn(node_modules)缓存
  • nx monorepo 缓存
  • ...

并行化

  • thread-loader:多线程构建

专项

  • 更快的工具链:swc-loader、esbuild
  • 缩小文件搜索范围
  • 合理使用 Source Map
module.exports = {
  resolve: {
    // 明确指定模块目录
    modules: ['node_modules'],
    // 减少后缀尝试
    extensions: ['.js', '.jsx'],
    // 使用绝对路径
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        // 排除不需要处理的目录
        exclude: /node_modules/,
        include: path.resolve(__dirname, 'src')
      }
    ]
  },
  devtool: process.env.NODE_ENV === 'production' 
    ? 'cheap-module-source-map' 
    : 'eval-cheap-module-source-map'
}

资源加载

目的:更快的资源下载速度

从用户输入域名开始:

网络优化

DNS 解析优化

将主要用到的资源域名进行 DNS 预解析,减少 DNS 查询时间

  • <link rel="dns-prefetch" href="//example.com">

CDN

通过 CDN 网络,请求离用户最近的服务器,缩短资源传输距离,提升响应速度

并行化

  • HTTP 1 协议:多域名拆分,提升并发请求数(chrome 中同一域名同时只能有 6 个请求链接)
    • 请求合并:CSS 精灵图
    • splitChunk 策略应该更少的拆分文件
  • HTTP 2/3 协议,支持多路复用和更快的传输
    • splitChunk 策略可以更多的拆分文件
    • script defer/async:并行 JS 脚本加载

加载时机

Pre Load

资源预加载(preload)和预获取(prefetch)提升后续加载速度

  • link 标签的属性:pre-load/pre-fetch 实现浏览器级别的缓存
    • import(/* webpackPrefetch: true */ './path/to/Component')
    • 不仅可以预取静态资源,也可以预取接口(as="fetch"
  • 路由/状态管理框架:JS 内存级别的数据缓存
  • 客户端离线包

Lazy Load

资源懒加载(图片、组件等)减少首屏加载压力,调整请求优先级

资源大小

Tree Shaking

移除没有使用到的代码

  • JS:webpack 生产模式自动启用
  • CSS:purgecss-webpack-plugin

Tree Shaking 依赖于 ES6 的模块解析功能,原因在于 ES6 的模块解析是静态的,具备编译时确定性

  1. 导入/导出语句必须位于模块顶层(不可嵌套在条件语句中)
  2. 所有导入路径必须是字符串字面量(不能是变量)

压缩

  • JS:terser、esbuild
  • CSS:CssMinimizerWebpackPlugin
  • 图片:image-webpack-loader
  • Gzip:compression-webpack-plugin

缓存

HTTP 缓存

  • 协商缓存
    • SPA 应用中将 index.html 做为协商缓存(no-cache)
    • 大数据量或不常变动的接口
      • 文章内容、用户信息
  • 强制缓存
    • JS、CSS、字体、图片等静态资源通过强制缓存长期(一年)缓存在浏览器本地
    • 通过构建时的 hash 控制缓存失效(文件名变化,cache miss)

JS

  • 通过(localStorage、IndexedDB)等手段对接口进行缓存,加快请求速度
    • 通过 SWR 的思想对接口数据进行更新
  • 使用 Service Worker 技术实现更灵活的缓存控制

构建相关

Hash

无脑用 content hash 即可


强制缓存中 hash 的选择很关键,它决定了文件变化后,浏览器缓存的命中率

  • hash:基于项目所有构建产物计算 hash 值
  • chunk hash:基于 chunk 中的所有构建产物计算 hash
  • content hash:基于每个构建产物计算 hash

hash 自不必说,正常是不会使用它的


重点说下 chunk 和 content 的区别,无论选择哪一种,webpack 的构建产物总是以 chunk 进行划分的,一个 chunk 中可能包含多个 JS、CSS、图片等其他资源

重点在于,当一个 chunk 中包含多种类型资源时,不同的资源类型是可以(通过 loader)被单独提取为独立的文件产物的

在这种情况下,如果使用 chunk hash,就会发生修改了 css 文件,结果 js 文件的 hash 也发生了变化的情况

而使用 content hash,不同独立产物计算 hash 时是根据自己的产物内容进行计算的,就可以避免这种情况


再简单的点说,如果一个 chunk 构建出的产物只有一种资源类型(比如 JS),那我们使用 chunk 和 content 是没有区别的

只有当一个 chunk 中有多种资源类型的产物并且产物被拆分为单独文件时,两者的区分才有意义

  • chunk hash
    • main.hash(JS,CSS).js
    • main.hash(JS,CSS).css
  • content hash
    • main.hash(JS).js
    • main.hash(CSS).css
SplitChunk

由上可知,即使选择了 content hash,同一类型文件的缓存命中依然是由 chunk 划分决定的

好在目前的框架实践中都会对路由进行懒加载处理,这种情况下默认的 SplitChunk 配置就会根据不同页面(路由)进行 chunk 划分,多数情况下是可以满足预期的

而当默认配置不满足预期时,我们应该使用插件进行观测和测量,再根据项目情况进行配置(就是说这个并没有通用的完美配置方案)

  • webpack-bundle-analyzer
  • speed-measure-webpack-plugin
稳定性 ID

在默认情况下,Webpack 会为每个模块分配数字 ID(0, 1, 2...)

这种 ID 分配方式会导致新增/删除模块时,其余模块 ID 也会改变

从而使构建产物的文件名变化,导致缓存的命中失效

// 初始构建
0.index.js
1.utils.js

// 添加新模块后
0.new.js  ← ID序列被打乱
1.index.js
2.utils.js

可以通过 deterministic 的配置,是 webpack 改变这一行为

使用模块的路径 hash 作为 ID 名,从而避免这种情况

module.exports = {
  optimization: {
    moduleIds: 'deterministic',  // 保持模块ID稳定
  }
}
Runtime 代码分离

Webpack 的 runtime 代码默认内嵌在每个入口 chunk 中,其中包含:

  • 一小段管理模块加载/缓存的代码
  • 包含 chunk 和模块之间的依赖关系图

这会导致:

  1. 项目中任何模块变化(依赖关系)都会导致 runtime 内容变化
  2. 进而导致所有入口 chunk 的 hash 变化,缓存命中失效

通过 runtimeChunk 的配置,可以将 runtime 单独提取为独立的文件,避免对其他文件产生影响

  • single:为所有 chunk 生成一个共享的运行时文件
  • multiple:为每个入口 chunk 单独生成一个运行时文件
module.exports = {
  optimization: {
    runtimeChunk: 'single'       // 提取 runtime 到单独文件
  }
}

渲染

  • 减少重排(Reflow)和重绘(Repaint)
    • 尽量使用 position:absolute、transform 等属性进行动画变换,避免动画元素影响页面中的其他元素(重排)
    • 避免频繁修改 DOM 树,合并多次 DOM 操作为一次性批量更新
    • 使用文档片段(DocumentFragment)或离线节点,减少页面渲染次数
    • 尽量避免逐条样式修改,推荐使用 class 切换
  • 避免阻塞渲染
    • 将 JS 脚本放在底部或使用 deferasync 属性
    • 将非关键 CSS 异步加载,使用 media 属性或 preload
  • 异步渲染与分片渲染
    • 利用 requestAnimationFrame 优化动画和大批量 DOM 更新
    • 对长列表、复杂节点采用虚拟滚动、懒加载或分片渲染
  • 优化 CSS 选择器
    • 避免使用低效的选择器(如通配符、后代选择器),优先使用类选择器
    • 精简样式层级,减少浏览器样式匹配计算量

运行

  • 减少主线程压力
    • 复杂计算、数据处理放到 Web Worker,避免阻塞 UI 渲染
    • 使用节流(throttle)、防抖(debounce)控制高频事件(如 scroll、resize)
  • 优化 JavaScript 执行效率
    • 避免过度嵌套循环和递归,优化算法复杂度
    • 减少全局变量和作用域链层级
  • 减少内存泄漏与优化内存使用
    • 及时移除事件监听、定时器、未使用的 DOM 节点
    • 避免闭包滥用,防止对象被意外引用导致无法释放
  • React/Vue 框架层优化
    • 遵循相应框架中的最佳实践:比如 React 中的 memo、useMemo、useTransition、diff key 等

性能分析/监控

核心网络指标

按顺序:

对于性能分析,可以通过 chrome lighthouse 对页面进行分析(打开开发者工具即可)

对于性能监控,可以通过开源库(web-vitals 等),或者通过 performance API 自行获取各阶段时间,然后通过 API 进行上报分析

REF

觉得有帮助?给个 Star 支持一下吧!

Star on GitHub

On this page