ISSUE-045: docs-site navbar 跨项目切换整页刷新导致数秒卡顿
- 日期:2026-06-10
- 状态:已修复
- 类型:性能 / 功能缺陷
- 影响范围:docs-site 全部 navbar 项目切换(dev 与 build 产物均受影响)
问题现象
- 点击 navbar 切换到另一个项目(如 互联通信 → 通用知识库)加载数秒,远慢于站内普通链接点击。
- 已在某项目内时再次点击该 navbar 项,跳回该项目第一篇文档,丢失当前阅读位置。
调查过程
- [查代码]
src/clientModules/rememberLastDoc.js(修复前版本):setupNavbarInterception对跨 sidebar 的 navbar 点击e.preventDefault()后执行window.location.href = lastDoc。 - [假设 1,确认为根因]
window.location.href触发浏览器整页重载:重新下载执行全部 app bundle、重建 React、重新加载 2.2MB MathJax 并全页重排公式;dev 模式还需重新请求全部编译 chunk。 - [旁证] 首次访问无 localStorage 记录的项目不触发拦截、走 SPA 路由,切换反而快——与"去过的项目更慢"的现象一致。
- [假设 2,排除为主因] dev 按需编译只放大首次进入的耗时,build 产物上切换同样慢,主因不在编译。
- [查代码] 现象 2 是拦截逻辑的显式分支:
targetSidebar === currentSidebar时不拦截,落入 Docusaurus 默认行为(跳 sidebar 第一篇)。原注释认为"用户可能想回总览页",与用户实际预期不符(2026-06-10 与用户确认:同项目内点击应保持当前位置)。
根因分析
"恢复上次阅读位置"实现在 DOM 点击拦截层,只能用 window.location.href 跳转,绕过了 React Router——SPA 站点的导航被降级为整页刷新。同项目点击跳第一篇是同一实现下的行为分支缺陷。
解决方案
把恢复逻辑从点击拦截上移到 React 层:swizzle 替换 @theme/NavbarItem/DocSidebarNavbarItem(新文件 src/theme/NavbarItem/DocSidebarNavbarItem.js),渲染时改写链接 to:
- 已在该 sidebar →
to= 当前 pathname + hash(点击原地不动) - 跨 sidebar →
to= localStorage 中该 section 上次文档(无记录则默认第一篇)
点击始终是普通 <Link> 客户端导航。SSR 阶段无 localStorage,首渲染用默认链接、useEffect 在客户端改写,避免 hydration 不一致。
rememberLastDoc.js 精简为只负责写入 localStorage(onRouteDidUpdate 保存 {section: pathname+hash}),删除全部点击拦截与跳转代码。
涉及文件:
- 新建
docs-site/src/theme/NavbarItem/DocSidebarNavbarItem.js - 重写
docs-site/src/clientModules/rememberLastDoc.js docs-site/docusaurus.config.ts(themeCommonDedup 插件扩展,见下)
实施中遇到的次生问题:swizzle 组件 import @docusaurus/plugin-content-docs/client 编译失败(Module not found)——该包不是 docs-site 的直接依赖,pnpm 严格 node_modules 下 src/ 代码解析不到;且 .pnpm 中已存在两个 plugin-content-docs 实例,直接 pnpm add 可能引入第二实例导致 useActiveDocContext 的 React context 不共享。解法与既有 themeCommonDedup 同构:webpack alias 把 @docusaurus/plugin-content-docs/client$ 精确指向 preset-classic 实际使用的实例(经 preset 虚拟 store 的 node_modules realpath 追踪)。
验证
pnpm build全量构建通过(swizzle 组件编译 + SSR 渲染无错)。- 手动验证步骤(浏览器):
- 进入项目 A 的非首篇文档,切到项目 B,再点 navbar 切回 A → 应瞬时回到刚才的文档(Network 面板无 document 类型请求)。
- 在项目 A 内再次点击 navbar 的 A → 停留在当前文档,不跳第一篇。
- 清空 localStorage 后点击未访问过的项目 → 落到该项目第一篇。
Lessons Learned / Prevention
- SPA 站点内任何"自定义跳转"必须走框架路由(React 层改
<Link to>或框架 history API),clientModule 点击拦截 +window.location.href必然整页刷新。 - clientModule 适合纯副作用(记录、打点);凡是要改变导航目标的逻辑,应放在 swizzle 的 theme 组件里。