【性能优化】如何系统性地优化页面性能?

lxf2023-03-14 13:52:01

前言

之前也整理过一些具体的优化手段,但后来发现AdminJS上有很多优秀的、关于性能优化的高赞文章,写的比我还详细,所以这里就不班门弄斧了。PS:本文侧重分享一些 如何系统性地优化页面性能 的思路/方向。

从输入 URL 到页面展示,这中间发生了什么?

一个老生常谈的问题,相信大多数人面试的时候都会被问到。在了解如何性能优化前,我们必须要熟悉页面渲染的完整流程,才能知道可以从哪些方向着手做性能优化。有兴趣的,可以学习下 李兵 大佬的 浏览器工作原理与实践_浏览器_V8原理,拜读了很多遍,收获很多,本文部分内容更是直接引用了该专栏中的,没办法,人家讲的实在太好了。

一个 HTTP 请求的完整流程

【性能优化】如何系统性地优化页面性能?

从图中可以看到,浏览器中的 HTTP 请求从发起到结束一共经历了如下八个阶段:构建请求、查找缓存、准备 IP 和端口、等待 TCP 队列、建立 TCP 连接、发起 HTTP 请求、服务器处理请求、服务器返回请求和断开连接

从输入 URL 到页面展示的完整流程

【性能优化】如何系统性地优化页面性能?

哪些因素会影响到性能?

通常一个页面有三个阶段:加载阶段、交互阶段、关闭阶段

  • 加载阶段:是指从发出请求到渲染出完整页面的过程,影响到这个阶段的主要因素网络和 JavaScript 脚本
  • 交互阶段:主要是从页面加载完成到用户交互的整合过程,影响到这个阶段的主要因素JavaScript 脚本
  • 关闭阶段:主要是用户发出关闭指令后页面所做的一些清理操作。

我们只需要重点关注加载阶段和交互阶段,因为影响到我们体验的因素主要都在这两个阶段。

DNS

参考:一张图看懂DNS域名解析全过程

在网络中,一个文件通常会被拆分为很多个数据包来进行传输。数据包要在互联网上进行传输,就要符合 网际协议(Internet Protocol,简称 IP) 标准

什么是 IP 协议?互联网上不同的在线设备都有唯一的地址,地址只是一个数字,这和大部分家庭收件地址类似,你只需要知道一个家庭的具体地址,就可以往这个地址发送包裹,这样物流系统就能把物品送到目的地。计算机的地址就称为 IP 地址,访问任何网站实际上只是你的计算机向另外一台计算机请求信息。

【性能优化】如何系统性地优化页面性能?

DNS 的解析时长会直接影响页面的渲染速度

常见的优化思路:使用 DNS 预解析(dns-prefetch)、CDN 加速域名等。

HTTP

HTTP 缓存与否的区别

【性能优化】如何系统性地优化页面性能?

由上图可见,一旦使用了缓存后,资源请求的速度会非常快,从而加快页面的渲染速度。

HTTP 1.1 的好与坏

好:HTTP/1.1 为网络效率做了大量的优化,其中一个特性就是浏览器为每个域名最多同时维护 6 个 TCP 持久连接。基于这个特性,我们可以让 1 个站点下面的资源放在多个域名下面,比如放到 3 个域名下面,这样就可以同时支持 18 个 TCP 连接了,大大减轻了整个资源的下载时间,这种方案称为域名分片技术。

坏:虽然 HTTP/1.1 采取了很多优化资源加载速度的策略,也取得了一定的效果,但是 HTTP/1.1对带宽的利用率却并不理想,这也是 HTTP/1.1 的一个核心问题。其中一个原因就是同时开启了多条 TCP 连接,会导致这些连接竞争固定的带宽。

你可以想象一下,系统同时建立了多条 TCP 连接,当带宽充足时,每条连接发送或者接收速度会慢慢向上增加(慢启动:和一辆车的启动过程类似);而一旦带宽不足时,这些 TCP 连接又会减慢发送或者接收的速度。比如一个页面有 200 个文件,使用了 3 个 CDN,那么加载该网页的时候就需要建立 6 * 3,也就是 18 个 TCP 连接来下载资源;在下载过程中,当发现带宽不足的时候,各个 TCP 连接就需要动态减慢接收数据的速度。

这样就会出现一个问题,因为有的 TCP 连接下载的是一些关键资源(会阻塞网页首次渲染的资源),如 CSS 文件、JavaScript 文件等,而有的 TCP 连接下载的是图片、视频等普通的资源文件,但是多条 TCP 连接之间又不能协商让哪些关键资源优先下载,这样就有可能影响那些关键资源的下载速度了。

