Web 性能优化全览

2024 年 7 月 3 日 星期三(已编辑)
/ ,
47
1
摘要
web 性能优化的各种思路
这篇文章上次修改于 2025 年 1 月 26 日 星期日,可能部分内容已经不适用,如有疑问可询问作者。

阅读此文章之前,你可能需要首先阅读以下的文章才能更好的理解上下文。

Web 性能优化全览

I also have published The English Version at Medium

从这篇文章你能得到什么 ?

  1. 如何优化网页性能 (优化 TTFB , 优化 FCP, 优化 LCP, 优化 INP, 优化 FID, 优化图片加载)
  2. 如何衡量网页性能 (Clarity, Cloudflare, GA4, GoogleSearchConsole)
  3. 网页性能优化的收益

为什么性能优化很重要

  1. 性能就是留住客户
  2. 性能就是提高转化率
  3. 性能关乎用户体验

Google 从几个角度统计了各项性能指标对业务数据的提升作为佐证:

  1. 较低的 LCP 会有更少的跳出率和更高的转化率
  2. 提高 Core Web Vitals 的表现, 经济时报总体跳出率降低了 43%
  3. BBC 发现, 网站每多加载一秒, 就会多流失 10% 的用户
  4. 沃达丰:LCP 提高 31%,销售额增加 8%客户访问率提高 15%,购物车访问率提高 11%
  5. RedBus 如何改进其网站的 INP,并将销售额提高 7%

性能衡量

不论是开源社区还是商业工具, 都有很多的手段和工具可以帮助我们衡量性能, 这里笔者列举几个自己使用过的, 准确率较高且接入简单的工具

  1. Cloudflare Web Analysis : 这个既支持托管在 Cloudflare 上的网站接入, 也支持 javascript 接入. 接入方式也很简单, 数据维度也比较全面. 同时还支持通过 GraphQL API 查询, 方便接入到自己的面板中, 筛选更多维度的数据.
  2. Sentry: 相比于 Cloudflare 的通用性能数据, Sentry 还包括一些框架层面的细节. 比如 Vue, React 的渲染细节, 路由匹配时间, 资源加载时间, 接口请求时间, 更适合开发者去 Debug 性能缺陷产生的原因.
  3. Clarity: 相比于 Core Web Vitals 的性能数据, Clarity 更注重用户交互和行为的性能数据, 它会记录并回放用户的异常操作. 比如疯狂点击 (Rage Click), 死亡点击 (Dead Click), 过度滚动 (Excessive scrolling), 快速回退 (Quick backs) 等.
  4. Google Search Console: 这是一个所有站长都会使用的工具, 它给出的是 Google SEO 相关的数据, 由于 Google 将 Core Web Vitals 纳入了搜索结果的参考范畴, 所以 GSC 面板有相关的数据. 不过对于国内网站, 这份报告的参考度有限, 因为 GSC 的数据主要来源于 CrUX (Chrome 用户体验报告) , 它是针对有资格的用户从浏览器侧收集的数据. 条件如下:
    1. 启用用量统计信息报告
    2. 同步浏览器历史记录
    3. 未设置同步密码
    4. 使用受支持的平台。

      这意味着国内大部分用户都无法正常上报, 所以 GSC 的性能报告在国内环境参考程度有限.

关键指标

关于关键指标的文章和内容也比较多了, 这里笔者不再赘述, 此处只是简单罗列指标. (更多细节可以参考 Web Vitals)

  • LCP — 衡量加载性能. 最大的内容绘制 Timing, 时间起点为 Navigation 发生.
  • INP — 衡量交互性. 交互发生, 到下次绘制的 Timing, 时间起点为 Interaction 发生.
  • CLS — 衡量视觉稳定性. CLS 表示页面布局变化的累计值

以上指标为 Core Web Vitals 会体现在 Google Tools 中. 还有一些指标同样属于 Web Vitals, 但不属于核心指标, 更多的是开发者需要关心的细节.

  • FCP — 衡量加载性能. 页面首次绘制 Timing, 时间起点为 Navigation 发生.
  • TTFB — 衡量加载性能. 服务器返回 Response 的首字节时间, 时间起点为 Navigation 发生. 不过需要注意的是, 关于在引入 Early Hints 之后, 关于 TTFB 的定义是有些争议的.
  • TBT — 衡量交互性. FCP 发生之后主线程被阻塞足够长的时间以阻止输入响应的总时间, 也就是每个长任务的超时部分 timing 的累加. 不过由于 TBT 是实验室属性, 不属于真实用户的体感反应.

