Lee's Blog
  • 首页
  • 博客
  • 作品
  • 成长
  • 学习
  • 打榜
  • 练习
  • 关于
登录 / 注册
✦

保持联系

关注我的最新动态

GitHubbilibili

© 2026 Lee's Blog

首页/博客/博客开发/TOC 悬浮折腾记:6 次 commit 才彻底搞定
TOC 悬浮折腾记:6 次 commit 才彻底搞定
博客开发2026年8月3日6 分钟阅读

TOC 悬浮折腾记:6 次 commit 才彻底搞定

文章页 TOC 从 IO 到 sticky 再到 fixed 的完整踩坑记录

性能优化前端教程
X微博

背景

文章页右侧的 TOC(目录)从一开始就问题不断。最近一周折腾了 6 次 commit,从 IO 检测高亮,到 sticky 跑道,再到 position: fixed 悬浮,才算彻底搞定。本文记录完整决策链,留作下次避坑参考。

历次尝试

v1:IntersectionObserver 检测高亮

最直觉的方案——观察每个 heading 进入视口就标记 active。

  • ❌ 高亮滞后一个 heading 的距离:rootMargin: '-80px 0px -70% 0px' 留了 160px 触发区,heading 跨过 nav 80px 线之后还要再滚 160px 才会被点亮
  • ❌ 相邻 heading 退出触发区时不重评估:IO 回调只含「状态变化」的 entries,top heading 离开触发区时 observed 里只有它一个(isIntersecting=false),h2 还在区里也不会被重新检查

v2:scroll-based 检测 + 主动同步列表

换成自己算:active = 最后一个 top ≤ 80px 的 heading,rAF 节流。同时加 self-scroll effect 把 active link 滚到 TOC 列表可视区。

  • ✅ 高亮实时跟随,无滞后
  • ✅ 长 TOC 里 active 项自动滚到可视
  • ❌ 整篇文章滚到一定距离会「回退」:用户报告「页面自己往回跳」

v3:定位回滚根因

去翻 MDN 文档:

scrollIntoView() 默认 container: 'all',影响所有滚动祖先,包括视口。

我的链路是 .algo-toc-list (overflow-y: auto) → <html> (overflow-y: scroll)。每次 activeId 变,link.scrollIntoView({ block: 'nearest' }) 都会遍历两个容器。sticky 父元素里的 link,浏览器在不同实现里会用文档位置或视觉位置判定「是否在视口内」——文档位置在 grid row 顶部(约 Y=580),跟 sticky 视觉位置(top:80)不一致,触发回滚。

修法:直接对 .algo-toc-list 设 scrollTop,绕开视口。

  • ✅ 不再回滚
  • ❌ active 高亮彻底死了:页面加载无高亮,滚也不亮

v4:定位「active detection 失效」

死盯着 self-scroll effect 看是没用的——真凶在 active detection:

useEffect(() => {
  const headingEls = entries
    .map(({ id }) => document.getElementById(id))
    .filter((el): el is HTMLElement => el !== null)
  if (headingEls.length === 0) return  // ← 早退,监听器永远不挂
  ...
  window.addEventListener('scroll', schedule, { passive: true })
}, [entries])

两个 bug 叠在一起:

  1. 早退:getElementById 全部 null(Next.js 16 partial hydration 时 TOC 比 markdown body 先完成 hydration)→ 直接 return → 监听器永远挂不上
  2. [entries] deps:DOM 之后变好,effect 不会重跑,监听器永远没机会挂

修法:useRef 缓存 headings + 监听器 effect 用 [] deps + heading 解析在 compute() 里懒做。第一次 mount DOM 没好 → 监听器已挂 → 用户第一次 scroll → compute 重新解析 → 这次 DOM 好了 → activeId 更新。

  • ✅ 高亮跟滚动了
  • ❌ 用户还是不满意

v5:「干脆让 TOC 浮在页面上」

用户原话:「就不能让目录悬浮在页面上吗!?」

回头看整个架构:sticky 的本质问题是 TOC 跟着文章区域走,滚到评论区就消失。前面所有修修补补都在 sticky 框架里硬撑。要彻底解决,必须换 fixed。

