骨架屏(skeleton screen),每个前端都爱用。它不仅能提升用户体验,制作起来也简单,数据上(FP,首次渲染)也好看,基本上是无脑上不会有错的。
如今大部分关于骨架屏的探讨在于如何借助工具来快速生成骨架屏,虽然这种流水线式的骨架屏即使不借助工具,也不会耗费开发太多时间。反而大家不太会去关注,骨架屏应该做成什么样,它的底层逻辑是什么,什么是好的骨架屏?
本文通过回顾骨架屏的前世今生,并结合货拉拉的真实案例,来探讨下骨架屏背后的思想,以及如何做出更好的骨架屏。
打破沉寂
界面没有反馈,会让用户觉得不安,这是一个自人机交互诞生以来就存在的问题。这在如今移动互联网场景下,就是我们常说的「白屏」问题。所以,「自古」以来,遇到需要用户等待的场景,都要搞点小动画来打破尴尬,例如大家熟悉的「无限滚动条」和「转圈圈」:
早期的货拉拉:
一方面告诉用户「我还在运转,没有出故障」,另一方面吸引用户注意力,让等待的时间不那么无聊。
骨架屏诞生
但是时间久了,大家便不满足于这些小动画,它既不反映实际的加载进度,和后面要出现的内容也毫无关系,如果停留时间过久反而让用户更加疲惫,觉得时间过得更慢了。随着移动互联网时代到来,人们对体验越来越重视,于是体验更好的「骨架屏」就被发明了出来。
根据当前能查到的资料,最早介绍骨架屏的文章出现于 2013年:LukeW | Mobile Design Details: Avoid The Spinner
顾名思义,在数据和资源还在加载中时,先展示一个页面的「骨架」,让用户知道接下来的页面大致长什么样,然后逐步的填充里面的内容。一般来说,应用在加载数据和资源时比较耗时,但是页面的框架结构是可以预先确定好的,把这部分信息通过骨架屏尽快传达给用户,让用户感受到页面的进展,产生「页面马上就好了」的预期(为何我联想到了PDD再砍一刀),同时也使整个加载过程更连贯,界面变化不那么突兀。
总结一下,骨架屏的优势在于:
- 及时反馈,让用户感受到进展。
- 连贯性,细粒度变化,不突兀。
粗糙的骨架屏
以其独特的优势,骨架屏迅速开始流行起来,并且催生了看似丰富的生态。
一些 UI 库提供了现成的骨架屏组件,实现了「拿来即用」。
有人觉得组件库还是太麻烦,于是开发了自动化工具,可以根据现有的页面自动生成与之匹配的骨架屏。
这些眼花缭乱的「生态」吸引了大部分人的注意力,以至于在谈论到骨架屏时,大家都在比拼如何「快速」、「省事」地给应用装上一个骨架屏。
案例1:
一个应用原来的加载过程是:
空白 -> 转圈圈 -> 内容
老板提议用骨架屏优化一下,开发用 UI 组件库很快速的就搞定了,于是加载过程变成了:
空白 -> 转圈圈 -> 骨架屏 ->内容
老板:???为什么还有转圈圈
开发:你就说我做的快不快吧!
老板:能不能把转圈圈也搞成骨架屏啊?
开发:这个骨架屏是个 react 组件,你懂不?react 得在 mount 之后才能看到,mount 之前还是得用转圈圈......
老板:我虽然不懂 react 但我略懂 html,你直接把骨架屏放做到原生 html 里不行吗?
开发:得加时间...
可见,用了工具,就会受限于工具。骨架屏的意义之一就在于「以最快的速度给用户带来反馈」,好的做法是直接放到原生的 html 模板中。但是工具的泛滥让大家变懒了,于是变成了「以最快的速度应付老板的需求」。
案例2:
小程序的开发工具,也集成了自动生成骨架屏的功能:
生成的骨架屏类似这样:
😂 这能用?好吧没关系,我们基于它生成的代码进行修改,也能省不少事,打开代码看看:
额这🤮
小程序的包体积本来就有上限,这下雪上加霜。
...
finally 我们终于拿到了一个还不错的骨架屏,把它装上试试:
🤔感觉有点突兀呢,而且好像等的时间更长了?
之前是哪个接口先返回就先渲染对应的模块,虽然有点杂乱吧,但好歹是渐进式的,让人能先看到一些东西。用了这个所谓「骨架屏」,反而要等到最慢的接口返回以后,才「啪的一下」全部出现。
这与其说是骨架屏,不如说是「启动屏」(splash screen),还不如转圈圈,转圈圈起码不会把整个页面都挡住。
本来是想省事,结果更加费事,效果还没有达到预期。
好的骨架屏
「什么都是现成的只会害了你」,「无脑」让人丢掉了思考,忽视了骨架屏的本意。
回顾下骨架屏的核心思想:及时反馈、连贯性,让我们以此出发来做一个真正有效的骨架屏。
更细粒度的变化
可能骨架屏的「屏」字产生了误导,让大家以为骨架屏一定是覆盖了整个屏幕。其实更应该关注的是「骨架」,即整个加载流程是一个先有骨架然后逐步的填充的过程。
如下所示(为了展示效果,特意放慢了速度):
可以看出,我们把一个大的骨架「屏」拆分到了各个组件中,由各个模块独立控制自己的骨架屏,相较于原始的随接口实时加载,变化更加丝滑流程,让人产生确定的期望,不会有突兀感。
彩色骨架屏
骨架屏往往被设计成淡灰色,因为比较不显眼,不会占用用户过多注意力,后面无论被什么样的内容替换掉,都不会太突兀。
但如果要加载的内容本身有比较确定的颜色,那骨架屏也可以是彩色的。如下图,包括颜色和一些铁定不会变的文字,都可以是骨架屏的一部分:
这体现了骨架屏的「及时反馈」原则,只要是确定的,准备好的东西,就尽快的反馈给用户。
骨架屏动效
回顾前面说的「打破沉寂」,会动的画面会减少用户的焦虑,骨架屏如果再动起来是不是更好。
例如这种闪烁的光影,好像是比不会动的要好那么一点点?
但是这种闪烁的光影没有体现出连贯性,它只是一个大号的无限进度条或转圈圈。
一种比较好的做法是把骨架屏本身的加载也变成连贯的动画:
这个骨架屏有一个渐进出现的效果,你可能意识不到这个动画花费了400毫秒,用户往往会忽略这个时间,不认为是白等,因为他们观察到了页面在发生「进展」,这就是细粒度变化的好处。手机系统动画做得好,会让人觉得系统更流畅,也是这个道理(不信你把系统动画关了试试)。
终极骨架屏
至此,我们的骨架屏看起来已经很完美了,但,还不是最好。
骨架屏本身是一个过渡状态,是为了掩盖加载的时延。而且由于加载一个骨架本身就非常快,这会让一些性能指标非常好看,例如 FP(首次渲染),FCP(首次内容渲染)。但是用户看到骨架屏并不意味着页面加载完成,反而是「加载刚刚开始」。骨架屏不仅会麻痹用户也会麻痹开发,让他们忽略真正需要做的优化:优化接口响应速度,优化资源加载速度,优化程序运行效率,这些实实在在的优化将会使骨架屏的展示时间越来越短。
因此在上了骨架屏之后,并非就万事大吉了,骨架屏的终极目标应当是:「让骨架屏消失」。
真的能实现吗?无论怎么优化,毕竟还有网络、客户机性能等无法掌控的客观因素,让骨架屏真正消失是不现实的。那有没有办法让用户「感觉」骨架屏已经消失了呢?
一个案例:
这里看一个货拉拉小程序的例子,事先声明我并没有提前打开小程序放在后台,是真实的从头启动,且动图做了放慢处理:
请注意,在小程序弹出的一瞬间,就已经是一个「富内容」的界面,看起来就和真实页面一样。如顶部的业务导航,以及车型信息,通常是需要请求接口才能获取到的,是如何做到秒开的呢?
其实你看到的仍然是一个骨架屏,只不过是一个「富内容」的骨架屏。我承认这里用了一些数据缓存、首屏预渲染,甚至依赖了一些微信平台特有的能力,但这不是重点------即使不是微信平台,也有类似的办法------重点是骨架屏可以做到「以假乱真」。
用户「看到」界面,并不意味着他马上就要进行交互。骨架屏本质上是一个「视觉方案」,页面出现的越快,丰富度越高,用户就越满意,至于显示的内容是不是缓存,是否可交互,并没太大所谓,因为用户真正开始下手操作,还要等到几秒以后。
当然这种方案也要根据实际业务酌情考虑,如果缓存的内容和稍后要显示的真实内容差距过大,可能体验并不会很好(这里吐槽一下小红书和知乎,打开APP第一眼看到了想看的内容,然后瞬间给刷没了)。
骨架屏与量化指标
骨架屏往往被当成一种性能优化的方式,因此很多人希望通过量化指标来衡量它所带来的性能提升。但这里其实混淆了概念,骨架屏本身并不会提升性能,它属于一种「体验优化」。
它确实会提升首屏渲染时间,即 FP、FCP 指标。在有些性能统计报告里,FP、FCP 被当成重要的指标,这造成了一些误导。
例如在微信小程序的官方文档里,对「启动完成」的定义是:
在完成视图层代码注入,并收到逻辑层发送的初始数据后,结合从初始数据和视图层得到的页面结构和样式信息,小程序框架会进行小程序首页的渲染,展示小程序首屏,并触发首页的
Page.onReady
事件。......
小程序框架层面,以
Page.onReady
事件触发标志小程序启动过程完成。
可见,小程序把「页面初次渲染」当成了「启动完成」的标志。
众所周知,小程序分为「视图层」和「逻辑层」,而根据微信小程序关于启动流程的解释,视图层的初次渲染依赖逻辑层发送的初始数据。假如我们利用一个变量来控制骨架屏的展示,例如skeletonVisible
,一般初始值就是 true
,视图层在收到这个初始数据之后便会渲染骨架屏,然后触发 onReady
,也就是触发了「启动完成」。
凡是通过这种方式使用骨架屏的小程序,在后台看到的所谓「总启动耗时」都会非常好看,让人误以为小程序已经完全加载好,可以给用户使用了,而实际上它只是在记录骨架屏出现的时间。
微信之所以这么定义「启动完成」,应该这么理解:小程序的启动过程包括加载元数据、创建运行环境、下载代码包、代码包注入、应用逻辑初始化、首次路由等等一长串步骤,等到页面初始数据准备好,开始渲染视图的时候,可以认为「一切都已就绪,可以开始表演了」,至于首屏之后的事,那是你们自己写的代码逻辑,我们无法统计。
那么应该用什么指标来衡量真正的「启动完成」呢?
一个常用的指标是 LCP:
LCP(Largest Contentful Paint)最大内容绘制,在可视区域内,页面上的最大内容元素(如图片、视频或文本块)开始出现在屏幕上时触发。该时间会随着页面渲染变化而变化,因为页面中的最大元素在渲染过程中可能会发生改变,另外该指标会在用户第一次交互后停止记录
页面已经渲染了最多的内容,可以算作完成了吧。但是考虑这种情况,页面中有大量广告,一般是在主内容加载完成后才出现,而显然我们不用等广告全出现,就可以开始使用了。这时,LCP 时间会比我们实际期望的要滞后。
既然如此可以用TTI?
TTI(Time To Interactive:页面交互时间), 用于衡量网页实现完全可交互所需的时间。在以下情况下,网页会被视为完全互动网页:
- 网页会显示有用内容,内容的衡量依据是 FCP
- 大多数可见页面元素都会注册事件处理脚本,并且
- 网页会在 50 毫秒内响应用户互动。
但是如何定义页面可以开始交互了呢?哪怕页面上只出现了一个按钮,其他部分还在加载中------例如渐进式的骨架屏------也可以认为是可交互。 实际上 lighthouse 对 TTI 的计算方式非常苛刻和复杂,而且因为普适性不高,已从最新的标准中移除了。
在货拉拉小程序的实践中,我们创造了一个指标来衡量加载性能:当渲染首屏所需的数据都已准备好时,标记为「加载已完成」。这个指标是根据实际业务逻辑,在代码中手工记录的。你可能会说这个指标只考虑数据没考虑渲染,实际页面渲染完成的时间肯定要比这个时间要长。的确,严格意义上需要等页面渲染完成才能算「启动完成」。但我们记录这个指标的目的,是为了以此为参照进行性能优化,而性能优化的最主要的目标就是「尽快把数据准备好」。渲染耗时相对比较独立,可以另外专门优化。
所以,不要盲目的迷恋官方指标,而是要理解其具体的含义。指标的制定,需要结合实际需求,「什么都信官方的只会害了你」。
总结
骨架屏的诞生是为了提升用户体验,而由于开发成本较低,在业务中容易被滥用,反而影响了实际的体验。本文通过回顾骨架屏演变历史和一系列案例,想要告诉读者的是:骨架屏是一种「手段」,而「体验」才是其核心目的。要做好骨架屏,应当从「提升用户体验」出发,即时的反馈进展,细粒度、平滑的过渡,让加载的过程不突兀,不枯燥。这样你就会发现,骨架屏不是孤立的、一成不变的,它可以有更多样的表现形式,可以和其他优化点结合实现更好的效果。只要掌握了其核心的原则,想做出一个好的骨架屏就不是什么难事了。