为了更直观的感受到各个指标的含义, 我将会给出一张图片来表示各个指标发生的时间. 需要的注意的是, 图片中关于执行的细节, 只做参考. (下图所有的 script 均指 <script type="module" /> )

web-application-progress

web-application-progress
INP

INP

如何进行性能优化

其实性能优化的思路就是在以上访问链路中, 针对性的减少各个阶段的 Timing. 我将会从两种常见的现代网页运行模式分别讲解优化思路, 以下讨论均在 Vue, React 这两种常见的 Web 框架范畴内.

CSR (Client-side Rendering) 的优化思路

CSR 在生产环境运行时, 往往是由 Nginx 这种 Web Server 实现路由控制, 所有的 path 的请求都会返回相同的 index.html , 具体的 path 与页面展示逻辑由 javascript 在前端实现. SPA 具体的运行逻辑, 本篇文章将不再赘述. 接下来将从 Web Vitals 的各个指标入手, 提供一些优化思路

scr-progress

scr-progress

TTFB 优化

从 CSR 的运行模式我们很容易的知道, CSR 的应用往往会有比较好的 TTFB 表现, 因为 CSR 中 Server 只做简单的文件操作, 同时 index.html 往往只包含 jscss 的索引, 因此 Response Size 也会较小. 所以在 CSR 中, TTFB 指标的优化, 可能并没有很高的 ROI.

FCP 优化

从 CSR 的默认配置来说, index.html 中不包含任何有视觉的代码也就是白屏的, 这对用户加载体验来说是致命的, 用户无法轻易确定此时是否收到了页面, 以至于他可能会再次点击浏览器刷新按钮.

为了提高用户的加载体验, 同时在 SPA 中获得较好的 FCP 表现, 我们可以考虑在 index.html 中直接渲染一些品牌 Landing 的样式, 或者加载中的动画, 就像 x.com 的实现一样.

x.com first screen

x.com first screen

LCP 优化

在 CSR 的 Real-World 应用中, LCP 的提升是很艰难的. 因为 LCP 在 CSR 中有较长的链路, 除了必要的 js 和 css 的加载, 它往往还包括 API Server 的响应时间和 JS 的执行时间.

因此我们可以考虑一些前置的方案, 以下提供一些参考:

  • 针对静态 Path 的页面, 我们可以考虑 SSG (Static Site Generation) 的方案, 比如 https://desktop.telegram.org/ 这个页面就非常适合使用 SSG 的方案.
    1. 考虑 Nuxt.js, Next.js 这类 SSR 框架, 它们本身就支持 SSG 的方案.
    2. 考虑使用 Puppeteer 这类 library, 在构建时就将对应页面的 HTML 快照存为 index.html
  • 针对动态 Path 的页面, 我们需要更加细致的来做讨论
    • LCP 内容依赖 API Server 的响应, 比如 https://x.com/elonmusk 的页面, LCP 是用户设置的 Background Image, 这种我们只能通过缩减页面渲染的成本来优化.
    • LCP 内容是相对固定的内容, 比如品牌的 Logo 或者某部分固定的 HTML 代码. 这类页面, 我们可以参考 FCP 的优化手段, 将这部分内容直接输出到 HTML 中.

一旦我们想针对不同 Path 输出不同的 HTML 时, 我们就需要考虑如何将一个 SPA 项目转换成 MPA 项目, 社区中也有不少实现经验, 此处不再赘述.

