给AI应用做骨架屏与流式加载态

4 阅读3分钟

AI应用的等待体验,和普通页面不一样。普通页面是"loading几百毫秒然后全出来",AI是"先愣两秒(首token延迟),然后慢慢吐字"。这两段得用不同的加载态去接,一刀切体验很差。

我把AI对话的等待拆成三个阶段,每个阶段配不同的视觉反馈。这篇讲具体怎么做。

三段式加载态

  1. 首页加载(拉历史、拉配置)→ 骨架屏
  2. 等首token(请求发出到第一个字)→ "思考中"动效
  3. 流式中(字在往外蹦)→ 闪烁光标 + 自动滚动

混为一谈是新手常见错误。我第一版整个过程就一个转圈圈,用户反馈"卡住了是不是挂了"——其实模型在思考,只是没反馈。

阶段一:骨架屏

历史消息加载用骨架,别用spinner。骨架屏的核心是形状贴近真实内容,让用户大脑提前"占好位"。

function MessageSkeleton() {
  return (
    <div className="skeleton-msg">
      <div className="skeleton-line" style={{ width: '90%' }} />
      <div className="skeleton-line" style={{ width: '75%' }} />
      <div className="skeleton-line" style={{ width: '40%' }} />
    </div>
  );
}
.skeleton-line {
  height: 14px;
  margin: 8px 0;
  border-radius: 4px;
  background: linear-gradient(90deg, #eee 25%, #f5f5f5 50%, #eee 75%);
  background-size: 200% 100%;
  animation: shimmer 1.4s infinite;
}
@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

注意行宽我故意做成90% / 75% / 40%错落的——真实文字就是参差不齐的,全做成等宽反而假。这是个小细节,但骗过眼睛全靠它。

阶段二:等首token的"思考中"

这段是AI应用最特殊的。首token延迟(TTFT)经常1~3秒,这段时间必须给反馈,否则用户以为死了。

我用三个点的呼吸动效,比转圈圈更"它在想":

function ThinkingDots() {
  return (
    <div className="thinking">
      <span /><span /><span />
    </div>
  );
}
.thinking span {
  display: inline-block;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: #999;
  animation: blink 1.4s infinite both;
}
.thinking span:nth-child(2) { animation-delay: 0.2s; }
.thinking span:nth-child(3) { animation-delay: 0.4s; }
@keyframes blink {
  0%, 80%, 100% { opacity: 0.3; }
  40% { opacity: 1; }
}

什么时候从"思考中"切到"流式中"?第一个token到达的瞬间。

let firstToken = true;
function onToken(t: string) {
  if (firstToken) {
    hideThinking();   // 撤掉思考动效
    firstToken = false;
  }
  appendText(t);
}

阶段三:流式光标

字一边出,末尾跟一个闪烁竖线,强化"正在打字"的感觉:

.streaming::after {
  content: '▋';
  animation: cursor 1s steps(2) infinite;
}
@keyframes cursor { 50% { opacity: 0; } }

[DONE]之后把streaming类去掉,光标消失。一个细节:光标用steps(2)做硬切,比渐变更像真实的终端光标。

一个反直觉的优化:故意延迟隐藏骨架

如果数据回得飞快(比如命中缓存20ms就回来),骨架屏一闪而过反而让人觉得"闪了一下什么鬼"。我加了个最短展示时间:

const start = Date.now();
await loadData();
const elapsed = Date.now() - start;
if (elapsed < 300) await sleep(300 - elapsed); // 骨架至少撑300ms
hideSkeleton();

听起来很反人类(明明能更快为啥要等),但视觉上更稳,不闪。这是从一堆"加载态闪烁"的吐槽里学来的取舍。

没做好的地方

骨架屏的暗色模式我适配得很糙,shimmer渐变的色值在深色背景下对比度过高,像在发光。临时调了组深色专用色值,但说实话不够细。有空再磨。


这套加载态我是套在一个新demo上的,后端是在一个零代码搭智能体的平台里拖节点配出来的——TTFT、流式输出它都帮我处理好了,前端我只管把这三段动效接上去,一下午就调顺了。

后端模型层不想自己折腾的话,讯飞 的MaaS能直接调模型、配完发布成API。你们的AI等待态是几段式?那个"首token之前的空白"你们拿什么填的?评论区交流下。