如何让你的网页在加载前就变得更快?

作为开发人员(尤其是前端开发人员),我们通常会在衡量浏览器窗口中出现内容时以及可以使用内容或与页面交互时所发生的情况的背景下讨论 Web 性能。例如,以下核心 Web 指标指导了关于我们可以看到、使用和体验的内容的讨论:

  • 首次内容绘制 (FCP),测量从用户首次导航到页面到页面内容任何部分呈现的时间,
  • 最大内容绘制 (LCP),即页面加载时间线上页面主要内容可能已加载的时间点,以及
  • 交互到下一次绘制 (INP),用于衡量网页响应用户输入的速度。

这是有道理的,因为如果没有任何东西可供使用和/或交互,我们实际上根本没有体验可衡量,但是浏览器接收到网页的第一个字节之前发生的事件呢?我们能否衡量这些事件,然后对其进行优化,以使我们的网页和应用程序加载速度更快?

Sentry Trace View 中如何可视化 TTFB 前事件

在查看Sentry 中的跟踪视图时,上述问题浮现在我的脑海中,其中捕获了在浏览器窗口中呈现任何内容之前发生的事件并将其标记为browser跨度。按时间顺序注册了六个跨度:缓存、DNS、连接、TLS/SSL、请求和响应。所有事件都response发生在第一个字节时间 (TTFB) 之前,TTFB 测量的是网页/资源请求与响应的第一个字节开始到达之间的时间。

如何让你的网页在加载前就变得更快?

您可能想知道Sentry如何捕获这些事件,因为 Sentry 在页面加载时间轴的这个点上尚未在浏览器中初始化;我也想知道同样的事情!答案在于Performance API(一组用于衡量 Web 应用程序性能的 Web 标准),更具体地说是Navigation Timing API,它在每次发生事件(称为 )时向浏览器提供一些非常有用的指标,包括高精度时间戳PerformanceEntry

真正酷的是,您还可以直接访问浏览器中的性能 API:大多数性能条目都是针对任何网页记录的,无需任何设置或额外代码即可检索它们。现在打开开发工具控制台并输入 来尝试一下window.performance。您将看到类似这样的内容(我手动分组了相关时间戳并对其进行了排序以便于解析):

// captured from https:// on Tues 30 July @ 13.42
{
'timing': {
'navigationStart': 1722343304923,

'redirectStart': 0,
'redirectEnd': 0,

'fetchStart': 1722343304928,

'domainLookupStart': 1722343304928,
'domainLookupEnd': 1722343304928,

'connectStart': 1722343304928,
'secureConnectionStart': 0,
'connectEnd': 1722343304928

'requestStart': 1722343304988,

'responseStart': 1722343304989,

'unloadEventStart': 0,
'unloadEventEnd': 0,

'responseEnd': 1722343304998,

'domInteractive': 1722343305161,
'domContentLoadedEventStart': 1722343305161,
'domContentLoadedEventEnd': 1722343305161,
'domLoading': 1722343304996,
'domComplete': 1722343305381,

'loadEventStart': 1722343305381,
'loadEventEnd': 1722343305381,
},
}

那么 Sentry 如何用这些浏览器跨度填充跟踪?由于 Performance API 从浏览器中请求 URL 的那一刻起就使用时间戳记录这些指标,因此Sentry JavaScript SDK能够在初始化后访问这些指标,补填在网页加载之前按时间顺序发生的完整事件列表,并将它们作为跨度发送到相关的完整跟踪,以便它们可以在跟踪视图中可视化。

网页加载之前会发生什么?

提供window.performance了一个窗口(绝对是双关语),让我们可以看到在浏览器中看到任何网页内容之前发生的许多不同事件。它返回一个Performance对象,其中包含一个timingproperty,如上面的代码示例所示。虽然这是一种快速检查浏览器在页面加载时记录的事件而无需编写任何代码的方法,但该Performance.timing属性现在已被弃用并已被取代PerformanceNavigationTiming API(到目前为止只有一些微小的变化)。