以下就是某网站使用 HTTP 1.1 的例子,所有请求的资源文件都是在同一个域名下,所以导致后面的请求需要等前面的请求结束后才能开始介入,同一时间只支持少量的 TCP 连接。
【性能优化】如何系统性地优化页面性能?

以下是AdminJS使用 HTTP 2.0 的例子,可以看到很多 HTTP 请求的时间线是比较贴近的。
【性能优化】如何系统性地优化页面性能?

看起来 HTTP/2 似乎可以完美取代 HTTP/1 了,不过 HTTP/2 依然存在一些缺陷,于是就有了 HTTP/3,具体可以参考 HTTP/3:甩掉TCP、TLS 的包袱,构建高效网络,这里就不过多展开了。

关键资源

什么是关键资源?

一个页面至少会包含 HTML、JavaScript、CSS 这些资源文件,额外的可能会有图片、音频、视频等其他资源文件。并非所有的资源都会阻塞页面的首次绘制,比如图片、音频、视频等文件就不会阻塞页面的首次渲染;而 JavaScript、首次请求的 HTML 资源文件、CSS 文件是会阻塞首次渲染的,因为在构建 DOM 的过程中需要 HTML 和 JavaScript 文件,在构造渲染树的过程中需要用到 CSS 文件。我们把这些会阻塞网页首次渲染的资源称为关键资源。

什么是 DOM 树?

为什么要构建 DOM 树呢?这是因为浏览器无法直接理解和使用 HTML,所以需要将 HTML 转换为浏览器能够理解的结构——DOM 树。
【性能优化】如何系统性地优化页面性能?

什么是 CSSOM?

HTML 加载 CSS 的三种方式
【性能优化】如何系统性地优化页面性能?

styleSheets
和 HTML 文件一样,浏览器也是无法直接理解这些纯文本的 CSS 样式,所以当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets。
【性能优化】如何系统性地优化页面性能?

计算出 DOM 树中每个节点的具体样式
【性能优化】如何系统性地优化页面性能?

什么是布局树?

最终 DOM 树和 CSSOM 会结合生成一棵布局树,渲染进程会根据这棵树去绘制页面。

【性能优化】如何系统性地优化页面性能?

页面渲染流水线

结合下面的代码,来看看对应的渲染流水线是什么样的:

// demo.css
div{ 
    color : red;
}
...
// demo.html
<html>
<head>
    <link href="demo.css" rel="stylesheet">
</head>
<body>
  <div>111</div>
  <script src='https://cdn.xxx.demo.js'></script>
  <div>222</div>
</body>
</html>

【性能优化】如何系统性地优化页面性能?

从图中可以看到,JS/CSS 的加载&解析都会阻塞页面渲染。上图只是最简单的页面渲染流水线,不同的 HTML 内容,页面渲染流水线也不一样,在项目中实际情况更复杂。

页面渲染过程中为什么会出现页面卡顿?

什么是 FPS?

FPS(Frames Per Second):表示每秒钟渲染多少帧画面。对于浏览器来说,通常 FPS 为 60 帧。

浏览器每一帧需要执行哪些任务?

【性能优化】如何系统性地优化页面性能? 从图中可以看到,浏览器每一帧需要执行很多任务,只要其中一个任务运行时间过长,就会出现“掉帧”的情况,导致页面出现卡顿。

常见的优化思路:使用 rAF(requestAnimationFrame) 代替 setTimeout/setInterval 运行 JS 逻辑、减少会引起浏览器不断布局/绘制的操作、使用 rIC(requestIdleCallback) 在空闲时段执行任务、节流、防抖等等。

如何系统性地优化?

【性能优化】如何系统性地优化页面性能?

性能优化