那么如果只想真正优化 LCP 我们可以有哪些思路呢 ?

  1. 减少 LCP 渲染前的 js
    1. 比如 main.js , 对于所有 SPA 代码的入口, 一些插件的全局引用可能导致 main.js 过大.
    2. 路由懒加载, 通过对路由组件的动态引入, 可以减少其他页面 js 的加载.
      1. 优化点: 对于其他页面的 javascript, 可以使用 Resource prefetching 做预拉取.
      2. webpack: import(/* webpackPrefetch: true */ './navbar.tsx') // prefetch
      3. React: 中可以通过 React.lazy 来实现, 参考 The ultimate guide to React Lazy Loading 或者使用 react-loadable 来做更多功能拓展.
      4. Vue: 中可以通过 vue-router-prefetch 来实现.
    3. 动态加载
      1. 对于不在关键渲染路径上的 js, 比如首屏不可见的内容区域, 改为动态引入.
      2. 基于 Event 的动态引入, 比如 Click/Hover 才会触发.
    4. Code Splitting
  2. 优化 LCP 依赖的 API 接口
    1. 裁剪 API Response Size 使其只包含必要信息将会有不错的收益.
    2. 将串行请求改为并行请求, 能够有效的减少请求带来的瀑布流.
  3. 减少 js, css 等静态资源拉取的时间
    1. 将资源托管在正确的 CDN 上, 可以极大减少 Response Time.
    2. 开启 Brotli/Gzip 压缩, 通过缩减 Response Size 也可以极大减少 Response Time.
    3. 使用 HTTP2 , 特别是对同一个域名有大量请求时, 一个约 400 字节的 HTTP/1.1 请求有可能被压缩成一个约 10 字节的 HTTP/2 请求.
    4. 在 HTML <head> 中将异步的 js, css 标记为 preload, 比如 preload-webpack-plugin 将会有效减少 Resource Fetch 的瀑布流 (默认情况下其它的 js/css 都将在 main.js 执行后拉取)
  4. 优化图片资源的加载: 很多情况下, LCP 的内容都是图片, 所以图片资源的加载尤为关键
    1. 使用响应式图片, 为不同的屏幕大小提供不同尺寸和清晰的图片.
    2. 使用现代化的图片格式比如: Webp
    3. 使用一些图片优化产品, 比如 Cloudflare Polish

TBT 优化

在 React 18 中, React 引入了新的 Concurrent Renderer 通过可打断的更新方式, 让 React 能每 5ms 返回一次主线程, 来保证页面的可交互性. 参考 https://vercel.com/blog/how-react-18-improves-application-performance

而在 Vue 中, 通过对 templates 的分析, 解决了虚拟 DOM 调节的基本开销. 通过一系列优化实现了智能组件树级优化, 这意味着对于同一更新,在 React 应用程序中可能会导致多个组件重新渲染,但在 Vue 中很可能只导致一个组件重新渲染. 由此, Vue 中的更新本身就会更加轻量. Why remove time slicing from vue3?

顺便提一句, 当我们做大列表渲染的时候, 虚拟列表会是一个不错的选项.

Just remember that 'scheduling' doesn't let you break the laws of physics. The best way to get better performance: do less work. FROM Rich Harris' X

SSR (Server-side Rendering) 的优化思路

在 SSR Web Application 中, 由于服务端返回的 html 中包含有内容,那么优化思路和方向相比于 CSR 又会有所差异。

ssr-application-process.drawio.png

ssr-application-process.drawio.png

从上图我们可以发现,在 SSR 中,TTFB ,FCP 和 LCP 往往相继发生,中间的 Timing 往往只有较短的时间,这一点和 CSR 完全不同。同时这也意味着,一旦 TTFB 有所提升,则 FCP, LCP 也会随之提升。

优化 TTFB

TTFB 的优化可以从几个角度看

  1. 服务端尽快返回
    1. 减少服务端计算 (do less work)
    2. 使用全文缓存
    3. 流式渲染
  2. 减少网络耗时
    1. 将服务器前移, 比如 CF worker
    2. 减少响应体积, 比如前面提到的 Brotli/Gzip
    3. CDN (Content Delivery Network)

就我的经验来看, 优化 TTFB 最直接有效的手段是 CDN ,通过 CDN 能轻松达到以下几个目的:

  1. 减少网络耗时,请求可以在就近的 CDN 节点返回,而无需经过层层转发到源站。
  2. 减少服务端返回的时间,CDN 等于一层 Cache,大量请求将在命中缓存后直接返回,服务端压力变小。
  3. 对于流量变化较大的业务场景, 比如各种突发流量, CDN 都可以轻松应对。相反不论如何优化服务端计算, 在 QPS 较高的场景终究会带来一定程度的延迟.

