AI应用的等待体验,和普通页面不一样。普通页面是"loading几百毫秒然后全出来",AI是"先愣两秒(首token延迟),然后慢慢吐字"。这两段得用不同的加载态去接,一刀切体验很差。
我把AI对话的等待拆成三个阶段,每个阶段配不同的视觉反馈。这篇讲具体怎么做。
三段式加载态
- 首页加载(拉历史、拉配置)→ 骨架屏
- 等首token(请求发出到第一个字)→ "思考中"动效
- 流式中(字在往外蹦)→ 闪烁光标 + 自动滚动
混为一谈是新手常见错误。我第一版整个过程就一个转圈圈,用户反馈"卡住了是不是挂了"——其实模型在思考,只是没反馈。
阶段一:骨架屏
历史消息加载用骨架,别用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之前的空白"你们拿什么填的?评论区交流下。