累积布局偏移(CLS)试图测量页面的那些刺耳的运动,因为新的内容--无论是图像、广告还是其他什么--比页面的其他部分更晚进入角色。它根据页面意外移动的程度和频率计算出一个分数。这些内容的移动是非常恼人的,使你在开始阅读的文章中失去位置,或者更糟糕的是,使你点击错误的按钮
在这篇文章中,我将讨论一些减少CLS的前端模式。
为什么CLS是不同的
在我看来,CLS是Core Web Vitals中最有趣的,部分原因是它是我们以前从未真正测量或优化过的东西。因此,它往往需要新的技术和思维方式来试图优化它。这是一个与其他两个核心网络指标非常不同的野兽。
简要地看一下其他两个核心网络指标,最大内容绘画(LCP)正如它的名字所暗示的那样,更多的是对以前的加载指标的一种扭曲,测量页面的加载速度。是的,我们改变了对页面加载的用户体验的定义,以考察最相关内容的加载速度,但它基本上是在重复使用旧的技术,以确保内容尽可能快地加载。对于大多数网页来说,如何优化你的LCP应该是一个比较好理解的问题。
首次输入延迟(FID)测量交互中的任何延迟,对大多数网站来说似乎不是一个问题。优化这个问题通常是清理(或减少!)你的JavaScript,而且通常是针对具体网站的。这并不是说解决这两个指标的问题很容易,但它们是相当好理解的问题。
CLS不同的一个原因是,它是通过页面的生命周期来测量的--这就是名称中的 "累积 "部分其他两个核心网络指标在页面上发现主要组件后就停止了(对于LCP),或对于第一次互动(对于FID)。这意味着我们传统的基于实验室的工具,如Lighthouse,往往不能完全反映CLS,因为它们只计算最初的加载CLS。在现实生活中,用户会向下滚动页面,可能会有更多的内容落入导致更多的转移。
CLS也是一个人为的数字,它是根据页面的移动程度和频率来计算的。LCP和FID是以毫秒为单位的,而CLS是一个无单位的数字,通过复杂的计算输出。我们希望页面是0.1或以下,以通过这个核心网络活力。任何高于0.25的数值都被视为 "差"。
由用户互动引起的转变不被计算在内。这被定义为在一组特定的用户互动中的500ms内,尽管指针事件和滚动被排除在外。假设用户点击一个按钮,可能会期望内容出现,例如展开一个折叠的部分。
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注入的内容,但有一些不同的选项可供网页开发者使用。
在图片和iFrame上设置宽度和高度
我以前写过这个,但你能做的减少CLS的最简单的事情之一是确保你在你的图像上设置width 和height 属性。如果没有这些属性,图片在下载后会导致后续内容的移动,为其让路。

这只是一个简单的问题,将你的图像标记从:
<img src="hero_image.jpg" alt="...">
到。
<img src="hero_image.jpg" alt="..."
width="400" height="400">
你可以通过打开DevTools并将鼠标悬停在(或点选)该元素上来找到图片的尺寸。

