设计网页上的加载状态经常被忽视或作为事后才想到的东西。性能不仅仅是开发人员的责任,构建一个能够在慢速连接下正常工作的体验也是一项设计挑战。
虽然开发人员需要关注诸如缩小和缓存之类的事情,但设计师必须考虑 UI 在“加载”或“离线”状态下的外观和行为。
速度的错觉
随着我们对移动体验的期望发生变化,我们对性能的理解也在发生变化。人们期望网页应用程序感觉起来与原生应用程序一样敏捷和响应迅速,无论其当前网络覆盖范围如何。
感知性能 是衡量某件事对用户感觉有多快的指标。其理念是,如果用户知道发生了什么,并且可以在内容实际出现之前预料到内容,那么他们会更有耐心,并且会认为系统更快。这很大程度上是关于管理期望和让用户保持知情。
对于网页应用程序,这个概念可能包括显示文本、图像或其他内容元素的“模型”——称为骨架屏幕 💀。您可以在现实世界中找到这些内容,被 Facebook、Google、Slack 等公司使用。


示例
假设您正在构建一个网页应用程序。它是一种旅行建议类型的应用程序,人们可以在其中分享他们的旅行并推荐地点,因此您的主要内容可能看起来像这样。

您可以将该卡片简化为其基本视觉形状,即 UI 组件的骨架。

