AI 打字跟随优化
前文提到通过API去监听滚动容器或容器尺寸去触发打字跟随,监听用户的滚动去读取造成重排的属性来实现用户是否跟随打字实际会造成浏览器多次重排。
重排
浏览器重排(回流)是浏览器对DOM元素计算位置、尺寸、布局的过程。 浏览器解析HTML合成DOM树,解析CSS合成CSSOM树,它们会一步步合成渲染树,进行布局。页面、结构、尺寸发生变化就会再走一遍布局的计算流程,称为重排。
为什么重排会造成性能开销?
- 当一个元素变化后,可能影响父元素、子元素、兄弟元素等,浏览器需要进行递归遍历。
- 重排需要在浏览器主线程发生,阻塞JS、渲染。
- 频繁重排会造成浏览器卡顿,浏览器的刷新率是60fps,每帧近16ms,一次重排就会占据一部分时间;多次重排会导致掉帧。
哪些操作会导致重排?
- 元素几何属性发生变化。
- 增删、移动DOM。
- 窗口变化(resize、scroll页面等)
- 获取布局相关属性:
-
offsetTop/offsetLeft/offsetWidth/offsetHeight -
scrollTop/scrollHeight -
clientTop/clientWidth等 -
getComputedStyle() -
getBoundingClientRect()
-
IntersectionObserver 哨兵模式
这里直接取消滚动事件的监听,在容器的最底部放一个哨兵容器,通过 IntersectionObserver 去监听哨兵在监听的父元素在可视区域的交叉值来判断用户是否滚动。让哨兵通过 scrollIntoView 直接暴露在可视区域,实现打字跟随。
<div class="chat-scroll-container" ref="scrollContainerRef">
<div class="chat-container" id="messagesRef"></div>
<div class="scroll-sentinel" ref="sentinelRef"></div> // 哨兵
</div>
threshold: 1 // 1 :表示全部进入,0 :露头就秒
onMounted(() => {
const ro = new ResizeObserver(() => {
if (enableAutoScroll.value) {
scrollToBottom();
}
});
ro.observe(chatMessagesRef.value);
observer = new IntersectionObserver(
(entries) => {
const isIntersecting = entries[0].isIntersecting;
enableAutoScroll.value = isIntersecting;
},
{
root: scrollContainerRef.value, // 监听父元素,默认为 root
threshold: 1, // 1表示全部进入,0 :露头就秒
rootMargin: '10px', // 提前10px开始生效
}
);
if (sentinelRef.value) observer.observe(sentinelRef.value);
});
const scrollToBottom = () => {
nextTick(() => {
const el = sentinelRef.value;
if (!el) return;
if (enableAutoScroll.value) {
sentinelRef.value.scrollIntoView({ behavior: 'instant' });
}
});
};
在实践过程中,如果使用 behavior: 'smooth' ,浏览器在触发 scrollToBottom 时发生的动画会频繁抖动,将哨兵挤到父容器的可视区域外,导致 IntersectionObserver 频繁触发,可能产生 bug,使用 instant 取消浏览器动画抖动,直接抵达底部,避免该情况产生。