文章

Astro 内容站:ScrollRestore 组件与刷新后阅读位置恢复

利用 sessionStorage、Navigation Timing 和多帧校正,在刷新文章页后尽量回到原阅读位置,同时避免页面先跳再闪。

更新说明(2026-04-16):这套自定义滚动恢复后来已经从站点里移除。它确实能恢复刷新位置,但代价是可见的重影和跳帧;当前站点改成了更保守的刷新策略:一律回到页首。

内容站的文章页有一个很具体的体验问题:读到中段甚至后段时,如果用户刷新页面,浏览器并不总能稳定回到原来的位置。

当前仓库给这个问题单独做了一个组件:

  • src/components/layout/ScrollRestore.astro

它只做一件事,尽量把刷新后的阅读位置恢复回来。

为什么不直接依赖浏览器默认行为

因为默认行为并不总稳定,尤其在下面几种情况下更明显:

  • 页面内容较长
  • 代码块、图片或字体造成布局延后稳定
  • 浏览器已经开始还原位置,但文档高度还没准备好

结果通常就是:

  • 刷新后先落在顶部
  • 再跳一下
  • 或者停在一个接近但不准确的位置

对内容站来说,这种体验很伤阅读连续性。

当前策略只在 reload 时触发

这段逻辑并不想接管所有导航,而只处理“当前页面被刷新”的场景:

const navigationEntry = performance.getEntriesByType?.("navigation")?.[0];
const isReload = navigationEntry?.type === "reload";

这样做的好处是边界清楚:

  • 普通站内跳转不被干扰
  • 真正需要恢复的位置,只限于刷新当前页

用 sessionStorage 按路径保存滚动值

当前实现的 key 不是一个固定字符串,而是:

const scrollKey = `scroll:${location.pathname}${location.search}`;

这意味着不同页面、不同查询参数下的滚动位置会分开保存,不会互相覆盖。

保存时机选的是 pagehide

window.addEventListener("pagehide", saveScroll);

这比只在 beforeunload 上下注更稳,因为页面离开、刷新、进入 bfcache 等场景里,pagehide 更接近这类需求。

恢复前先隐藏页面,避免“先闪一下再跳”

当前实现里最关键的一步,其实不是 scrollTo,而是这句:

root.style.visibility = "hidden";

只有在满足两个条件时才会这样做:

  • 这是一次刷新
  • sessionStorage 里确实找到了合法的历史滚动值

这样页面在恢复目标滚动位置之前不会先以顶部状态短暂显示,用户看到的不是“闪一下再跳”,而是更接近直接回到原阅读点。

恢复不是一次 scrollTo,而是多帧校正

恢复函数现在不是简单地执行一次 window.scrollTo(0, restoreTarget) 就结束,而是通过 requestAnimationFrame 反复尝试:

const tryRestoreScroll = () => {
  window.scrollTo(0, restoreTarget);
  restoreAttempts += 1;

  const reachedTarget =
    Math.abs(window.scrollY - restoreTarget) < 2 ||
    window.innerHeight + window.scrollY >= document.documentElement.scrollHeight;

  if (reachedTarget || restoreAttempts >= maxRestoreAttempts) {
    revealPage();
    return;
  }

  requestAnimationFrame(tryRestoreScroll);
};

这样做是因为内容高度有时会在加载后继续变化。单次恢复很容易太早,多帧校正则能给页面一点时间稳定下来。

为什么还要设置最大尝试次数

因为恢复逻辑不能无限跑。当前实现把最大次数设成 60,目的有两个:

  • 避免异常页面一直占着循环
  • 即使没精确到目标位置,也要在合理时间内把页面显示出来

也就是说,这个组件的目标不是“绝对精确”,而是“尽量准确,同时不把页面卡住”。

最终行为边界

当前这个 ScrollRestore 组件具备这些特点:

  • 只处理刷新,不干扰普通导航
  • 每个路径独立保存滚动位置
  • 恢复前先隐藏页面,减少视觉跳动
  • 通过多帧重试提高命中率
  • 命中目标或达到上限后立即显示页面

它不是一个通用的复杂滚动管理系统,而是一个针对内容站阅读体验做的最小专用组件。

这一步的价值非常实际

很多“阅读体验优化”听上去都很抽象,但滚动恢复不是。它解决的是一个直接感知的问题:

用户读到哪,刷新后最好还在哪。

在当前仓库里,把这件事单独抽成一个布局级组件,是比把逻辑零散写进详情页更稳的一种做法。

相关文章

Astro 内容站:PageHero 组件与页面头部统一

把列表页、首页、系列页和详情页里重复的标题区抽成 PageHero,让返回链接、eyebrow、标题和状态文案共用一套骨架。

2026-04-16

Astro 内容站:PageSectionHeader 组件与区块标题复用

把 section 标题、补充说明、右侧动作和分组计数统一到一个组件里,让首页、归档和索引列表共享稳定的区块头结构。

2026-04-16

Astro 内容站:组件组合重构、重复代码清理与文档同步

一次收口页面骨架、分页辅助、概览卡和旧文档漂移,把页面组合整理成更稳定的单组件文件结构。

2026-04-16

Astro 内容站:SummaryStatGrid 组件与归档总览卡

把归档页里重复的统计卡结构抽成 SummaryStatGrid,为总条目、文章数、章节数和最近更新时间提供统一的数值卡组件。

2026-04-16