在 AI 流式输出(Streaming)场景下,这更是一个高频痛点:AI 正在逐字蹦出回复,容器高度持续增长,如果用户此时正在向上翻看 5 分钟前的历史记录,新消息的注入会不断把滚动条向下“推”,导致用户视野内的文字疯狂跳动,这就是典型的 “滚动夺权” 问题。
要优雅地解决这个“小而疼”的问题,我们需要从底层的滚动机制入手,而不仅仅是简单的 scrollTo。
1. 核心原理:锚点锁定 (Scroll Anchoring)
现代浏览器(Chrome 56+)其实内置了 overflow-anchor: auto 属性,旨在自动处理内容增长时的位置锁定。但在复杂的 AI 聊天室(涉及图片、代码块异步渲染)中,原生机制往往会失效。
我们需要手动实现一套 “视口守卫” 逻辑:
- 判断状态:用户是否处于“触底”状态?
- 锁定决策:如果在底部,随新消息滚动;如果不在底部,锁定当前像素偏移。
2. 实战方案:IntersectionObserver 方案
传统的 onscroll 监听性能极差,且在流式输出的高频更新下容易掉帧。我们使用 IntersectionObserver 监听聊天框底部的“哨兵”节点。
第一步:在 HTML 底部埋伏一个“哨兵”
HTML
<div id="chat-container" style="overflow-y: auto;">
<div id="message-list"></div>
<div id="anchor-sentinel" style="height: 1px;"></div>
</div>
第二步:逻辑封装
JavaScript
let isAtBottom = true;
// 监听哨兵是否在视口内
const observer = new IntersectionObserver((entries) => {
// 如果哨兵在视口内,说明用户处于底部
isAtBottom = entries[0].isIntersecting;
}, { threshold: 1.0 });
observer.observe(document.getElementById('anchor-sentinel'));
// AI 流式输出时的处理函数
function onAIStreamUpdate() {
const container = document.getElementById('chat-container');
if (isAtBottom) {
// 方案 A:原生平滑滚动
container.scrollTo({
top: container.scrollHeight,
behavior: 'instant' // AI 输出频率高,建议用 instant 避免动画叠加
});
} else {
// 方案 B:什么都不做。由于浏览器默认的滚动偏移是基于顶部的,
// 只要不手动触发 scrollTo,用户的视野就会自然“锁定”在当前位置。
}
}
3. 极端情况:当 AI 突然甩出一张大图或代码块
AI 渲染过程中,如果上方的内容突然加载了图片(高度从 0 变成 300px),即使你没动滚动条,原本在看的内容也会被顶走。
解决方案:图片占位与 overflow-anchor 显式声明
CSS
.chat-message img {
/* 必须设置图片占位,防止异步加载导致高度塌陷 */
aspect-ratio: 16 / 9;
background: #f0f0f0;
}
#chat-container {
/* 强制开启浏览器的滚动锚定 */
overflow-anchor: auto;
}
/* 针对流式打字机效果的特定优化 */
.ai-typing-node {
overflow-anchor: none; /* 防止打字过程中的微小抖动触发不必要的锚定计算 */
}
4. 8 个细分场景的避坑指南
| 场景 | 处理策略 | 关键点 |
|---|---|---|
| 首次进入页面 | 强制触底 | requestAnimationFrame 确保 DOM 渲染后执行 |
| 用户手动向上滚动 | 立即解除锁定 | isAtBottom 变为 false |
| 用户点击“回到最新” | 动画滚动到底部 | 重置 isAtBottom = true |
| AI 正在生成图片 | 预设 Skeleton | 避免图片加载完后视口大幅度偏移 |
| 手机键盘弹起 | 视口调整监控 | 监听 visualViewport 的 resize 事件 |
| 代码块高亮渲染 | 增量渲染 | 避免整个代码块重新渲染导致的重计算 |
| 窗口尺寸改变 | 重新计算位置 | 防抖处理 resize 事件 |
| 多轮对话清除 | 重置滚动高度 | scrollTop = 0 |
5. 资深开发者的高阶技巧:requestAnimationFrame 缓冲
如果 AI 输出速度极快(比如每秒 50 个字),每一帧都调 scrollTo 会产生明显的性能损耗。我们可以利用 “渲染缓冲” :
JavaScript
let scrollPending = false;
function smoothScrollToBottom() {
if (scrollPending || !isAtBottom) return;
scrollPending = true;
requestAnimationFrame(() => {
const container = document.getElementById('chat-container');
container.scrollTop = container.scrollHeight;
scrollPending = false;
});
}