针对上述部分的名词解释:

  • 前端业务改造:

    • 问题代码优化:找出&优化影响性能的代码(PS:AdminJS上有很多关于这方面的优秀文章,这里就不展开来说了);
    • 计算逻辑下沉:如果因为历史原因,你的项目代码里存在一些明明可以交给后端处理的复杂计算逻辑,但又交给前端去处理了,导致页面出现较明显的性能问题。这个时候,就可以和后端协商,把计算逻辑下沉到后端(Node/Java),通过接口返回最终计算完的数据。
    • 梳理&拆分 首屏/次屏 业务:

    以淘宝的商品详情页为例:一个商品详情页,至少会包含商品主图、商品描述、营销活动、SKU 面板、底部悬浮按钮组、发货信息、商品评价、店铺推荐、商品详情、价格说明等模块,从头划到尾至少会占据好几个手机屏幕。这些模块如果在页面初始化时,同时去请求数据 + 数据处理 + 渲染 UI,等页面完全可用时(用户可点击交互),会消耗非常多的时间,用户体验差到极致。

    所以这个时候,就需要拆分 首屏/次屏 业务,把那些需要在页面初始化时,立即展示在首屏中的模块拆分出来,优先加载&渲染首屏模块(如:商品主图、商品描述、营销活动、SKU 面板、底部悬浮按钮组、发货信息),延迟加载次屏模块(如:商品评价、店铺推荐、商品详情、价格说明等)。
    【性能优化】如何系统性地优化页面性能?

  • 前端基础能力升级

    • 加载性能治理: 大部分是针对页面初始化加载时的性能优化手段;
      • 资源优化: 比如选择合适的图片格式(如:webp)& 适当压缩图片的体积;
      • NPM 包体积治理(二方): 如果项目中使用了公司内部自研的 NPM 包,可以查看该包 build 后的体积,是否有优化的空间。
      • 静态数据缓存:如果是一些常态化的数据(很长一段时间内不会变更的数据,如:商家店铺配置信息、用户基本信息等),可以考虑缓存到本地/云上;
    • 运行性能治理: 如果项目中使用了公司内部自研的框架/NPM 包,可以分析该框架/NPM 包运行时的性能,如果存在明显的性能问题,那就优化它;
  • 运维基础能力升级: 对运维相关的知识不熟,不敢妄言,所以只罗列了一些常见的优化手段;

  • 服务端接口响应优化:

    • 梳理&拆分首屏/次屏业务 + 减少 Node 聚合: 如果你的团队使用 Node 搭建 BFF 架构(即 Backends For Frontends,服务于前端的后端)。对于一个复杂的页面,在页面初始化加载时,通常会调用一些核心 Node 接口,这些 Node 接口内部会聚合很多个 Dubbo 接口同步调用,以此来获取页面渲染需要的所有数据。如果聚合的接口过多,就会导致页面渲染速度变慢,白屏时间过长的问题。此时就需要梳理首屏/次屏业务,梳理哪些接口是首屏加载时必须调用的,哪些接口是可以在次屏加载时调用的,将以前臃肿的接口拆分地更细。
    • 支持分页查询:支持页面通过接口调用的方式,分页展示数据。是的没错,看起来就是个很常见、很基础的功能。但我还真遇到过前端一次性请求所有数据,缓存到本地分页的做法(曾遇到过一个 case,一次性请求几万条数据,页面加载了半天,人都麻了。。。)
  • 业务优化:

    • 如果你的项目中引入了 A/B test 代码,用于收集用户对新功能的反馈、点击次数等等,在经过很长一段时间收集后,可以推动产品分析数据,决策是否将新功能推广给所有用户或者下线新功能;
    • 当上面的优化手段能做的都做了后,达到了性能瓶颈,这个时候就可以考虑下线一些历史悠久、低价值/低访问、非核心业务/功能,以此降低包总体积的大小(PS:适用于庞大的小程序项目,因为小程序的总包和分包有体积限制,一旦超过就无法发布。我们以前就经常遇到这个问题,每次发版时会包含很多个业务团队新增的功能,这个时候就得强行逼各个业务团队去检查&优化自己的分包体积,否则当前版本发布不允许你上车)。

性能保持

按照上述的思路进行性能优化后,可以明显地感知到页面性能的提升,用户体验上升不少,从而间接地提高产品使用率、GMV 等等,老板对你很满意,年底打算给你打个高分绩效,晋升走一波美滋滋。但这样真的结束了吗? 如果在经历了很长一段时间后,期间遇到产品迭代、团队人员流动等等因素,又发现页面出现了性能问题,这个时候又需要按照上述的思路去进行性能优化,周而复始(听起来又是一个高绩效项目,hhh,开个玩笑)。这个时候,我们就需要对优化完后的性能进行保持,让页面性能始终维持在一个理想的指标内。

  • 性能度量: 以常见的浏览器指标为依据,来设定我们的页面性能好与坏的指标;
  • 性能分析和监控
    • 搭建性能监控平台,收集&展示相关埋点数据,支持分析页面加载性能、接口性能;
    • 时刻监控性能指标数据,一旦超出阈值就通过聊天机器人/短信的形式预警相关负责人;
    • 定期进行性能专项测试,及时发现问题;
  • 性能门禁: 当性能监控平台搭建完毕后,就可以在测试环境中通过该平台分析页面的性能是否超过设定的指标,一旦超过指标,那么就需要提前在测试阶段开始进行性能优化处理。如果发布正式版时,还没有进行性能优化处理或者性能优化结果不理想,原则上不允许发布(就看怎么 撕逼 友好地协商了)。

优化思路

  • 了解性能指标(多快才算快);
  • 利用测量工具和 API 分析页面性能;
  • 寻找性能瓶颈(如寻找前端、网络、后端三个阶段中的性能问题),确定优化目标和指标;
  • 优化问题,重新测量(迭代);