在实际业务中, 如果是因为服务端计算时依赖客户端状态, 比如用户登录态, 千人千面算法等原因无法使用 CDN 的情况. 应当对业务逻辑进行改造, 尽可能允许服务端输出无状态的内容, 而将状态相关的逻辑放在客户端计算. 如此一来, 业务就具备上 CDN 的能力 (其实是上缓存的能力).

幸运的是, 不论是 NextJs 还是 NuxtJs 同构的代码, 让我们不用付出太大的代价就可以将计算逻辑从服务端迁移到客户端, 这也是现代 SSR 的优势所在. (相比 PHP, JSP 等传统 SSR 模式)

不过我们这里讨论可以尽量单纯一点, 在不考虑 CDN 和全文缓存的情况下, 我们能做什么来优化 TTFB ?

  1. 减少不必要或者开销过高的计算 (do less work)
  2. 避免在服务端的串行请求, 减少后链路的依赖, 这一点在 CSR 项目改造成 SSR 项目时会非常常见.
  3. 合理切割渲染逻辑, 减少服务端计算. 对于不同屏幕尺寸的设备, 渲染不同的内容.
    1. 利用  Sec-CH-Viewport-Width,  Sec-CH-Viewport-Height 等请求头, 决定应该如何渲染
    2. 利用 UA 判断设备类型, 决定如何渲染
  4. 减少 HTML 体积: 对 SEO 无价值, 且首屏不可见的内容, 迁移至客户端渲染.
  5. 部分缓存响应中 HTML 的 <head> 部分, 利用 HTTP 的分块传输 , 优先返回 HTML 的 <head> 部分, 这部分内容实时性要求不高, 同时能提高 FCP 指标, 获得更好的用户体验.
  6. 还有前面提到的 Early Hints

总之一切可以减少 Response Time 的手段, 都可以减少 TTFB.

优化 FCP

在 SSR 的应用中, 一般情况下 FCP 和 LCP 的时间间隔会非常短. 在实际应用中, FCP 受控于 TTFB 的时间和 HTML 的书写顺序. 所以优化 TTFB 我们可以从以下几个角度入手:

  1. 优化 TTFB
  2. 减少 pending rendering 的部分
    1. 只将首屏关键 css 内联到 html 中, 并且放在 <head> 中. (放在 <head> 的原因是, 避免 css 加载后造成的页面抖动, 影响 CLS) 事实上在很多框架中, 都会将大量的 css 通过 link 的方式引入到 <head> 中, 这会造成渲染延迟.
    2. 将 js 通过不阻塞渲染的方式引入到 html 里, 比如 <script type="module|async|defer" /> 或者将 <script> 标签放在 <bodu> 之后.
    3. 按照正确的顺序书写 <html> , 页面内容顺序应当和 HTML 元素顺序一致
  3. HTTP 2.0 , 在允许的情况下使用 HTTP 2.0 对于静态资源的获取非常有利.
  4. 通过 Early Hints 这类新的 HTTP 标准, 提前拉取对应静态资源.

优化 LCP

在 SSR 的应用中, 如果 FCP 是良好的那么往往就会有比较好的 LCP 指标. 在 LCP 元素是图片时, LCP ≈ FCP + image download timing.

所以我们可以参考 CSR 的优化手段优化图片资源的加载

  1. 使用响应式图片, 为不同的屏幕大小提供不同尺寸和清晰的图片.
  2. 使用现代化的图片格式比如: Webp
  3. 使用一些图片优化产品, 比如 Cloudflare Polish

优化 FID / INP

在 Nextjs/Nuxtjs 等框架中, 服务端渲染的页面在到达浏览器之后, 距离可交互还有一小段 Timing, 这段 Timing 对应的动作是 Hydration , 用来将 js 事件挂在到 dom 上的.

为了减少用户输入的延迟, 我们可以考虑将部分按钮的交互放在框架之外或者使用原生的 HTML 标签实现某些功能. 比如通过<dialog> + <a> 就可以实现一个简单的登录交互, 并且完全不需要框架的参与.

总结

至此本篇文章内容完全结束, 本篇内容梳理了两种不同形式的 Web 应用的优化手段, 同时列举了一些通用的指标和分析工具. 如果你也在从事相关内容的工作, 那么希望本篇内容能给你带来一些帮助.

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...