此图表来自导航计时级别 2 规范,显示了从浏览器中发出导航请求到当前文档加载事件完成期间记录事件的顺序PerformanceNavigationTiming。并非所有事件都会在每次页面加载时可用,但顺序与我们在上面观察到的顺序相符window.performance

如何让你的网页在加载前就变得更快?

让我们探索每个相关事件指标,看看幕后发生了什么,以及 Sentry 如何根据特定时间戳计算这些指标,以填充 Trace View 中的浏览器跨度。希望借助这些新知识,我们将开始了解如何在 TTFB 之前优化 Web 性能。

浏览器缓存

如果使用 HTTP GET(例如,网页的标准请求)获取资源,浏览器将首先检查 HTTP 缓存。返回浏览器开始检查缓存之前的时间。Sentry Trace View 中的缓存跨度计算为时间戳和时间戳fetchStart之间的时间。fetchStartdomainLookupStart

跟踪视图中缓存跨度的非零值表示浏览器从浏览器缓存中检索资源所花费的时间。较长的缓存跨度可能表示使用了较慢或较旧的浏览器,或者用户不经常清除浏览器缓存(甚至根本不清除)。

浏览器 DNS

下一个跨度报告 DNS(域名系统)查找时间。当用户请求 URL 时,DNS 会被查询以在数据库中“查找”域并将其转换为 IP 地址。完成此操作所需的总时间是通过从时间戳值中减去domainLookupStart时间戳值来计算的domainLookupEnd

浏览器连接

接下来,是时候测量浏览器连接到网络服务器所需的时间了。这被称为“连接协商”,测量的是两个事件之间的时间:(connectStart浏览器开始打开与网络服务器的连接时)和connectEnd(与网络服务器的连接已建立时)。

浏览器 TLS/SSL

如果浏览器连接的 Web 服务器使用的是 HTTPS,则在和 connectEndsecureConnectionStart之间会发生一个事件。 标记浏览器和 Web 服务器交换消息以确认和验证安全加密连接(称为 TLS(传输层安全性)协商)的时间。如果未使用 HTTPS 或连接是持久的,则 的值可能是。connectStartsecureConnectionStartsecureConnectionStart0

在 Sentry 中,连接和 TLS 事件被报告为单独的跨度。在此跟踪视图图像中,您会注意到连接事件开始,TLS 事件随后不久开始,连接结束事件在 TLS 协商完成后立即结束。这种事件表示有助于识别 Web 服务器连接或 TLS 协商中是否存在任何瓶颈。

如何让你的网页在加载前就变得更快?

浏览器请求

在与网络服务器建立(安全)连接后,浏览器将正式发出资源请求,并以事件为标记requestStart

浏览器响应

最后,浏览器会接收到第一个字节的内容。在 Sentry Trace View 中,TTFB(第一个字节时间)垂直线标记在此处。

如何让你的网页在加载前就变得更快?

我们可以让PerformanceNavigationTiming事件发生得更快吗?

现在我们了解了网页的第一个字节传送到浏览器之前发生了什么,让我们更深入地了解是否可以加快导航时间轴中发生的事件。

您能加快浏览器缓存检索事件的速度吗?

作为一名想要提高自己应用程序性能的开发人员,我不确定您是否可以为用户群加快此事件的速度。但是,通过认真处理您自己的个人浏览器缓存,您可能可以为自己加快此事件的速度。清除浏览器缓存就像向 git 存储库提交更改一样:少量多次。

您能加快 DNS 查找速度吗?

DNS 查找的速度会受到多种因素的影响,包括(但不限于):

  • DNS 提供商基础设施的规模:全球范围内“接入点”(POP)的减少意味着更长的延迟和更慢的查询速度,
  • POP 的位置:如果您的网站访问者距离 DNS 服务器较远,DNS 查找将花费更长时间,
  • DNS 缓存时间:DNS 由缓存提供,直到缓存过期。DNS 缓存时间的长度由 DNS 记录(将 URL 指向 IP 地址)上指定的生存时间 (TTL) 值决定。TTL 值越高,浏览器在每次后续请求中需要执行另一次 DNS 查找的可能性就越小。