我建议使用内在尺寸(这是图像源的实际尺寸),当你使用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 和0 默认值要好!
这也适用于本地懒惰加载的图像(尽管Safari默认不支持本地懒惰加载)。
新的aspect-ratio CSS属性
上面计算响应式图像高度的width 和height 技术,可以使用新的CSSaspect-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: attr(width) / attr(height); ,根据元素属性动态地设置aspect-ratio ,但遗憾的是,这还不被支持。
或者你甚至可以在你创建的某种自定义控件的<div> 元素上使用aspect-ratio ,以使其具有响应性。
#my-square-custom-control {
max-width: 100%;
height: auto;
width: 500px;
aspect-ratio: 1;
}
<div id="my-square-custom-control"></div>
对于那些不支持aspect-ratio 的浏览器,你可以使用较旧的padding-bottom hack,但是,由于较新的aspect-ratio 的简单性和广泛的支持(特别是一旦这从Safari技术预览版转到常规Safari),很难证明这种较旧的方法是合理的。
Chrome是唯一能将CLS反馈给谷歌的浏览器,它支持aspect-ratio ,这意味着在核心网络生命力方面将解决你的CLS问题。我不喜欢把指标放在用户之上,但事实上,其他Chromium和Firefox浏览器都有这个功能,Safari也有望很快实现,而且这是一个渐进式的改进,这意味着我认为我们已经到了可以抛弃底部填充的黑客,写出更干净的代码的时候。
自由使用min-height
对于那些不需要响应式尺寸而需要固定高度的元素,可以考虑使用min-height 。例如,这可能是一个固定高度的标题,我们可以像往常一样使用媒体查询为不同的断点设置不同的标题。
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 元素产生偏移,因为 margin 算作该元素的一部分,所以当它被移除时,会出现偏移(尽管它在屏幕上实际上并没有移动)。然而,至少它下面的内容不会被移位,所以应该减少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"> ),但对JavaScript驱动的动态菜单则用一个<button> 。这些元素的样式看起来是一样的,但是回退链接比按钮小了几个像素!这太小了。
这一点非常小,而且JavaScript通常启动得非常快,所以我没有注意到它的问题。然而,Chrome浏览器在计算CLS时注意到了这一点,由于它位于页眉,它将整个页面向下移动了几个像素。因此,这对CLS得分产生了相当大的影响--足以将我们所有的页面打入 "需要改进 "的类别。
这是我的一个错误,修复方法只是为了使这两个元素同步(也可以通过在页眉上设置min-height ,如上所述),但它让我有点困惑。我相信我不是唯一一个犯这种错误的人,所以要注意在没有JavaScript的情况下,页面的渲染效果。不认为你的用户禁用了JavaScript?[你的所有用户在下载你的JS时都是不使用JS]的。
网络字体导致布局偏移
网络字体是造成CLS的另一个常见原因,因为浏览器最初是根据回退字体来计算所需空间,然后在下载网络字体时重新计算。通常情况下,CLS很小,如果使用的是同样大小的后备字体,那么它们通常不会造成足够的问题而导致Core Web Vitals失败,但它们还是会让用户感到不安。

