关于如何分解 LCP 并确定需要改进的关键领域的分步指南。
Largest Contentful Paint (LCP) 是三个 Core Web Vitals 指标之一,它代表了网页主要内容的加载速度。 具体来说,LCP 测量从用户开始加载页面到在视口中呈现最大的图像或文本块的时间。
为了提供良好的用户体验,网站应努力使至少 75% 的页面访问的 LCP 为 2.5 秒或更短。
有许多因素会影响浏览器加载和呈现网页的速度,其中任何一个的阻塞或延迟都会对 LCP 产生重大影响。
对页面的单个部分进行快速修复很少会对 LCP 产生有意义的改进。 要改进 LCP,您必须查看整个加载过程并确保优化过程中的每一步。
针对 LCP 进行优化是一项复杂的任务,对于复杂的任务,通常最好将它们分解为更小、更易于管理的任务并分别处理每个任务。 本指南将介绍如何将 LCP 分解为其最关键的子部分的方法,然后介绍如何优化每个部分的具体建议和最佳实践。
有关本指南中介绍的上下文的直观概述,请参阅 Google I/O '22 中的深入探讨优化 LCP:
LCP 分解
大多数页面加载通常包括许多网络请求,但为了确定改进LCP的机会,您应该从以下两个开始:
-
初始HTML文档
-
LCP资源(如果适用)
虽然页面上的其他请求会影响LCP,但这两个请求(特别是LCP资源开始和结束的时间)揭示了页面是否为LCP进行了优化。
要识别 LCP 资源,您可以使用开发人员工具(例如 Chrome DevTools 或 WebPageTest)来确定 LCP element,然后您可以从那里匹配该元素在所有资源的 network waterfall 上加载的 URL(同样,如果适用) 由页面加载。
例如,下面的可视化显示了来自典型页面加载的网络瀑布图中突出显示的这些资源,其中LCP元素需要一个图像请求来呈现。
对于优化良好的页面,您希望LCP资源请求尽可能早地开始加载,并希望LCP元素在LCP资源完成加载后尽可能快地呈现。为了帮助可视化某个特定页面是否遵循这一原则,您可以将LCP总时间分解为以下子部分:
此表更详细地解释了这些 LCP 子部分:
LCP sub-part 描述
第一个字节时间(TTFB) 从用户开始加载页面到浏览器接收到HTML文档响应的第一 个字节的时间。(更多细节请参见TTFB指标文档。)
资源加载延迟 TTFB与浏览器开始加载LCP资源之间的时间差。*
资源加载时间 加载LCP资源本身所需的时间。*
元素渲染延迟 元素渲染延迟 LCP 资源完成加载到 LCP 元素完全渲染之间 的时间差。
* 如果 LCP 元素不需要资源加载来呈现(例如,如果元素是使用系统字体呈现的文本节点),则此时间将为 0。
每个页面都可以将其 LCP 值分解为这四个子部分。 它们之间没有重叠或间隙,它们加起来就是完整的 LCP 时间。
在优化 LCP 时,尝试单独优化这些子部分会很有帮助。 但同样重要的是要记住,您需要优化所有这些。 在某些情况下,应用到某个部分的优化不会提高 LCP,它只会将节省的时间转移到另一部分。
例如,在早期的网络瀑布中,如果您通过更多压缩或切换到更优化的格式(例如 AVIF 或 WebP)来减小图像的文件大小,这将减少资源加载时间,但实际上并不会 改进 LCP,因为时间只会转移到元素渲染延迟子部分:
发生这种情况的原因是,在此页面上,LCP 元素是隐藏的,直到 JavaScript 代码完成加载,然后所有内容都会立即显示出来。
此示例有助于说明您需要优化所有这些子部分以实现最佳 LCP 结果。
最优sub-part时间
LCP sub-part % of LCPTime to first byte (TTFB) ~40%Resource load delay <10%Resource load time ~40%Element render delay <10%TOTAL 100%
请注意,这些时间细分并不是严格的规则,而是指导方针。 如果您页面上的 LCP 时间始终在 2.5 秒内,那么相对比例是多少并不重要。 但是,如果你在任何一个“延迟”部分都花费了大量不必要的时间,那么就很难持续达到 2.5 秒的目标。
考虑 LCP 时间细分的一个好方法是:
-
绝大多数 LCP 时间应该花在加载 HTML 文档和 LCP 源代码上。
-
在 LCP 之前的任何时候,如果这两种资源之一没有加载,都是改进的机会。
警告: 鉴于 LCP 的 2.5 秒目标,尝试将这些百分比转换为绝对数字可能很诱人,但不建议这样做。 这些子部分仅相对于彼此有意义,因此最好始终以这种方式测量它们。
如何优化每个部分
现在您已经了解了LCP子部分时间在一个优化良好的页面上是如何分解的,您可以开始优化您自己的页面了。
接下来的四个部分将介绍如何优化每个部分的建议和最佳实践。它们按顺序呈现,从可能产生最大影响的优化开始。
1. 消除资源加载延迟
此步骤的目标是确保LCP资源尽可能早地开始加载。虽然理论上资源最早可以在TTFB之后立即开始加载,但实际上在浏览器真正开始加载资源之前总是有一些延迟。
一个好的经验法则是,您的LCP资源应该在该页加载第一个资源的同时开始加载。或者,换句话说,如果LCP资源开始加载比第一个资源晚,那么就有改进的机会。
一般来说,有两个因素会影响LCP资源的加载速度:
-
当发现资源时。
-
资源的优先级。
优化什么时候资源被发现
为了确保LCP资源尽可能早地开始加载,必须让浏览器的预加载扫描器(preload scanner)在初始HTML文档响应中发现该资源。例如,在以下情况下,浏览器可以通过扫描HTML文档响应发现LCP资源:
-
LCP 元素是一个
元素,其 src 或 srcset 属性存在于初始 HTML 标记中。
-
LCP 元素需要 CSS 背景图像,但该图像是通过 HTML 标记中的 (或通过 Link 标头)预加载的。
-
LCP 元素是一个文本节点,需要 Web 字体才能呈现,字体通过 HTML 标记中的 加载(或通过 Link 标头)。
以下是一些无法通过扫描 HTML 文档响应发现 LCP 资源的示例:
-
LCP 元素是一个通过 JavaScript 动态添加到页面的
。
-
LCP 元素通过隐藏其 src 或 srcset 属性(通常为 data-src 或 data-srcset)的 JavaScript 库延迟加载。
-
LCP 元素需要 CSS 背景图像。
在每种情况下,浏览器都需要运行脚本或应用样式表(这通常涉及等待网络请求完成),然后它才能发现 LCP 资源并开始加载它。 这从来都不是最优的。
为了消除不必要的资源加载延迟,您的 LCP 资源应该始终可以从 HTML 源中发现。 如果资源仅从外部 CSS 或 JavaScript 文件中引用,则应以高获取优先级预加载 LCP 资源(下一节将详细介绍获取优先级(fetch priority in the next section)); 例如:
<!-- Load the stylesheet that will reference the LCP image. -->
<link rel="stylesheet" href="/path/to/styles.css">
<!-- Preload the LCP image with a high fetchpriority so it starts loading with the stylesheet. -->
<link rel="preload" fetchpriority="high" as="image" href="/path/to/hero-image.webp" type="image/webp">
注意:在大多数页面上,确保LCP资源与第一个资源同时开始加载就足够了,但是要注意,有可能构造一个页面,其中没有任何资源在较早的时候被发现,所有资源都比TTFB开始加载的时间要晚得多。因此,虽然与第一个资源进行比较是确定改进机会的好方法,但在某些情况下这可能是不够的,因此,相对于TTFB衡量这个时间并确保它保持在较小的范围内仍然很重要。
优化资源被赋予的优先级
即使从 HTML 标记中可以发现 LCP 资源,它仍然可能不会早于第一个资源开始加载。 如果浏览器预加载扫描器的优先级试探法没有识别出该资源很重要,或者如果它确定其他资源更重要,则可能会发生这种情况。
例如,如果在 元素上设置
loading="lazy",则可以通过 HTML 延迟 LCP 图像。 使用延迟加载意味着在布局确认图像在视口中之前不会加载资源,因此可能会比其他情况更晚开始加载。
注意:永远不要延迟加载 LCP 映像,因为这总是会导致不必要的资源加载延迟,并对 LCP 产生负面影响。
即使没有延迟加载,浏览器最初也不会以最高优先级加载图像,因为它们不是呈现阻塞资源。你可以通过fetchpriority属性来提示浏览器哪些资源是最重要的,这些资源可以从更高的优先级中受益:
<img fetchpriority="high" src="/path/to/hero-image.webp">
如果您认为元素很可能是页面的LCP元素,那么在该元素上设置fetchpriority="high"是一个好主意——但是将其限制为仅1或2张图像(基于常见的桌面和移动视口大小),否则信号将变得毫无意义。你也可以降低那些可能在文档响应早期,但由于样式而不可见的图像的优先级,例如在启动时不可见的旋转木马幻灯片中的图像:
<img fetchpriority="low" src="/path/to/carousel-slide-3.webp">
优先处理某些资源可以为更需要带宽的资源提供更多带宽——但要小心。总是在DevTools中检查资源优先级,并使用实验室和现场工具测试更改。
在优化了LCP资源优先级和发现时间之后,网络瀑布应该如下所示(LCP资源与第一个资源同时启动):
_IMPORTNT LCP资源可能不会尽早开始加载的另一个原因是(即使可以从HTML源发现它),它是否驻留在不同的源上,因为这些请求需要浏览器在资源开始加载之前连接到该源。在可能的情况下,将关键资源托管在与HTML文档资源相同的原点上是一个好主意,因为这些资源可以通过重用现有连接来节省时间(稍后将详细讨论这一点)_
2.消除元素渲染延迟
此步骤的目标是确保LCP元素在其资源完成加载后能够立即呈现,无论加载何时发生。
LCP元素不能在其资源完成加载后立即呈现的主要原因是由于其他原因导致呈现阻塞:
-
由于中的样式表或同步脚本仍在加载,整个页面的呈现被阻塞。
-
LCP资源已经完成加载,但是LCP元素还没有添加到DOM中(它正在等待加载一些JavaScript代码)。
-
元素隐藏在其他代码中,比如A/B测试库,它仍然在决定用户应该进行什么实验。
-
主线程由于长任务而阻塞,呈现工作需要等待这些长任务完成。
以下部分解释了如何解决不必要的元素渲染延迟的最常见原因。
Reduce or inline render-blocking stylesheets
从 HTML 标记加载的样式表将阻止呈现它们后面的所有内容,这很好,因为您通常不希望呈现无样式的 HTML。 但是,如果样式表太大以至于加载时间比 LCP 资源要长得多,那么它将阻止 LCP 元素呈现——即使在其资源完成加载后也是如此,如下例所示:
要解决此问题,您的选择是:
- 将样式表内联到 HTML 中以避免额外的网络请求; 或者,
- 减小样式表的大小。
通常,仅当您的样式表很小时才建议内联样式表,因为 HTML 中的内联内容无法从后续页面加载中的缓存中受益。 如果样式表太大以至于加载时间比 LCP 资源长,那么它不太可能是内联的好候选。
在大多数情况下,确保样式表不会阻止呈现 LCP 元素的最佳方法是减小其大小,使其小于 LCP 资源。 这应该确保它不是大多数访问的瓶颈。
一些减少样式表大小的建议是:
-
删除未使用的 CSS:使用 Chrome DevTools 查找未使用且可能被删除(或延迟)的 CSS 规则。 缩小和压缩 CSS:对于至关重要的样式,请确保尽可能减少它们的传输大小。
-
推迟非关键 CSS:将样式表拆分为初始页面加载所需的样式,然后是可以延迟加载的样式。
-
缩小和压缩 CSS:对于至关重要的样式,请确保尽可能减少它们的传输大小。
延迟或内联渲染阻止 JavaScript
几乎从不需要向页面的 添加同步脚本(没有 async 或 defer 属性的脚本),这样做几乎总是会对性能产生负面影响。
如果 JavaScript 代码需要在页面加载时尽早运行,最好将其内联,这样渲染就不会延迟等待另一个网络请求。 但是,与样式表一样,您应该只在脚本非常小的时候内联脚本。
Don't
<head>
<script src="/path/to/main.js"></script>
</head>
Do
<head>
<script>
// Inline script contents directly in the HTML.
// IMPORTANT: only do this for very small scripts.
</script>
</head>
使用服务器端渲染
Server-side rendering (SSR) 是在服务器上运行客户端应用程序逻辑并使用完整 HTML 标记响应 HTML 文档请求的过程。
从优化 LCP 的角度来看,SSR 有两个主要优势:
-
您可以从 HTML 源中发现您的图像资源(如前面的步骤 1 中所述)。
-
您的页面内容在呈现之前不需要完成额外的 JavaScript 请求。
SSR 的主要缺点是它需要额外的服务器处理时间,这会减慢您的 TTFB。 这种折衷通常是值得的,因为服务器处理时间在您的控制范围内,而您的用户的网络和设备能力却不是。
与 SSR 类似的选项称为静态站点生成 (SSG) 或预渲染。 这是在构建步骤而不是按需生成 HTML 页面的过程。 如果您的架构可以进行预渲染,那么它通常是性能更好的选择。
分解长任务
即使您遵循了上述建议,并且您的 JavaScript 代码没有渲染阻塞,也不负责渲染您的元素,它仍然会延迟 LCP。
发生这种情况的最常见原因是当页面加载需要在浏览器的主线程上解析和执行的大型 JavaScript 文件时。
这意味着,即使您的图像资源已完全下载,它仍可能必须等到不相关的脚本完成执行才能呈现。 今天所有的浏览器都在主线程上渲染图像,这意味着任何阻塞主线程的东西也会导致不必要的元素渲染延迟。
3.减少资源加载时间
此步骤的目标是减少通过网络将资源字节传输到用户设备所花费的时间。 一般来说,有三种方法可以做到这一点:
- 减少资源的大小。
- 缩短资源必须传递的距离。
- 减少对网络带宽的争用。
- 彻底消除网络时间。
减少资源的大小
页面的 LCP 资源(如果有的话)将是图像或 Web 字体。 以下指南详细介绍了如何减小两者的大小:
- 提供最佳的图像大小 (Serve the optimal image size)
- 使用现代图像格式 (Use modern image formats)
- 压缩图片 (Compress images)
- 减小网页字体大小(Reduce web font size)
缩短资源必须传递的距离
除了减少资源的大小之外,您还可以通过让您的服务器在地理上尽可能靠近您的用户来减少加载时间。 最好的方法是使用内容交付网络 content delivery network (CDN)。
事实上,image CDNs 尤其是个不错的选择,因为它们不仅缩短了资源必须传输的距离,而且通常还减小了资源的大小——自动实现前面为您提出的所有减小大小的建议。
**注意:虽然图像 CDN 是减少资源加载时间的好方法,但使用第三方域来托管图像会带来额外的连接成本。 虽然预连接到源可以减轻部分成本,但最好的选择是提供与 HTML 文档相同的源的图像。 许多 CDN 允许您将请求从您的来源代理到他们的来源,如果可用,这是一个很好的选择。**
减少对网络带宽的争用
即使您已经减少了资源的大小和它必须移动的距离,如果您同时加载许多其他资源,那么资源仍然需要很长时间才能加载。这个问题被称为网络争用。
如果您给LCP资源一个高的获取优先级 high fetchpriority,并尽快开始加载它 started loading it as soon as possible,那么浏览器将尽最大努力防止低优先级资源与它竞争。但是,如果您正在以高获取优先级加载许多资源,或者只是加载大量资源,那么它可能会影响LCP资源加载的速度。
彻底消除网络时间
减少资源加载时间的最佳方法是从流程中完全消除网络。 如果您使用有效的缓存控制策略 - efficient cache-control policy 来服务您的资源,那么第二次请求这些资源的访问者将从缓存中获得它们 - 使资源加载时间基本上为零!
而如果你的 LCP 资源是网页字体,除了减少网页字体大小 - reducing web font size 外,你还应该考虑是否需要在网页字体资源加载时阻止渲染。 如果您将 font-display 值设置为 auto 或 block 以外的任何值,则在加载期间文本将始终可见 always be visible during load,并且 LCP 不会在其他网络请求时被阻止。
最后,如果您的 LCP 资源很小,将资源内联为数据 URL (data URL)可能是有意义的,这也将消除额外的网络请求。 但是,使用数据 URL 会带来一些注意事项(comes with caveats),因为这样资源就无法被缓存,并且在某些情况下,由于额外的解码成本,可能会导致更长的渲染延迟(decode cost)。
4. 减少 TTFB
此步骤的目标是尽快交付初始 HTML。 此步骤最后列出,因为它通常是开发人员控制最少的一个。 然而,它也是最重要的步骤之一,因为它直接影响到它之后的每一步。 在后端交付内容的第一个字节之前,前端不会发生任何事情,因此您可以采取任何措施来加快 TTFB 的速度,也将改善所有其他负载指标。
有关此主题的具体指导,请参阅:如何改进 TTFB (How to improve TTFB)。
在 JavaScript 中监控 LCP 故障
通过以下性能 API 的组合,您可以在 JavaScript 中使用上面讨论的所有 LCP 子部分的时间信息:
- 最大的内容绘画 API (Largest Contentful Paint API)
- 导航计时 API (Navigation Timing API)
- 资源计时 API(Resource Timing API)
在 JavaScript 中计算这些时间值的好处是它允许您将它们发送给分析提供商或将它们记录到您的开发人员工具中以帮助调试和优化。
例如,以下屏幕截图使用 User Timing API 中的 performance.measure() 方法向 Chrome DevTools Performance 面板中的 Timings 轨道添加条形图。
当与网络和主线程轨道一起查看时,时序轨道中的可视化特别有用,因为您可以一目了然地看到在这些时间跨度内页面上发生的其他事情。
除了在计时轨道中可视化 LCP 子部分,您还可以使用 JavaScript 计算每个子部分占总 LCP 时间的百分比。 有了这些信息,您可以确定您的网页是否符合前面描述的建议百分比细分。
此屏幕截图显示了一个示例,该示例记录了每个 LCP 子部分的总时间,以及它在控制台中占总 LCP 时间的百分比。
这两个可视化都是使用以下代码创建的:
const LCP_SUB_PARTS = [
'Time to first byte',
'Resource load delay',
'Resource load time',
'Element render delay',
];
new PerformanceObserver((list) => {
const lcpEntry = list.getEntries().at(-1);
const navEntry = performance.getEntriesByType('navigation')[0];
const lcpResEntry = performance
.getEntriesByType('resource')
.filter((e) => e.name === lcpEntry.url)[0];
// Ignore LCP entries that aren't images to reduce DevTools noise.
// Comment this line out if you want to include text entries.
if (!lcpEntry.url) return;
// Compute the start and end times of each LCP sub-part.
// WARNING! If your LCP resource is loaded cross-origin, make sure to add
// the `Timing-Allow-Origin` (TAO) header to get the most accurate results.
const ttfb = navEntry.responseStart;
const lcpRequestStart = Math.max(
ttfb,
// Prefer `requestStart` (if TOA is set), otherwise use `startTime`.
lcpResEntry ? lcpResEntry.requestStart || lcpResEntry.startTime : 0
);
const lcpResponseEnd = Math.max(
lcpRequestStart,
lcpResEntry ? lcpResEntry.responseEnd : 0
);
const lcpRenderTime = Math.max(
lcpResponseEnd,
// Prefer `renderTime` (if TOA is set), otherwise use `loadTime`.
lcpEntry ? lcpEntry.renderTime || lcpEntry.loadTime : 0
);
// Clear previous measures before making new ones.
// Note: due to a bug this does not work in Chrome DevTools.
LCP_SUB_PARTS.forEach((part) => performance.clearMeasures(part));
// Create measures for each LCP sub-part for easier
// visualization in the Chrome DevTools Performance panel.
const lcpSubPartMeasures = [
performance.measure(LCP_SUB_PARTS[0], {
start: 0,
end: ttfb,
}),
performance.measure(LCP_SUB_PARTS[1], {
start: ttfb,
end: lcpRequestStart,
}),
performance.measure(LCP_SUB_PARTS[2], {
start: lcpRequestStart,
end: lcpResponseEnd,
}),
performance.measure(LCP_SUB_PARTS[3], {
start: lcpResponseEnd,
end: lcpRenderTime,
}),
];
// Log helpful debug information to the console.
console.log('LCP value: ', lcpRenderTime);
console.log('LCP element: ', lcpEntry.element, lcpEntry.url);
console.table(
lcpSubPartMeasures.map((measure) => ({
'LCP sub-part': measure.name,
'Time (ms)': measure.duration,
'% of LCP': `${
Math.round((1000 * measure.duration) / lcpRenderTime) / 10
}%`,
}))
);
}).observe({type: 'largest-contentful-paint', buffered: true});
您可以按原样使用此代码进行本地调试,或对其进行修改以将此数据发送到分析提供商,以便您可以更好地了解 LCP 在您的真实用户页面上的故障。
概括
LCP 很复杂,它的时间安排会受到许多因素的影响。 但是如果你认为优化 LCP 主要是为了优化 LCP 资源的负载,它可以大大简化事情。
概括地说,优化 LCP 可以概括为四个步骤:
- 确保 LCP 资源尽早开始加载。
- 确保 LCP 元素可以在其资源完成加载后立即呈现。
- 在不牺牲质量的情况下尽可能减少 LCP 资源的加载时间。
- 尽快交付初始 HTML 文档。
如果您能够在您的页面上执行这些步骤,那么您应该确信您正在为您的用户提供最佳加载体验,并且您应该会在您的真实 LCP 分数中看到这一点。