当文章超过约 3000 字,或一个 MDX 页面同时包含大量标题、代码高亮、数学公式、表格和框架组件时,Safari 可能出现滚动掉帧、手势响应延迟和设备发热。问题通常不是“文字太多”本身,而是海量 DOM 节点同时承担过渡状态、滚动容器接管、目录布局读取和组件即时水合造成的叠加成本。
本项目在提交 9dd965d 中完成了针对性修复。下面的规则既是变更记录,也是后续开发必须遵守的性能基线。
一、删除文章内容节点的全局 transition
旧规则曾给标题、段落、链接、span、列表、引用、代码、表格等几乎所有文章节点统一添加 transition。代码高亮和 KaTeX 会生成大量嵌套节点,Safari 需要持续维护这些节点的过渡状态,导致样式计算和绘制成本随文章长度快速上升。
修复方式是在 src/styles/main.css 中移除这类全局规则:
/* 禁止恢复这种面向正文所有后代节点的规则 */h1, h2, h3, h4, h5, h6, p, a, span, li, ul, ol,blockquote, code, pre, table, th, td, strong { @apply transition;}组件按钮、卡片、链接等确实需要反馈的元素仍可按需保留过渡。代码块和公式继续使用明确的保护规则:
pre, pre *, code, code *, .katex, .katex * { transition: none !important;}二、Posts 与 Docs 长文路由使用原生滚动
OverlayScrollbars 适合部分普通页面的视觉统一,但不应接管文章长页的 body 滚动。长文滚动由 Safari 原生滚动管线处理,通常拥有更好的惯性、合成和功耗表现。
src/layouts/Layout.astro 会识别 Posts 与 Docs 路由:
function isLongFormContentRoute() { const pathname = stripBasePath(window.location.pathname); return /^\/(?:[a-zA-Z_-]+\/)?(?:posts|docs)\//.test(pathname);}进入长文路由时不初始化 body 级 OverlayScrollbars;如果通过 Swup 从其他页面进入文章页,则销毁已有实例。离开长文路由后才按需重新初始化,并通过实例缓存避免重复创建。
三、目录更新合并到单个动画帧
长文章滚动时,多个标题可能在同一帧内连续触发 IntersectionObserver。目录组件现在只排队一个 requestAnimationFrame,并用活动标题范围键跳过无变化的更新,从而减少重复的 DOM 样式切换和布局读取。
目录自动跟随保留 smooth 视觉效果,但只会在活动标题范围真正变化后启动,并且通过单个 requestAnimationFrame 合并同一帧内的多次观察器通知。系统开启“减少动态效果”时会自动退回 auto。组件卸载时必须取消尚未执行的帧任务。
文章页依然使用浏览器原生滚动管线。为了兼顾桌面端外观,src/styles/scrollbar.css 仅通过标准滚动条属性与 ::-webkit-scrollbar 伪元素设置宽度、颜色和圆角,不创建额外 JavaScript 滚动容器。
四、多组件 MDX 默认使用 client
长文中的图表、演示器和交互组件不应批量使用 client:only 或 client:load。这两种策略会让大量组件在页面打开时集中下载、执行和渲染,放大主线程压力。
本项目已将 D3 入门文档中的 17 个图表从:
<Diagram client:only="react" />改为:
<Diagram client:visible />浏览器验证结果为:首屏未进入视口的图表激活数量为 0/17;滚动到第一个图表后为 1/17;控制台无 hydration 错误。
只有真正依赖浏览器 API、无法进行服务端渲染的组件才允许使用 client:only。首屏必须立即交互的少数组件才使用 client:load。
五、不要盲目给 Astro Islands 外层使用 content-visibility
content-visibility: auto 对纯静态超长内容可能有效,但实测发现,将它直接施加到包裹 Astro Islands 的章节容器后,可能阻止或延迟 client:visible 的可见性检测,导致组件滚动到视口后仍不激活。
因此本项目没有提交章节级 content-visibility 规则。若未来重新尝试,必须在 Safari 和 Chromium 中逐个验证首屏、滚动进入、反向滚动以及 Swup 路由返回后的水合行为,不能只看 Lighthouse 分数。
六、验证清单
每次修改长文渲染、目录、滚动容器或 MDX 水合策略后,至少完成以下检查:
- 执行
pnpm build,确保生产构建成功。 - 在 Safari 或 WebKit 中打开超过 3000 字的页面,持续上下滚动并观察掉帧与发热。
- 确认文章页没有启用 body 级 OverlayScrollbars。
- 确认首屏外的
client:visible组件没有提前激活。 - 滚动到组件后确认其正常水合,且控制台没有 hydration 错误。
- 检查目录高亮、锚点跳转、主题切换和 Swup 前进/返回。
- 使用 Safari 长文 MDX 压力测试 作为固定回归样本。
pnpm check 与 pnpm type-check 当前会被仓库中既有的类型问题阻塞。它们仍应执行或记录,但不能把既有错误误判为本次性能改动产生的回归。
