等待感从哪来:骨架屏要解决的问题

用户对慢的感知,往往不是来自「真的慢」,而是来自「不知道要等多久」。一个白屏干等三秒,和一个带着占位骨架的过渡三秒,主观感受天差地别。骨架屏要解决的,就是把这段空白等待变成一次有结构、可预期的过渡。

骨架屏的思路很朴素:数据还没回来,先把页面的轮廓画出来。用灰色块占位标题、图片、列表的位置,等数据到了再无缝替换成真实内容。用户看到的是页面「逐渐被填满」,而不是从空白突然蹦出一堆字。

好的加载体验不是让人等得更短,而是让人等得不慌。
本文核心知识点
  • 骨架屏用占位块预渲染页面轮廓,消除白屏焦虑。
  • 预加载在合适的时机提前拉数据,等用户点开时数据已就绪。
  • 分层缓存把数据按新鲜度分档,弱网下优先用旧数据兜底。
  • 降级弱网或失败时给出明确反馈,而不是无限转圈。
小程序骨架屏与弱网体验示意:手机屏幕上灰色占位骨架与加载环、缓存缓存层组成的蓝调扁平科技插画
把等待结构化:骨架屏负责过渡,预加载负责抢时间,缓存负责兜底。

骨架屏的两种实现方式

落地骨架屏,常见有两条路。一条是在页面里手写一套占位结构,数据到位后切换显示;另一条是借助组件或自动化工具,根据真实结构生成占位。对中小项目,手写一套往往更可控。

// 列表页骨架屏:用 loaded 标志切换占位与真实列表
data() {
  return { loaded: false, list: [] };
},
onLoad() {
  wx.request({ url: "/api/list" }).then(res => {
    this.setData({ list: res.data, loaded: true });
  });
}

骨架屏的关键不是「有占位」,而是占位要长得像真页面:

  • 占位块的位置、高度、圆角和真实卡片对齐,切换时才不跳。
  • 给骨架加一点轻微的呼吸动画,告诉用户「在动」。
  • 占位的条目数按实际列表长度估,别写死。

切换那一刻是体验的分水岭:如果骨架和真实内容对不齐,替换时页面会抖一下,前面的努力就白费了。

预加载与数据预热

骨架屏只是把等待变好看,预加载才是真正把等待变短。思路是:在用户还没点进去之前,就把下一页的数据先拉回来。

// 首页 onShow 时,顺手预拉详情页会用到的公共数据
onShow() {
  if (!wx.getStorageSync("hotReady")) {
    wx.request({ url: "/api/hot-list" }).then(res => {
      this.setData({ hotList: res.data });
      wx.setStorageSync("hotReady", true);
    });
  }
}

几个分寸要拿捏好:

  • 预加载只做用户「大概率下一步」会用到的数据,别什么都抢。
  • 把预加载放在页面空闲时机,别和首屏渲染抢资源。
  • 预拉的数据放进缓存,下次进入直接命中,连请求都省了。

缓存策略:分层与过期

弱网体验的核心,是「即使网不好,也能给用户看到点东西」。这就要求你对不同数据采用不同的缓存策略,而不是一刀切每次都重新请求。

数据类型新鲜度要求缓存策略
用户个人信息高进入时刷新,本地仅兜底
列表内容中缓存几分钟,过期后台静默更新
字典/配置低长缓存,版本号变化才更新
// 带过期时间的缓存读取:先给旧数据,后台再悄悄更新
function getCached(key, fetchUrl) {
  const hit = wx.getStorageSync(key);
  if (hit && (Date.now() - hit.time) < 5 * 60 * 1000) {
    return hit.data; // 缓存未过期,直接用
  }
  wx.request({ url: fetchUrl }).then(res => {
    wx.setStorageSync(key, { data: res.data, time: Date.now() });
  });
  return hit ? hit.data : null; // 过期则先返回旧的兜底
}

这套「先旧后新」的模式,是弱网体验的精髓:用户永远先看到内容,新鲜度由后台悄悄补齐。

弱网下的降级与兜底

再好的缓存也有缓存空的时候——第一次打开、数据过期又完全断网。这时候要明确告诉用户「现在不行」,而不是让一个 loading 无限转下去。

  • 超时要短:请求设一个合理超时,别让用户干等二十秒。
  • 失败要友好:给出「网络不太顺,点我重试」,而不是一句冰冷的报错。
  • 有旧数据就展示旧数据,上面标一句「内容更新于某时」。
wx.request({ url: "/api/list", timeout: 8000 })
  .then(r => render(r.data))
  .catch(() => {
    this.setData({
      showError: true,
      fallback: wx.getStorageSync("listCache") || null
    });
  });

把「空态、加载态、错误态」这三种状态都设计好,弱网体验才算真正闭环。