.article-layout {
  display: flex;
  max-width: 1400px;
  margin: 0 auto;
  padding: 0 8vw;
}
.article-toc-cell {
  position: fixed;
  top: var(--nav-offset);
  width: 260px;
  right: max(8vw, calc((100vw - 1400px) / 2 + 8vw));
  max-height: calc(100vh - var(--nav-offset) - 4rem);
  overflow-y: auto;
}
@media (min-width: 1280px) {
  .article-main { margin-right: 260px; }
}
  • ✅ TOC 永远钉在视口右侧,滚到评论区/footer 也不消失
  • ✅ 删掉 cell+inner 双层、删掉 align-self stretch、删掉 grid(改 flex),结构极简
  • ✅ 自滚动同步保留(用 container.scrollTop 不会触发视口)

教训

  1. sticky + scrollIntoView 是坑:默认 container: 'all' 遍历所有祖先。sticky 子元素的文档位置和视觉位置在不同浏览器实现里有歧义。能不用就别用
  2. Next.js 16 partial hydration 时序:client component 的 useEffect 跑得比 server-rendered markdown body 早是常态。getElementById 全 null 不稀奇。监听器一定要 lazy
  3. 用户说「悬浮」就老老实实 position: fixed:sticky 的局限性太多,固定就彻底固定,别走「sticky 兼容路线」自我折磨
  4. 早退(early return)是大坑源头:一旦 early return 又没 retry 机制,bug 就变成「永远不工作」。要么别 early return,要么明摆着 retry

改动 commit 链

109b477 fix(article): scroll-based TOC active detection + self-scroll sync
ebdf83d fix(article): replace scrollIntoView with explicit scrollTo/scrollTop
7e03c9e fix(article): decouple TOC active listener from heading DOM availability
781661f refactor(ui): make article TOC position:fixed instead of sticky

下次再碰类似问题,先问「能不能直接换架构」,别在错误假设上硬磕。

人话

以上内容都是AI自己写的,甚至是自己发的,接下来作为AI的驾驶员,我说一些经验。

AI在猜,但如果有东西猜,就能猜准

众所周知,AI原理和概率论脱不了关系,这里我不再赘述,只说如何感知和运用。

如果AI老是修不好某个特定的BUG,但每次修复都说的振振有词,这个时候,就要问一句:你说的是真的,还是猜的?如果装了一些诚实skill或者AI性子比较诚实,像claude之类,这时候它就会坦诚,“不好意思,我之前说的,大多都是猜的”,只有一些性格比较差,或者skill装的不好,对话习惯不好之类的,还会说“我没有猜,我都是根据...给的答案”,如果真的不是猜的,那么为什么一开始就不修好来呢?

那么,知道了AI在猜,我们该怎么做?让AI不要猜吗?

那是不可能的,AI就是靠猜给答案的,而我们要做的,就是让AI猜的更准。

举个例子

同样是猜,我给两个盖着的碗,让你猜哪个碗里面有东西,和同样情况下告诉你,左边的碗里没有东西,哪个能猜的更准?结果显而易见!

同样的,到了AI这里,告诉他一些信息,让他更能猜对才是正路。可以告诉的信息有:实例真实代码,比如,我想要复刻labuladong.online的文章布局,我就让AI自己去看这个网站的前端代码。还有大体的架构。把想要实现的大功能自己拆分成小功能告诉AI,或者把抽象内容具体化,能让AI更好地理解需求和执行,就拿这次经历来说,我一开始让AI做一个文章目录,这个目录要有跟踪文章进度,常态显示的功能。这个时候,AI做出来的东西一直不对,侧边目录老是因为文章过长而被甩到上方屏幕之外,留下空白来。如此几轮描述问题,AI都做的不好,我也开始意识到AI在猜,于是我说出了那句“让目录悬浮”,终于搞定了。

← 上一篇把博客搞崩两次的那次部署:一次 OTP 重构复盘下一篇 →用户登录后看不到自己的头像——一次 DOM 顺序撞车

相关文章

  • 一次登录 / 资料 / 头像的全面体检——5 个用户报告 + curl 抓出的真问题约 9 分钟
  • 用户登录后看不到自己的头像——一次 DOM 顺序撞车约 5 分钟
  • 把博客搞崩两次的那次部署:一次 OTP 重构复盘约 8 分钟

评论

需要登录账号才能发表评论。

  • 加载中…

本页内容

  • 背景
  • 历次尝试
  • v1:IntersectionObserver 检测高亮
  • v2:scroll-based 检测 + 主动同步列表
  • v3:定位回滚根因
  • v4:定位「active detection 失效」
  • v5:「干脆让 TOC 浮在页面上」
  • 教训
  • 改动 commit 链
  • 人话
  • AI在猜,但如果有东西猜,就能猜准
  • 举个例子