如何修复累积布局移位 (CLS) 问题
已发表: 2022-03-10Cumulative Layout Shift (CLS) 尝试将页面的那些不和谐的移动测量为新内容——无论是图像、广告还是其他任何东西——比页面的其余部分更晚发挥作用。 它根据页面的意外移动量和频率来计算分数。 这些内容的变化非常烦人,使您在已开始阅读的文章中失去自己的位置,或者更糟糕的是,使您单击了错误的按钮!
在本文中,我将讨论一些减少 CLS 的前端模式。 我不会过多谈论如何测量 CLS,因为我已经在之前的文章中介绍过。 我也不会过多谈论如何计算 CLS 的机制:Google 对此有一些很好的文档,而 Jess Peck 的 The Near-Complete Guide to Cumulative Layout Shift 也是一个很棒的深入探讨。 但是,我将提供一些了解一些技术所需的背景知识。
为什么 CLS 与众不同
在我看来,CLS 是 Core Web Vitals 中最有趣的,部分原因是我们以前从未真正衡量或优化过它。 因此,它通常需要新的技术和思维方式来尝试对其进行优化。 它与其他两个 Core Web Vitals 非常不同。
简要回顾一下其他两个核心 Web Vitals,Largest Contentful Paint (LCP) 就像它的名字所暗示的那样,它更像是对以前衡量页面加载速度的加载指标的一种扭曲。 是的,我们改变了定义页面加载用户体验的方式,以查看最相关内容的加载速度,但它基本上是重用确保内容尽快加载的旧技术。 对于大多数网页来说,如何优化你的 LCP 应该是一个比较容易理解的问题。
首次输入延迟 (FID) 测量交互中的任何延迟,对于大多数站点来说似乎不是问题。 优化这通常是清理(或减少!)您的 JavaScript 的问题,并且通常是特定于站点的。 这并不是说用这两个指标解决问题很容易,但它们是相当容易理解的问题。
CLS 不同的一个原因是它是通过页面的生命周期来衡量的——这是名称的“累积”部分! 其他两个 Core Web Vitals 在加载后(对于 LCP)或第一次交互(对于 FID)后在页面上找到主要组件后停止。 这意味着我们传统的基于实验室的工具(如 Lighthouse)通常不能完全反映 CLS,因为它们仅计算初始负载 CLS。 在现实生活中,用户将向下滚动页面,可能会导致更多内容下降,从而导致更多变化。
CLS 也是一个人为的数字,它是根据页面的移动量和频率计算得出的。 虽然 LCP 和 FID 以毫秒为单位测量,但CLS 是通过复杂计算输出的无单位数。 我们希望页面为 0.1 或以下以通过此 Core Web Vital。 任何高于 0.25 的值都被视为“差”。
由用户交互引起的班次不计算在内。 这被定义为在一组特定用户交互的500 毫秒内,但不包括指针事件和滚动。 假定单击按钮的用户可能期望出现内容,例如通过展开折叠的部分。
CLS 是关于测量意想不到的变化。 如果页面构建得最佳,滚动不应导致内容四处移动,并且类似地,将鼠标悬停在产品图像上以获得放大版本也不应导致其他内容跳跃。 但当然也有例外,这些网站需要考虑如何应对。
CLS 也随着调整和错误修复不断发展。 它刚刚宣布了一个更大的变化,应该为长期存在的页面提供一些喘息的机会,比如单页应用程序 (SPA) 和无限滚动页面,许多人认为这些页面在 CLS 中受到了不公平的惩罚。 不是像迄今为止所做的那样在整个页面时间上累积班次来计算 CLS 分数,而是根据特定时间框窗口内的最大班次集来计算分数。
这意味着如果您有 0.05、0.06 和 0.04 的三个 CLS 块,那么以前这将被记录为 0.15(即超过 0.1 的“好”限制),而现在将被评分为 0.06。 它仍然是累积的,因为分数可能由该时间范围内的单独班次组成(即,如果 0.06 CLS 分数是由 0.02 的三个单独班次引起的),但它不再在页面的总生命周期内累积.
也就是说,如果您解决了 0.06 偏移的原因,那么您的 CLS 将被报告为下一个最大的偏移 (0.05),因此它仍在查看页面生命周期内的所有偏移——它只是选择仅报告最大的一个作为 CLS 分数。
通过对有关 CLS 的一些方法的简要介绍,让我们继续讨论一些解决方案! 所有这些技术基本上都涉及在加载其他内容之前留出正确的空间量——无论是媒体还是 JavaScript 注入的内容,但是 Web 开发人员可以使用一些不同的选项来执行此操作。
在图像和 iFrame 上设置宽度和高度
我之前已经写过这方面的内容,但是减少 CLS 可以做的最简单的事情之一就是确保在图像上设置了width
和height
属性。 没有它们,图像将导致后续内容在下载后转移为它让路:
这只是将图像标记从以下位置更改的问题:
<img src="hero_image.jpg" alt="...">
到:
<img src="hero_image.jpg" alt="..." width="400" height="400">
您可以通过打开 DevTools 并将鼠标悬停在(或点击)元素上找到图像的尺寸。
我建议使用Intrinsic Size (这是图像源的实际大小),然后当您使用 CSS 更改这些大小时,浏览器会将它们缩小到呈现的大小。
快速提示:如果像我一样,你不记得它是宽度和高度还是高度和宽度,把它想象成 X 和 Y 坐标,所以像 X 一样,宽度总是首先给出。
如果您有响应式图像并使用 CSS 更改图像尺寸(例如,将其限制为屏幕大小的 100% 的max-width
),那么这些属性可用于计算height
- 前提是您记得将其覆盖为在你的 CSS 中auto
:
img { max-width: 100%; height: auto; }
所有现代浏览器现在都支持这一点,但直到最近才如我的文章中所述。 这也适用于<picture>
元素和srcset
图像(在后备img
元素上设置width
和height
),虽然还不适用于不同纵横比的图像——它正在处理中,在此之前你仍然应该设置width
和height
因为任何值都将优于0
by 0
默认值!
这也适用于本机延迟加载的图像(尽管 Safari 默认不支持本机延迟加载)。
新aspect-ratio
CSS 属性
上面的width
和height
技术,用于计算响应式图像的高度,可以使用新的 CSS aspect-ratio
属性推广到其他元素,现在基于 Chromium 的浏览器和 Firefox 支持,但也在 Safari 技术预览中,所以希望这意味着它将很快进入稳定版本。
因此,您可以在嵌入式视频上使用它,例如以 16:9 的比例:
video { max-width: 100%; height: auto; aspect-ratio: 16 / 9; }
<video controls width="1600" height="900" poster="..."> <source src="/media/video.webm" type="video/webm"> <source src="/media/video.mp4" type="video/mp4"> Sorry, your browser doesn't support embedded videos. </video>
有趣的是,如果没有定义aspect-ratio
属性,浏览器将忽略响应式视频元素的高度并使用默认的纵横比 2:1,因此需要上述内容来避免此处的布局偏移。
将来,甚至应该可以使用宽高比根据元素属性动态设置aspect-ratio
aspect-ratio: attr(width) / attr(height);
但遗憾的是,这还不被支持。
或者,您甚至可以在<div>
元素上使用aspect-ratio
来创建某种自定义控件,以使其具有响应性:
#my-square-custom-control { max-width: 100%; height: auto; width: 500px; aspect-ratio: 1; }
<div></div>
对于那些不支持aspect-ratio
的浏览器,您可以使用旧的 padding-bottom hack,但是,由于新aspect-ratio
的简单性和广泛的支持(尤其是从 Safari Technical Preview 转移到常规 Safari 时),它是很难证明这种旧方法的合理性。
Chrome 是唯一将 CLS 反馈给 Google的浏览器,它支持aspect-ratio
,这意味着将解决您在 Core Web Vitals 方面的 CLS 问题。 我不喜欢将指标优先于用户,但事实上其他 Chromium 和 Firefox 浏览器都有这个,而 Safari 有望很快实现,而且这是一个渐进式增强意味着我想说我们正处于我们的阶段可以留下 padding-bottom hack 并编写更清晰的代码。
自由使用min-height
对于那些不需要响应大小但需要固定高度的元素,请考虑使用min-height
。 例如,这可能是一个固定高度的 header ,我们可以像往常一样使用媒体查询为不同的断点设置不同的标题:
header { min-height: 50px; } @media (min-width: 600px) { header { min-height: 200px; } }
<header> ... </header>
当然,这同样适用于水平放置元素的min-width
,但通常是导致 CLS 问题的高度。
注入内容和高级 CSS 选择器的更高级技术是在尚未插入预期内容时定位。 例如,如果您有以下内容:
<div class="container"> <div class="main-content">...</div> </div>
并通过 JavaScript 插入一个额外的div
:
<div class="container"> <div class="additional-content">.../div> <div class="main-content">...</div> </div>
然后,您可以使用以下代码段在最初呈现main-content
div 时为其他内容留出空间。
.main-content:first-child { margin-top: 20px; }
这段代码实际上会创建一个到main-content
元素的移位,因为边距算作该元素的一部分,因此当它被删除时它会出现移位(即使它实际上并没有在屏幕上移动)。 但是,至少它下面的内容不会被移动,所以应该减少 CLS。
或者,您也可以使用::before
伪元素添加空格以避免main-content
元素上的移位:
.main-content:first-child::before { content: ''; min-height: 20px; display: block; }
但老实说,更好的解决方案是在 HTML 中使用div
并在其上使用min-height
。
检查后备元素
我喜欢使用渐进增强来提供一个基本的网站,即使可能没有 JavaScript。 不幸的是,最近在我维护的一个站点上,当备用非 JavaScript 版本与 JavaScript 启动时不同时,我发现了这一点。
问题是由于标题中的“目录”菜单按钮造成的。 在 JavaScript 启动之前,这是一个简单的链接,其样式看起来像将您带到目录页面的按钮。 一旦 JavaScript 启动,它就会变成一个动态菜单,让您可以直接导航到您想从该页面转到的任何页面。
我使用了语义元素,因此使用了一个锚元素( <a href="#table-of-contents">
)作为后备链接,但用<button>
代替了它作为 JavaScript 驱动的动态菜单。 这些样式看起来相同,但后备链接比按钮小几个像素!
它是如此之小,而且 JavaScript 通常启动得如此之快,以至于我没有注意到它已关闭。 然而,Chrome 在计算 CLS 时注意到了这一点,并且由于它位于标题中,因此它将整个页面向下移动了几个像素。 所以这对 CLS 分数产生了相当大的影响——足以让我们所有的页面都进入“需要改进”类别。
这是我的一个错误,修复只是让两个元素同步(也可以通过在标题上设置min-height
来修复,如上所述),但这让我有点困惑。 我确定我不是唯一犯此错误的人,因此请注意页面在没有 JavaScript 的情况下如何呈现。 不要认为您的用户禁用了 JavaScript? 您的所有用户在下载您的 JS 时都是非 JS。
网页字体导致布局变化
Web 字体是 CLS 的另一个常见原因,因为浏览器最初会根据备用字体计算所需的空间,然后在下载 Web 字体时重新计算它。 通常,CLS 很小,提供类似大小的后备字体,因此它们通常不会导致足以使 Core Web Vitals 失败的问题,但它们可能会令用户感到不安。
不幸的是,即使预加载 webfonts 也无济于事,因为虽然这减少了使用后备字体的时间(因此有利于加载性能 - LCP),但仍需要时间来获取它们,因此仍将使用后备字体在大多数情况下由浏览器进行,因此不会避免 CLS。 也就是说,如果您知道下一页需要网络字体(假设您在登录页面上并且知道下一页使用特殊字体),那么您可以预取它们。
为了完全避免字体引起的布局变化,我们当然可以完全不使用网络字体——包括使用系统字体,或者使用font-display: optional
不使用它们。 但老实说,这些都不是很令人满意。
另一种选择是确保这些部分的大小适当(例如使用min-height
),因此虽然其中的文本可能会发生一些变化,但即使发生这种情况,它下面的内容也不会被下推。 例如,在<h1>
元素上设置min-height
可以防止在加载稍高的字体时整篇文章向下移动——前提是不同的字体不会导致不同的行数。 这将减少变化的影响,但是,对于许多用例(例如通用段落),很难概括最小高度。
解决这个问题我最兴奋的是新的 CSS 字体描述符,它允许您更轻松地调整 CSS 中的后备字体:
@font-face { font-family: 'Lato'; src: url('/static/fonts/Lato.woff2') format('woff2'); font-weight: 400; } @font-face { font-family: "Lato-fallback"; size-adjust: 97.38%; ascent-override: 99%; src: local("Arial"); } h1 { font-family: Lato, Lato-fallback, sans-serif; }
在此之前,使用 JavaScript 中的 Font Loading API 调整所需的备用字体更复杂,但这个选项很快就会出现,最终可能会给我们一个更简单的解决方案,更有可能获得牵引力。 请参阅我之前关于此主题的文章,了解有关此即将推出的创新的更多详细信息以及更多相关资源。
客户端渲染页面的初始模板
许多客户端渲染页面或单页应用程序仅使用 HTML 和 CSS 渲染初始基本页面,然后在 JavaScript 下载并执行后“水合”模板。
这些初始模板很容易与 JavaScript 版本不同步,因为新组件和功能在 JavaScript 中添加到应用程序中,但未添加到首先呈现的初始 HTML 模板中。 当这些组件被 JavaScript 注入时,这会导致 CLS。
因此,请检查所有初始模板以确保它们仍然是良好的初始占位符。 如果初始模板由空<div>
组成,则使用上述技术确保它们的大小适当,以避免任何变化。
此外,与应用程序一起注入的初始div
应具有min-height
,以避免在插入初始模板之前最初以 0 高度呈现它。
<div></div>
例如,只要min-height
大于大多数 viewports ,这应该避免网站页脚的任何 CLS。 CLS 仅在它位于视口中时才会测量,因此会影响用户。 默认情况下,空div
的高度为 0px,因此给它一个更接近应用加载时实际高度的min-height
。
确保用户交互在 500 毫秒内完成
导致内容转移的用户交互不包括在 CLS 分数中。 这些限制在交互后的 500 毫秒内。 因此,如果您单击一个按钮,并进行一些耗时超过 500 毫秒的复杂处理,然后渲染一些新内容,那么您的 CLS 分数将会受到影响。
您可以通过使用“性能”选项卡记录页面,然后找到下一个屏幕截图所示的班次,来查看是否在 Chrome DevTools 中排除了班次。 打开 DevTools 转到非常吓人的(但一旦掌握它就非常有用!)性能选项卡,然后单击左上角的记录按钮(在下图中圈出)并与您的页面交互,并停止记录一次完全的。
您将看到页面的幻灯片,其中我加载了另一篇 Smashing Magazine 文章的一些评论,因此在我圈出的部分中,您几乎可以看到评论加载和红色页脚被移出屏幕。 在Performance选项卡的下方,在Experience行下方,Chrome 将为每个班次放置一个淡红色的框,当您单击该框时,您将在下面的Summary选项卡中获得更多详细信息。
在这里您可以看到我们得到了0.3359 的巨大分数——远远超过了我们的目标是低于 0.1 的阈值,但累积分数没有包括这个,因为最近的输入设置为使用。
确保交互仅在 First Input Delay 尝试测量的 500 ms 边界内移动内容,但在某些情况下,用户可能会看到输入产生了影响(例如显示了加载微调器),因此 FID 很好,但内容可能直到 500 毫秒限制之后才被添加到页面,所以 CLS 不好。
理想情况下,整个交互将在 500 毫秒内完成,但是您可以在处理过程中使用上述技术做一些事情来留出必要的空间,这样如果它确实需要超过神奇的 500 毫秒,那么您已经已经处理了班次,因此不会因此受到处罚。 这在从网络获取可能是可变的且超出您控制范围的内容时特别有用。
其他需要注意的项目是时间超过 500 毫秒的动画,因此会影响 CLS。 虽然这看起来有点限制,但 CLS 的目的不是限制“乐趣”,而是设定对用户体验的合理期望,我认为期望这些时间花费 500 毫秒或以下并不现实。 但是,如果您不同意,或者有他们可能没有考虑过的用例,那么 Chrome 团队愿意就此提供反馈。
同步 JavaScript
我要讨论的最后一种技术有点争议,因为它违背了众所周知的 Web 性能建议,但在某些情况下它可能是唯一的方法。 基本上,如果您知道内容会导致变化,那么避免变化的一种解决方案是在它稳定下来之前不渲染它!
下面的 HTML 最初会隐藏div
,然后加载一些阻止渲染的 JavaScript 来填充div
,然后取消隐藏它。 由于 JavaScript 正在渲染阻止,因此不会渲染任何低于此的内容(包括取消隐藏它的第二个style
块),因此不会发生任何变化。
<style> .cls-inducing-div { display: none; } </style> <div class="cls-inducing-div"></div> <script> ... </script> <style> .cls-inducing-div { display: block; } </style>
使用这种技术在 HTML 中内联 CSS很重要,因此它是按顺序应用的。 另一种方法是使用 JavaScript 本身取消隐藏内容,但我喜欢上述技术的是,即使 JavaScript 失败或被浏览器关闭,它仍然会取消隐藏内容。
这种技术甚至可以应用于外部 JavaScript,但是这会比内联script
造成更多的延迟,因为外部 JavaScript 被请求和下载。 这种延迟可以通过预加载 JavaScript 资源来最小化,这样一旦解析器到达那段代码就可以更快地使用它:
<head> ... <link rel="preload" href="cls-inducing-javascript.js" as="script"> ... </head> <body> ... <style> .cls-inducing-div { display: none; } </style> <div class="cls-inducing-div"></div> <script src="cls-inducing-javascript.js"></script> <style> .cls-inducing-div { display: block; } </style> ... </body>
现在,正如我所说,这肯定会让一些 web 性能的人感到畏缩,因为建议在 JavaScript 上使用async, defer
或更新的type="module"
(默认情况下是defer
-ed)以避免阻塞render ,而我们在这里做相反的事情! 但是,如果内容无法预先确定并且会引起不和谐的变化,那么提前渲染它就没有什么意义了。
我将这种技术用于加载在页面顶部并将内容向下移动的cookie 横幅:
这需要读取 cookie 以查看是否显示 cookie 横幅,虽然这可以在服务器端完成,但这是一个静态站点,无法动态更改返回的 HTML。
Cookie 横幅可以以不同的方式实现以避免 CLS。 例如,将它们放在页面底部,或者将它们覆盖在内容之上,而不是将内容向下移动。 我们更喜欢将内容保留在页面顶部,因此必须使用这种技术来避免移位。 由于各种原因,网站所有者可能更喜欢在页面顶部显示其他各种警报和横幅。
我还在另一个页面上使用了这种技术,其中JavaScript 将内容移动到“主”和“辅助”列中(由于我不会深入讨论的原因,不可能在 HTML 服务器端正确地构造它)。 再次隐藏内容,直到 JavaScript 重新排列内容,然后才显示它,避免了导致这些页面的 CLS 分数下降的 CLS 问题。 即使 JavaScript 由于某种原因没有运行并且显示未移动的内容,内容也会自动取消隐藏。
使用此技术可能会影响其他指标(特别是 LCP 和 First Contentful Paint),因为您正在延迟渲染,并且还可能阻止浏览器的前瞻预加载器,但对于不存在其他选项的情况,它是另一个需要考虑的工具。
结论
Cumulative Layout Shift 是由内容更改尺寸或通过延迟运行的 JavaScript 将新内容注入页面引起的。 在这篇文章中,我们讨论了避免这种情况的各种提示和技巧。 我很高兴 Core Web Vitals 将焦点放在了这个恼人的问题上——长期以来,我们 Web 开发人员(当然我自己也包括在内)忽视了这个问题。
清理我自己的网站为所有访问者带来了更好的体验。 我鼓励你也看看你的 CLS 问题,希望这些技巧中的一些在你这样做时会有用。 谁知道呢,您甚至可能设法将所有页面的 CLS 分数降到难以捉摸的 0 分!
更多资源
- Smashing Magazine 上的 Core Web Vitals 文章,包括我自己关于设置图像宽度和高度、测量核心 Web Vitals 和 CSS 字体描述符的文章。
- Google 的 Core Web Vitals 文档,包括他们在 CLS 上的页面。
- 有关 CLS 最近更改的更多详细信息,然后此更改开始在各种 Google 工具中更新。
- CLS 更改日志详细说明了每个版本的 Chrome 中的更改。
- Jess Peck 的几乎完整的累积布局转换指南。
- Cumulative Layout Shift: Measure and Avoid Visual Instability by Karolina Szczur。
- 一个 Layout Shift GIF 生成器,可帮助生成 CLS 的可共享演示。