最终,加快 DNS 查找速度需要投资拥有庞大且全球分布的 POP 网络的 DNS 提供商。如果您是一家规模庞大的企业的开发人员,那么这可能已经为您解决了问题。此外,对于不经常更改的 DNS 记录,将 TTL 值设置得尽可能高可能是一个不错的策略。

在撰写本文时,出于好奇,我检查了我的个人网站 DNS 记录,我将 TTL 设置为5 分钟。这意味着 DNS 缓存每五分钟过期一次,导致浏览器比需要更频繁地进行新的 DNS 查找。考虑到我永远不会将我的网站 URL 指向新服务器,我决定将 TTL 更改为 60 分钟。

在我的个人网站上进行的为期 5 天的有限实验中,自从切换 TTL 以来,我发现 Sentry 中浏览器 DNS 查找跨度的非零时间减少了。如果您的网站不是任务关键型网站,也不是赚钱的网站,这可能是一种有助于加快 DNS 查找速度的好方法。但请记住,如果您的主服务器出现故障,并且您想将 URL 指向备份服务器,则全球所有用户最多需要 60 分钟才能看到 DNS 更改。

话虽如此,根据Sematext 的说法,“平均 DNS 查找时间在 20 到 120 毫秒之间。介于此之间的任何时间通常都被认为是非常好的。”因此,当您需要在主服务器中断期间切换到备份服务器时,这种微优化可能不值得记住您的 TTL 设置为 60 分钟。

使用“改进第三方资源的 DNS 查找rel=”dns-prefetch”

大多数前端网站和应用程序可能至少加载一个或多个来自第三方的资源,即来自不同域的资源/图像/文件/脚本。对不同域的每个请求也将涉及 DNS 查找事件。值得注意的是,第三方资源将在 TTFB 之后被请求,因此在PerformanceNavigationTimeline本文我们关注的事件之后,您可以尝试通过使用请求资源的标签上的属性rel=’dns-prefetch” 和关联href值来加快这些第三方资源的 DNS 查找<link>。这向浏览器提供了一个提示,即用户可能需要从资源的来源获取内容,此时浏览器可以尝试通过在正式请求资源之前先对该来源执行 DNS 解析来改善用户体验。这在从 Google 提取第三方字体时很有用,例如:

<link rel='dns-prefetch' href='https://fonts./' />

根据页面加载时并行请求的第三方资源数量,这可以帮助加快事件发生后浏览器内事件的速度responseEnd,即当浏览器开始解析 HTML 并请求所有第三方资源(特别是如果它们是阻止渲染的资源)时。

注意:不要dns-prefetch在从网站顶级域名获取的资源上使用(即您使用您的网站托管的资源)。在 MDN 上了解有关使用的更多信息dns-prefetch。

您能加快连接和 TLS 协商事件的速度吗?

不使用 HTTPS?开玩笑的。关于 TLS 协商时间的底线是,即使早在 2010 年,在 Google 将 Gmail 改为使用 HTTPS 进行所有操作后,TLS 就被宣布“不再需要大量计算”。在 2013 年出版的《高性能浏览器网络》中,Ilya Grigorik 指出:“……早期的 Web 通常需要额外的硬件来执行’SSL 卸载’。好消息是,这不再是必要的,曾经需要专用硬件才能完成的工作现在可以直接在 CPU 上完成。”

Ilya 在 2013 年给出的一条建议是充分利用TLS 会话恢复,这是一种用于“在多个连接之间恢复或共享相同的协商密钥数据”的机制。简而言之,这是一种让您的计算机和网站互相记住对方的方式,这样它们就不必在每次重新连接时都经历检查加密密钥(秘密密码)的漫长过程。这使得浏览速度更快,并且消耗更少的计算能力。

