2284 字
约 7 分钟
14
S14-A22 · 骨架屏:它让"等待"变短,还是让"跳出"变多

一个列表页加了骨架屏(在数据返回前显示"灰色占位块"),期望改善"感知性能"。上线后 A/B 显示:首屏时间的指标没变,而"跳出率"上升了 4%——因为骨架屏"看起来像内容",用户以为"已经加载完",滑动后"什么都没有",于是离开。

骨架屏是"感知优化",而它的效果取决于"骨架是否与最终内容同形"。形状不符时,它比"空状态"更糟——这个"误导"的机制是"骨架与内容共享视觉语言":灰块看起来像"文字的占位",于是用户"按颜色深浅判断加载状态"——而骨架的深浅与真实内容太接近时会失效。
因为它制造了"内容已就绪"的误导。

1. 骨架屏解决的三个问题

问题 骨架屏是否有效
白屏(无反馈) 有效(有视觉反馈)
布局跳动(CLS) 有效(占位固定尺寸)
感知等待 有效(有"在进行"的感觉)

第二项是"最容易被忽略的真实收益":**骨架屏的"固定高度"能避免"内容进来时整页跳一下"**第二项的"量化指标"就是 CLS:内容进来时"下方元素被推下去"的距离。骨架把这段距离"提前占住",于是 CLS 接近 0——而这一点在"移动端"比桌面端更明显。
——而这一点与"好看"无关,是纯技术收益。

2. 三种"骨架无效/更差"的情形

情形 原因
形状与内容不符 用户以为加载完了
骨架停留太久(> 3s) 焦虑上升("卡住了")
骨架有"假内容"(灰条像文字) 用户去读,发现是占位

第二项有实验支撑:进度不确定时,"看起来很忙"的动画能降低焦虑;而"静止的骨架"超过 2–3 秒会让焦虑上升——因为"停住"等于"卡死"。第二项的"另一个前提"是"骨架要"看起来在动"":静止的灰块与"加载完成但内容为空"在视觉上没有差别——于是"用户以为没数据",而不是"以为在加载"。

// 三种"等待反馈"的适用时长
spinner(转圈):< 1s(短任务,不需要结构)
skeleton(骨架):1–3s(已知结构,即将有内容)
progress bar(进度条):> 3s(长任务,需要"还剩多少")
// 反例:骨架用 10s → 用户以为是内容,反复滑动、然后离开

3. "同形"的三个要求

要求 说明
尺寸一致 骨架块与真实内容的高度/列数相同
数量一致 骨架的条数 = 首屏条数
布局一致 图文位置不能变

第二项常被漏:骨架显示 5 条,实际只返回 2 条第二项的"修法"有两种:骨架条数 = min(首屏条数, 预期返回数),或"内容少时收缩骨架的高度"——前者更简单,后者更平滑。
——于是"内容进来时下面空了一大片",比"直接显示 2 条"更突兀。

"延迟显示"是"避免闪烁"的标准做法,而它需要一个"计时器 + 两种状态":

// 三段状态:idle(不显示)→ skeleton(>300ms 才显示)→ content
1 请求开始:启动 300ms 定时器
2 数据先回来:取消定时器,不显示骨架(无闪烁)
3 定时器先到:显示骨架(有反馈),等数据
// 关键:定时器与"取消"要成对,否则会"数据已到而骨架仍亮着"

4. 三种"实现方式"的代价

方式 代价
手写骨架组件 需与真实组件"同步维护"
由真实组件"降级"渲染(数据为空时) 天然同形,但需组件支持
自动生成(按 CSS 推测) 准确率有限

第二项的"实现要点"是"共用布局常量":行高、列数、内边距都写成常量,骨架与真实内容各引用一次——于是"改一处两边同步"。
第二项是"最省心"的:同一个组件在"数据为空"时渲染"骨架态",于是"形状天然一致"——而"维护成本"从"两套"降到"一套"。

// 由真实组件降级渲染(同形的最省做法)
function Row({ item }) {
  if (!item) return <SkeletonRow />;      // 骨架态与真实态共用布局常量
  return <div className="row">...</div>;
}
// 关键:骨架与真实行都用同一个 ROW_HEIGHT 常量,改一处两边同步

