history.scrollRestoration设为manual后,需手动保存window.scrollY到sessionStorage并在返回时用useLayoutEffect恢复滚动位置,虚拟滚动需保存组件内scrollTop,锚点优先于历史位置。

在单页应用(SPA)中,用户浏览长列表后点击某项进入详情页,再返回时若列表滚动位置丢失,体验会明显下降。history.scrollRestoration 是浏览器原生支持的机制,配合合理的路由与状态管理,可实现“返回即回到原位置”的自然滚动还原——但需注意它仅控制浏览器是否自动滚动,**不负责保存和恢复具体位置值**,真正起作用的是开发者对 scrollY 的手动捕获与应用。
启用 scrollRestoration 并理解它的作用边界
scrollRestoration 默认为 auto,此时浏览器会在前进/后退时自动尝试滚动到上一次离开该 URL 时的位置(基于内部 history entry 记录)。但在 SPA 中,由于 URL 可能未变化(如 hash 路由)或变化但无对应页面重载,这个自动行为往往失效或不触发。
显式设置 history.scrollRestoration = 'manual' 是关键一步——它关闭浏览器的自动滚动逻辑,把控制权完全交还给开发者,避免自动滚动与手动 restore 冲突:
if ('scrollRestoration' in history) {
history.scrollRestoration = 'manual';
}
⚠️ 注意:该设置只需执行一次(如在应用初始化时),且仅在支持该 API 的现代浏览器中生效(Chrome 46+、Firefox 46+、Edge 79+)。
在路由离开前保存当前滚动位置
需要在用户即将离开列表页(例如跳转到详情页)时,将当前 window.scrollY 存入与该路由状态绑定的持久化存储中。推荐使用 sessionStorage(页面关闭即清空,符合“本次会话内返回”场景)或结合路由 key 的 Map 结构:
- 对基于 React Router 的应用,可在
useEffect清理函数中保存:
useEffect(() => {
const saveScrollPosition = () => {
sessionStorage.setItem('list-scroll-y', window.scrollY.toString());
};
window.addEventListener('beforeunload', saveScrollPosition);
return () => window.removeEventListener('beforeunload', saveScrollPosition);
}, []);
- 更精准的做法是监听路由变化(如
useLocation+useEffect依赖 location.key),仅在离开特定列表路径时保存:
const location = useLocation();
useEffect(() => {
return () => {
if (location.pathname === '/list') {
sessionStorage.setItem('list-scroll-y', window.scrollY.toString());
}
};
}, [location]);
在路由进入后恢复滚动位置
当用户返回列表页,需在组件挂载后、内容渲染完成时调用 window.scrollTo()。注意两点:一是确保 DOM 已就绪(可用 useLayoutEffect 或 setTimeout(..., 0));二是避免在 SSR 环境中执行(判断 typeof window !== 'undefined'):
- React 示例(客户端安全 + 同步执行):
useLayoutEffect(() => {
if (typeof window === 'undefined') return;
const saved = sessionStorage.getItem('list-scroll-y');
if (saved && parseInt(saved, 10) > 0) {
window.scrollTo(0, parseInt(saved, 10));
}
}, []);
- 若列表数据异步加载,应在数据渲染完毕后再滚动(例如在
useEffect依赖 data 后执行),否则可能滚到空白区域。
处理虚拟滚动、锚点跳转等特殊情况
对于使用 react-window 或 vue-virtual-scroller 的长列表,window.scrollY 始终为 0,此时需保存组件内部的 scrollTop(如 ref.current?.scrollTop);若页面含锚点导航(如 #item-123),应优先响应 URL hash 而非历史滚动位置,可加判断:
if (location.hash) {
// 先尝试滚动到锚点
const el = document.querySelector(location.hash);
if (el) el.scrollIntoView();
} else {
// 再尝试恢复历史位置
const saved = sessionStorage.getItem('list-scroll-y');
if (saved) window.scrollTo(0, parseInt(saved, 10));
}
不复杂但容易忽略。

