优化原则

  • 加载阶段:减少关键资源的大小,减少关键资源的个数。
  • 交互阶段:尽量减少一帧的生成时间。可以通过减少单次 JavaScript 的执行时间、避免强制同步布局、避免布局抖动、尽量采用 CSS 的合成动画、避免频繁的垃圾回收等方式来减少一帧生成的时长。

测量工具

使用 Network 面板分析单个资源请求时间线

【性能优化】如何系统性地优化页面性能?
名词解释:developer.chrome.com/docs/devtoo…

  • Queueing: 在请求队列中的时间。
  • Stalled: 从 TCP 连接建立完成,到真正可以传输数据之间的时间差,此时间包括代理协商时间。
  • Proxy negotiation: 与代理服务器连接进行协商所花费的时间。
  • DNS Lookup: 执行 DNS 查找所花费的时间,页面上的每个不同的域都需要进行DNS查找。
  • Initial Connection / Connecting: 建立连接所花费的时间,包括TCP握手/重试和协商SSL。
  • SSL: 完成 SSL 握手所花费的时间。
  • Request sent: 发出网络请求所花费的时间,通常为一毫秒内。
  • Waiting for server response / Waiting(TFFB): 表示从发出请求到接收服务端第一批数据等待的时间,通常也称为“第一字节时间”,是反映服务端响应速度的重要指标,对服务器来说,TTFB 时间越短,就说明服务器响应越快。
  • Content Download: 从接收到第一个字节 到 接收到全部响应数据所用的时间。

使用 Chrome DevTools — Lighthouse

【性能优化】如何系统性地优化页面性能?

使用 Chrome DevTools — Performance

因为浏览器的渲染机制过于复杂,所以渲染模块在执行渲染的过程中会被划分为很多子阶段,输入的 HTML 数据经过这些子阶段,最后输出屏幕上的像素,我们把这样的一个处理流程叫做渲染流水线。一条完整的渲染流水线包括了解析 HTML 文件生成 DOM、解析 CSS 生成 CSSOM、执行 JavaScript、样式计算、构造布局树、准备绘制列表、光栅化、合成、显示等一系列操作。渲染流水线主要是在渲染进程中执行的,在执行渲染流水线的过程中,渲染进程又需要网络进程、浏览器进程、GPU 等进程配合,才能完成如此复杂的任务。另外在渲染进程内部,又有很多线程来相互配合。具体的工作方式你可以参考下图:
【性能优化】如何系统性地优化页面性能?
【性能优化】如何系统性地优化页面性能?

  • Main 指标:记录渲染主线程的任务执行过程(如何分析Performance中的Main指标?);
  • Compositor 指标:记录了合成线程的任务执行过程;
  • GPU 指标:记录了 GPU 进程主线程的任务执行过程;
  • Network 指标:网络记录展示了页面中的每个网络请求所消耗的时长,并以瀑布流的形式展现。这块内容和网络面板的瀑布流类似,之所以放在性能面板中是为了方便我们和其他指标对照着分析。
  • Timings 指标:用来记录一些关键的时间节点在何时产生的数据信息,诸如 FP、FCP、LCP 等。
  • Frames 指标:也就是浏览器生成每帧的记录,我们知道页面所展现出来的画面都是由渲染进程一帧一帧渲染出来的,帧记录就是用来记录渲染进程生成所有帧信息,包括了渲染出每帧的时长、每帧的图层构造等信息,你可以点击对应的帧,然后在详细信息面板里面查看具体信息。
  • Interactions 指标:用来记录用户交互操作,比如点击鼠标、输入文字等交互信息。

使用 WebPageTest 评估 Web 网站性能

【性能优化】如何系统性地优化页面性能?

使用三方云测平台

Testin

常用的性能测量 API

  • 关键时间节点(Navigation Timing、Resource Timing)
  • 网络状态(Network API)
  • 客户端服务端协商(HTTP Client Hints)&网页显示状态(UI API)
// 时间节点计算公式
DNS 解析耗时: domainLookupEnd - domainLookupStart
TCP 连接耗时: connectEnd - connectStart
SSL 安全连接耗时: connectEnd - secureConnectionStart
网络请求耗时 (TTFB): responseStart - requestStart
数据传输耗时: responseEnd - responseStart
DOM 解析耗时: domInteractive - responseEnd
资源加载耗时: loadEventStart - domContentLoadedEventEnd
First Byte时间: responseStart - domainLookupStart
白屏时间: responseEnd - fetchStart
首次可交互时间: domInteractive - fetchStart
DOM Ready 时间: domContentLoadEventEnd - fetchStart
页面完全加载时间: loadEventStart - fetchStart
http 头部大小: transferSize - encodedBodySize
重定向次数:performance.navigation.redirectCount
重定向耗时: redirectEnd - redirectStart

推荐阅读

浏览器工作原理与实践_浏览器_V8原理