5. 怎么判断"骨架有用"

指标 读法
感知性能(用户评分) 直接
跳出率/回访 变差要警惕
CLS(布局偏移) 应下降
首屏可交互时间 不该变差

第二项是"这轮教训的来源":骨架屏可能"改善感知"但"增加跳出"第二项的"读法"要分群:"新用户跳出上升"与"老用户不受影响" 是两种不同的信号——前者说明"误导",后者说明"变化可接受"。
——因为"误以为加载完"导致"滑动后发现空"。

第三项的"实测方式"很直接:DevTools 的 Layout Shift 高亮会圈出"跳动的元素"——若骨架阶段没有高亮而内容阶段有,说明"骨架没占住位"。
第三项是"技术上应该改善"的:如果 CLS 没降,说明骨架尺寸与内容不符。

"骨架与真实内容"的高度可以用一个常量表对齐,避免"两处改动不同步":

// 布局常量(骨架与内容都引用)
ROW_HEIGHT = 72px
ROW_GAP = 12px
FIRST_SCREEN_ROWS = 5
// 骨架高度 = ROW_HEIGHT × 条数 + GAP × (条数-1)
// 内容高度由同一常量决定 → 天然一致

6. 排错:五种"骨架相关问题"

现象 成因 处理方向
用户以为加载完了 形状/数量不符 同形 + 数量一致
内容进来时跳动 尺寸不同 固定高度常量
骨架停很久 数据慢 超时后转进度条
跳出率上升 误导 加"加载中"文案
维护两套组件 手写骨架 用降级渲染

第四项很便宜:**在骨架上方加一行"加载中…"**第四项的"替代"是"骨架带微光动画 + 一行小字":文字负责"说明状态",动画负责"说明在动"——两者分工,比"只加一个"更清楚。
,成本几乎为零,而它能消除"以为是内容"的误判。

最后一个"无障碍"写法,成本两行:

<!-- 骨架的无障碍表达 -->
<div aria-busy="true" aria-live="polite">
  <div class="skeleton" aria-hidden="true"></div>
  <span class="sr-only">内容加载中</span>
</div>
<!-- 收益:读屏用户听到"内容加载中",而不是一串空块 -->

7. 边界:什么情况不必用骨架

场景 为什么 做法
加载 < 500ms 骨架一闪而过(闪烁) 不用,或延迟 300ms 再显示
内容形状不固定 无法同形 用 spinner
首屏是"表单/空状态" 无可占位结构 不用
已有 SSR(内容已就位) 无"等待" 不用

第一项的"实现"是一个定时器:数据在 300ms 内返回就"不显示骨架",超过才显示——于是"快的情况"完全无闪烁。
第一项是"技术细节":骨架闪现 100ms 会让页面"闪一下",比"直接出内容"更差——所以"延迟 300ms 再显示骨架"是常见做法。

判断口径一句话:"这段等待是否'结构已知、即将有内容、且超过 1 秒'"。最后一条与"无障碍"有关的边界:骨架应当对屏幕阅读器"不可读"(aria-hidden),并用 aria-busy 表达"正在加载"——否则它会念出一串"空块"。
三者都成立,骨架有用;任一不成立(太快、形状不定、内容已在),它会带来负面影响。


8. 自测

  1. 骨架屏解决的三个问题里,哪一个是"纯技术收益"?它与"好看"无关在哪里?
  2. 为什么"静止的骨架"超过 2–3 秒会让焦虑上升?三种等待反馈的时长分界是什么?
  3. "同形"的三个要求里,哪一项常被漏?它会造成什么?
  4. "由真实组件降级渲染"为什么最省心?它靠什么保证形状一致?
  5. 为什么"骨架屏可能改善感知但增加跳出"?
  6. 判断"该不该用骨架"的那三个条件是什么?
S14-A22 · 骨架屏:它让"等待"变短,还是让"跳出"变多
https://www.clxhxhhr.top/posts/6615/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。