每当有人从服务器请求新内容时,您都可以立即开始显示骨架,同时在后台加载数据。内容准备就绪后,只需将骨架替换为实际卡片即可。这可以通过纯 JavaScript 或使用 React 之类的库来完成。
现在,您可以使用图像来显示骨架,但这将引入额外的请求和数据开销。我们已经在加载内容,因此等待另一个图像先加载不是一个好主意。此外,它没有响应能力,如果我们将来决定调整内容卡片的一些样式,我们将不得不将更改复制到骨架图像中,以便它们再次匹配。😒 糟糕。
更好的解决方案是仅使用 CSS 创建整个内容。没有额外的请求,最小的开销,甚至没有额外的标记。而且我们可以通过一种方式来构建它,这样将来更容易更改设计。
在 CSS 中绘制骨架
首先,我们需要绘制构成卡片骨架的基本形状。我们可以通过向 background-image
属性添加不同的 渐变 来做到这一点。默认情况下,线性渐变从上到下运行,具有不同的颜色停止过渡。如果我们只定义一个颜色停止点并将其余部分保留为透明,我们就可以绘制形状。
请记住,多个背景图像在此处叠加在一起,因此顺序很重要。最后一个渐变定义将在后面,第一个在前面。
.skeleton {
background-repeat: no-repeat;
background-image:
/* layer 2: avatar */
/* white circle with 16px radius */
radial-gradient(circle 16px, white 99%, transparent 0),
/* layer 1: title */
/* white rectangle with 40px height */
linear-gradient(white 40px, transparent 0),
/* layer 0: card bg */
/* gray rectangle that covers whole element */
linear-gradient(gray 100%, transparent 0);
}
这些形状像常规块级元素一样拉伸以填充整个空间。如果我们想改变这一点,我们将不得不为它们定义显式尺寸。background-size
中的值对设置每个图层的宽度和高度,保持我们在 background-image
中使用的相同顺序。
.skeleton {
background-size:
32px 32px, /* avatar */
200px 40px, /* title */
100% 100%; /* card bg */
}
最后一步是在卡片上定位元素。这就像 position:absolute
一样,值表示 left
和 top
属性。例如,我们可以模拟头像和标题的 24px 填充,以匹配真实内容卡片的外观。
.skeleton {
background-position:
24px 24px, /* avatar */
24px 200px, /* title */
0 0; /* card bg */
}
使用自定义属性拆分它
这在一个简单的示例中效果很好——但如果我们想构建一些稍微复杂的东西,CSS 会很快变得混乱且难以阅读。如果另一个开发人员获得了该代码,他们将不知道所有这些神奇数字是从哪里来的。维护它肯定很糟糕。
谢天谢地,我们现在可以使用 自定义 CSS 属性 以更简洁、更适合开发人员的方式编写骨架样式——甚至考虑到不同值之间的关系。
.skeleton {
/*
define as separate properties
*/
--card-height: 340px;
--card-padding:24px;
--card-skeleton: linear-gradient(gray var(--card-height), transparent 0);
--title-height: 32px;
--title-width: 200px;
--title-position: var(--card-padding) 180px;
--title-skeleton: linear-gradient(white var(--title-height), transparent 0);
--avatar-size: 32px;
--avatar-position: var(--card-padding) var(--card-padding);
--avatar-skeleton: radial-gradient(
circle calc(var(--avatar-size) / 2),
white 99%,
transparent 0
);
/*
now we can break the background up
into individual shapes
*/
background-image:
var(--avatar-skeleton),
var(--title-skeleton),
var(--card-skeleton);
background-size:
var(--avatar-size),
var(--title-width) var(--title-height),
100% 100%;
background-position:
var(--avatar-position),
var(--title-position),
0 0;
}
这不仅更易读,而且以后更改某些值也更容易。此外,我们可以使用一些变量(比如 --avatar-size
、--card-padding
等)来定义实际卡片的样式,并始终使其与骨架版本保持同步。
现在,添加媒体查询以在不同的断点处调整骨架的某些部分也很简单。
@media screen and (min-width: 47em) {
:root {
--card-padding: 32px;
--card-height: 360px;
}
}
自定义属性的浏览器支持 很好,但并非 100%。基本上,所有现代浏览器都支持,IE/Edge 稍微晚了一些。对于这个特定的用例,添加使用 Sass 变量的回退很容易。
添加动画
为了使它变得更好,我们可以为我们的骨架添加动画,使其看起来更像一个加载指示器。我们需要做的就是在顶层放置一个新的渐变,然后使用 @keyframes
为其位置添加动画。
这是完成的骨架卡片可能看起来如何的完整示例。
骨架加载卡片 由 Max Böck (@mxbck) 在 CodePen 上创建。
您可以使用 :empty
选择器和伪元素来绘制骨架,因此它只适用于空卡片元素。一旦内容被注入,骨架屏幕将自动消失。
更多关于为性能设计的信息
要详细了解为感知性能设计,请查看以下链接。
- 设计师 VS. 开发人员 #8: 为出色性能设计
- Vince Speelman: 设计的九个状态
- Harry Roberts: 使用多个背景图像提高感知性能
- Sitepoint: 设计师的感知性能指南
- Manuel Wieser: 主导颜色延迟加载
如果您没有定义宽度和高度,是否仍然应该至少定义一个高度以实现响应性?然后让其余部分在尺寸变小后隐藏(被截断)?
或者调整每个媒体查询会更好吗?
使用
:empty
很不错,我喜欢它。嗨,Chris!
您确实需要高度来计算
background-position
,但您可以轻松地用百分比替换固定宽度。或者,您可以执行类似calc(100% - var(--card-padding) * 2)
的操作来“伪造”块级元素。查看此快速分支以获得响应式版本。
@Max 太棒了!我可能必须为我正在开发的网页应用程序尝试一下这种技术,我们有一些 Ajax 调用,这将非常适合。感谢您的文章和后续 CodePen!
解释得很好。感谢分享这篇文章。
虽然我喜欢骨架元素的概念,但我一直觉得如果你需要在其中一个元素中使用动画,你的“感知”性能仍然很糟糕,因为用户仍然需要等待相当长的时间才能看到实际内容。我总是尝试让后端响应在任何 JavaScript 操作中保持在 100ms 以内。
@Max 感谢你提供的很棒的代码片段。我会在我的几个即将到来的项目中使用它。
太棒了!很高兴它对你有用。
在使用 :empty 伪类时,你不需要额外的伪元素,比如 ::after。只要容器没有任何子节点,选择器就会匹配,你可以使用骨架屏幕来设置容器的背景,从而消除不必要的复杂性。
我一年多前构建了相同的演示:https://codepen.io/oslego/pen/XdvWmd
此外,你应该解释径向渐变中 99% 的 hack。它在 Chrome 中不再需要(bug 已修复),但人们会感到困惑。
虽然动画很酷,但我建议避免它,特别是对于大型表面渐变。它会导致浏览器不断重绘,从而降低性能。要检查:打开 Chrome 开发工具,切换到渲染面板(在最下面的控制台旁边),然后勾选“绘制闪烁”复选框。
@Razvan 和/或 @Max,
首先,谢谢,这真是太棒了。
其次,有人能解释一下 99% 的 hack,或者提供一个解释的链接,关于 bug 修复的一些文档,或者只是解释一下那里发生了什么吗?
您好 Schuyler,
基本上,你希望径向渐变具有清晰的边缘,而不是默认的混合效果。
正如 Razvan 正确指出的那样,Chrome 中存在一个 bug(Firefox 和 Safari 中仍然存在),径向渐变使用 100% 和透明 0 颜色停止点不会渲染成圆形,而是会填充整个空间。99% 的 hack 是解决此问题的一种方法。
你也可以写成
radial-gradient(circle $radius, $color 99%, transparent 100%)
。它的边缘并不完全平滑,但足够接近;)哦,明白了。感谢你的解释!
我确实理解了这个概念,但动画部分没有解释清楚。也许作者假设他的读者了解动画。
在尝试理解关键帧部分时,我意识到 CSS 变量是解决问题的好方法,但有时你需要精确的值,而不需要锁定变量。
很棒的解释。这很好地展示了如何将几个简单的东西(多个背景、CSS 属性、简单的过渡和动画)结合起来,创建一个印象深刻的解决方案。
我更改了这个值,以使动画更流畅,而不是看起来很生硬。感谢您提供的
background-position
-200% 0
但这是一场输掉的比赛,不是吗?
我们用来创造加载感知的技巧正在变得过时,技巧本身变得和原始等待时间一样令人讨厌。
我们引入了旋转器和进度条,以便为用户创造一种活动的感觉,而不是给他们一个空白的空间。但现在旋转器已经不够了。为什么呢?很简单。用户习惯了旋转器,并意识到它是一个闪闪发光的东西,旨在分散他们的注意力。旋转器和空白屏幕变得如出一辙。
骨架屏幕也将遭遇同样的命运。它们相当普遍,但谁看到这种效果不会意识到发生了什么?
唯一有意义的是实际内容。也许页面可以在后台加载一些内容,并在用户等待时立即显示出来。这似乎是一个很好的机会,可以向用户提供促销信息或号召性用语。
或者就用旋转器吧。
感知即现实。这模拟了内容的形状,所以它比旋转器更好(就目前而言)。当你等待朋友来接你时,人们总是会沿着路看,看看车来了没有,而不是只看手表。
您好 Nuwanda,
你说得对,骨架屏幕的概念并不新鲜,我认为大多数人都曾在某个地方见过它,例如 Facebook。没有人真的被“欺骗”以为这不是加载状态,它只是一种类型的旋转器,传递了更多信息。
然而,我们的大脑以这样的方式运作,即对内容的期待让人感觉比盯着旋转的圆圈更好。这就是感知性能的全部意义。
当然,这不是跳过实际性能优化的借口,如果你能缓存有意义的内容并显示出来,那就太好了!