不幸的是,即使预装网络字体也无济于事,因为虽然这减少了后备字体的使用时间(所以对加载性能有好处--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中的字体加载API,这比较复杂,但这个即将推出的选项可能最终会给我们提供一个更容易的解决方案,而且更有可能获得支持。
客户端渲染页面的初始模板
许多客户端渲染的页面,或单页应用程序,只使用HTML和CSS渲染一个初始的基本页面,然后在JavaScript下载和执行后 "水化 "模板。
这些初始模板很容易与JavaScript版本不同步,因为新的组件和功能被添加到应用程序的JavaScript中,但没有添加到首先渲染的初始HTML模板。这样一来,当这些组件被JavaScript注入时,就会导致CLS。
因此,审查你所有的初始模板以确保它们仍然是好的初始占位符。如果初始模板由空的<div>,那么就使用上述技术来确保它们的大小合适,以尽量避免任何偏移。
此外,与应用程序一起注入的初始div ,应该有一个min-height ,以避免在初始模板插入之前,它的初始高度就被渲染为0。
<div id="app" style="min-height:900px;"></div>
只要min-height 大于大多数视口,这应该可以避免网站页脚的任何CLS,比如说。CLS只有在视口中才会被测量,因此会影响用户。默认情况下,一个空的div ,高度为0px,所以给它一个min-height ,更接近应用加载时的实际高度。
确保用户互动在500ms内完成
导致内容转移的用户互动不包括在CLS评分中。这些被限制在互动后的500毫秒内。因此,如果你点击一个按钮,并做一些复杂的处理,需要超过500毫秒,然后渲染一些新的内容,那么你的CLS分数就会受到影响。
你可以在Chrome DevTools中查看是否排除了这种转变,方法是使用性能标签来记录页面,然后找到转变,如下图所示。打开DevTools,进入非常令人生畏的(但一旦你掌握了它就非常有用!)性能选项卡,然后点击左上方的记录按钮(在下图中被圈起来),与你的页面互动,完成后停止记录。

你会看到一个页面的电影片段,其中我加载了另一篇Smashing杂志文章的一些评论,所以在我圈出的部分,你几乎可以看到评论的加载和红色页脚被移到屏幕外。在 "性能"选项卡的更下方,在 "体验"行下,Chrome会在每一次移动中放置一个红粉色的方框,当你点击该方框时,你会在下面的 "摘要"选项卡中获得更多细节。
在这里你可以看到,我们得到了一个巨大的0.3359分--远远超过了我们的目标是低于0.1的阈值,但累计得分没有包括这个,因为最近的输入被设置为使用。
确保交互只在500毫秒内转移内容与首次输入延迟试图测量的内容相接壤,但在有些情况下,用户可能会看到输入产生了效果(例如显示了一个加载旋钮),所以FID是好的,但内容可能直到500毫秒限制之后才被添加到页面,所以CLS是坏的。
理想情况下,整个交互过程将在500毫秒内完成,但你可以做一些事情,在处理过程中使用上述技术预留必要的空间,这样,如果它确实需要超过神奇的500毫秒,那么你已经处理了这个转变,所以不会因此受到惩罚。这在从网络上获取内容时特别有用,因为网络可能是可变的,不在你的控制范围内。
其他需要注意的项目是需要超过500ms的动画,因此会影响CLS。虽然这看起来有点限制性,但CLS的目的不是限制 "乐趣",而是为用户体验设定合理的期望值,我认为期望这些动画耗时500毫秒或更少是不现实的。
同步的JavaScript
我将要讨论的最后一项技术有点争议,因为它违背了众所周知的网络性能建议,但在某些情况下,它可能是唯一的方法。基本上,如果你知道你的内容会引起偏移,那么避免偏移的一个解决方案就是在它稳定下来之前不要渲染它!下面的HTML将隐藏。
下面的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>
现在,正如我所说的,我确信这将使一些网络性能人员感到害怕,因为建议是在JavaScript上使用async, defer 或更新的type="module" (默认情况下是defer-ed),以避免阻塞渲染,而我们在这里做的是相反的!然而,如果内容不能被预先确定,而且会导致令人震惊的转变,那么提前渲染就没有什么意义了。
我在一个cookie横幅上使用了这种技术,该横幅在页面顶部加载,并将内容向下移动。

这需要读取一个cookie,看是否显示cookie横幅,虽然这可以在服务器端完成,但这是个静态网站,没有能力动态地改变返回的HTML。
Cookie横幅可以通过不同的方式实现,以避免CLS。例如,将它们放在页面的底部,或将它们覆盖在内容的顶部,而不是将内容向下移动。我们倾向于将内容保持在页面的顶部,所以不得不使用这种技术来避免移动。还有其他各种警报和横幅,由于各种原因,网站所有者可能更喜欢放在页面的顶部。
我还在另一个页面上使用了这种技术,在这个页面上,JavaScript将内容移动到 "主 "和 "旁 "两栏中(由于我不说的原因,不可能在HTML服务器端正确构建这个内容)。再次隐藏内容,直到JavaScript重新安排内容,然后才显示出来,避免了拖累这些页面CLS得分的CLS问题。而且,即使JavaScript由于某种原因没有运行,内容也会自动解除隐藏,并显示出未转移的内容。
使用这种技术可能会影响其他指标(尤其是LCP和First Contentful Paint),因为你在延迟渲染,而且还有可能阻止浏览器的前瞻预加载器,但这是在没有其他选择的情况下可以考虑的另一种工具。
结论
累积布局偏移是由内容的尺寸变化,或者新的内容被延迟运行的JavaScript注入到页面中造成的。在这篇文章中,我们已经讨论了避免这种情况的各种技巧和方法。我很高兴Core Web Vitals对这一恼人问题的关注--长期以来,我们网站开发者(我肯定也包括我自己)都忽视了这个问题。
清理我自己的网站已经为所有的访问者带来了更好的体验。我鼓励你也看看你的CLS问题,希望这些提示中的一些在你做的时候会有用。