除非您直接负责在服务器上实施 TLS,否则尽可能快地进行 TLS 协商可能 99.999% 都已为您搞定。但是,就像您可以使用 向浏览器提示您可能需要的资源一样rel=’dns-prefetch’,您可以更进一步,使用rel=’preconnect’指向外部资源的链接,这也会预先执行部分或全部 TLS 协商。同样,这将在PerformanceNavigationTiming事件发生后发生,但无论如何,这都是很好的信息。

您能加快浏览器请求和响应事件(TTFB)的速度吗?

作为开发人员,第一个字节时间 ( responseStart) 最终是您在页面导航时间轴中拥有最大控制权的。留意requestStartresponseStart事件之间发生的一切,并高效地优化这些事件,可以对您的页面速度和最终的用户体验产生巨大影响。

以下是在您的网站和应用中需要调查的三件事:

减少或消除请求瀑布

“请求瀑布”是指一个资源请求(代码、数据、图像、CSS 等)直到另一个资源请求完成后才开始的情况。从某种意义上说PerformanceNavigationTimelinerequestStart事件可能会延迟responseStart,具体取决于网页或应用程序的架构,以及在浏览器收到第一个数据字节之前发生了多少个同步事件。我在我的个人网站上亲身经历了这种情况;在我注意到页面加载变得非常慢之后,我调查了这种情况(使用 Sentry),发现每次页面加载都会多次往返边缘服务器。选择完全删除这些即时请求并将所需数据包含在静态页面构建中意味着我将 TTFB 彻底减少了约 80%。

也许您的应用程序在事件发生时会进行一系列数据库调用requestStart。这些查询需要按顺序进行,还是可以并行进行?更好的是,您可以在单个查询中从数据库中获取所需的所有数据吗?如果您喜欢 React,请查看 Lazar 的这篇关于如何识别 React 中的获取瀑布的文章。

更好的是:您是否需要对数据库进行任何即时调用?或者您可以按照我的做法静态构建您的网页,这样之后需要做的requestStart就是从 CDN(内容分发网络)快速交付静态 HTML 页面?注意:这并不意味着您无法在页面加载后使用客户端 JavaScript 增强网页的交互性并获取新数据。

缓存,缓存,缓存

说到 CDN,它能够将内容缓存在物理上更靠近访问者的全球边缘服务器上,如果您的网站(或其页面的子集)不提供个性化和/或动态内容,您应该利用缓存:完整的 HTML 响应会按需存储和交付,而不是需要在请求时重新生成。作为一名使用现代托管解决方案来交付我的网站的前端开发人员,我甚至不需要考虑这种级别的配置,我不会假装自己是缓存专家。但我将分享 Google 文章“优化第一个字节的时间”中的这条信息:

  • 对于频繁更新内容的网站,即使很短的缓存时间也能为繁忙的网站带来明显的性能提升,因为只有这段时间内的第一位访问者会经历回到原始服务器的完整延迟,而所有其他访问者都可以重用边缘服务器的缓存资源。

与 TLS 协商类似,作为 2024 年的前端开发人员,我们通常不必担心这个问题;借助我们可用的工具,我们可以解决这个问题。说到现代工具,许多前端框架和库现在正在将 HTML 流式传输带给大众。

利用 HTML 流的强大功能

HTML 流式传输是指服务器不是一次性提供整个 HTML 文档,而是分阶段发送部分 HTML 文档。浏览器接收这些 HTML 片段并开始解析它们,甚至渲染它们,因此网页似乎可以更快地加载。HTML 流式传输允许事件更早发生,而不是等待在事件之间接收整个 HTML 文档requestStartresponseStart这还可能涉及数据库调用和其他逻辑处理),responseStart从而缩短 TTFB。

如果您在 React 生态系统中工作并且想要了解更多信息,Lazar 将在《React 服务器组件取证》中深入介绍 HTML 流。