fix(docs): fix Core Web Vitals regressions on docs.sim.ai#5630
fix(docs): fix Core Web Vitals regressions on docs.sim.ai#5630waleedlatif1 wants to merge 8 commits into
Conversation
Empirically measured under real trace-based (devtools) CPU/network throttling against the live site: mobile Performance 59, LCP 9.2s (TTFB 745ms + 8.4s element render delay). - sidebar-components.tsx / [lang]/layout.tsx: the docs sidebar renders every page in the doc tree as a link at once. Next's default viewport-prefetch fired an RSC payload fetch for every one of them on initial load - dozens of concurrent requests competing with the page's own content for bandwidth. Wired fumadocs' documented `sidebar.prefetch` option through to the custom SidebarItem/SidebarFolder components (which were bypassing it entirely, using next/link directly with no prefetch prop) via the `useSidebar()` context hook. - video.tsx: `autoPlay` forces browsers to fetch the full video file immediately on mount regardless of `preload`. Gated actual src loading behind an IntersectionObserver so a page with several of these doesn't pull down every video up front (5MB across 3 requests, in this case). Single shared component - fixes every doc page that embeds one. - proxy.ts: the i18n middleware matcher excluded favicon/robots.txt/etc but not `icon.svg`, so every request for it got routed through i18n negotiation instead of served as a static file, 404ing in production. - next.config.ts: enable productionBrowserSourceMaps - safe since this repo's source is already fully public, real debuggability benefit, zero performance cost. - shiki 4.0.0 -> 4.3.1 (verified: syntax highlighting still renders correctly). Attempted a coordinated fumadocs-core/ui/mdx/openapi upgrade to latest; fumadocs-openapi's v11 factory function became client-only (breaking change beyond its declared peer deps, requiring a component-boundary restructure), so only the safe, verified, docs-exclusive bumps (fumadocs-core/ui/mdx, shiki) are included here - the openapi major bump needs its own dedicated migration PR. Verified via a real production build (dummy env, all 3974 pages including API reference render/build cleanly) and a clean (non-stale) local server: Performance 59 -> 71 measured under real devtools throttling, RSC prefetch requests 63 -> 11, video requests/bytes 3/5MB -> 0. A pre-existing React hydration warning (#418) was found and confirmed present on live production before any of these changes, unrelated to this diff - documented, not blocking.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryLow Risk Overview Also fixes Smaller follow-ups: navbar Reviewed by Cursor Bugbot for commit 16145dc. Configure here. |
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit fef9274. Configure here.
Greptile SummaryThis PR improves docs performance and fixes a few docs rendering details. The main changes are:
Confidence Score: 5/5This looks safe to merge.
Important Files Changed
Reviews (6): Last reviewed commit: "fix(docs): fill the Ask Sim UI gap while..." | Re-trigger Greptile |
Greptile SummaryThis PR reduces docs page load work and updates a few docs runtime settings. The main changes are:
Confidence Score: 4/5The video lazy-loading path needs a small runtime fallback before merging.
apps/docs/components/ui/video.tsx Important Files Changed
Reviews (2): Last reviewed commit: "fix(docs): fix Core Web Vitals regressio..." | Re-trigger Greptile |
The lazy-load gate from the previous commit threw before isInView could ever become true in environments lacking IntersectionObserver (older browsers, some embedded webviews), leaving videos permanently source-less instead of falling back to eager loading.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 9713556. Configure here.
Repo convention is TSDoc-only, no plain // comments.
Audited with parallel subagents against the accessibility and SEO skill checklists, each fix verified by reading the actual code (not assumed): Accessibility: - lightbox.tsx: focus was never captured/restored on close, and Tab escaped the modal to the page behind it (no focus trap on the single focusable element) - heading.tsx: the per-heading copy-link icon only appeared on hover, invisible to keyboard-only navigation (added peer-focus-visible) - navbar.tsx: active nav tab had no aria-current - response-section.tsx: the status-code dropdown had no aria-haspopup/aria-expanded/role, and no Escape-to-close - workflow-preview.tsx: same focus-trap gap as lightbox.tsx on the expanded-canvas modal SEO: - page.tsx: generateMetadata's hreflang/canonical URLs used a naive String.replace to strip the locale prefix, which also matched "/en" inside unrelated slugs (platform/enterprise, integrations/enrich, platform/self-hosting/environment-variables), corrupting those pages' canonical and alternate-language URLs. Replaced with a prefix-only strip. - structured-data.tsx: the SoftwareApplication JSON-LD block compared url === baseUrl (no trailing slash) against the homepage's actual url (always has a trailing slash), so the condition was always false and this structured data never rendered anywhere, including the homepage. - structured-data.tsx: "Mothership" in the indexed featureList violated the constitution's required language (the agent is "Sim", the surface is "Chat") - this ships in JSON-LD search engines parse.
The chat panel (useChat from @ai-sdk/react, Streamdown + its CSS) was
mounted unconditionally in the root layout on every single page, so
its full weight loaded and executed even though the widget starts
closed on every page view.
Traced via the LCP breakdown insight under real devtools CPU/network
throttling: the LCP text element (the intro paragraph) had a ~8s
element render delay despite a ~13ms TTFB, and bootup-time attributed
~4.3s of scripting time to a single chunk containing React/ReactDOM's
own runtime plus this widget's eagerly-bundled dependencies.
Split into a lightweight ask-ai.tsx (just the toggle button + open
state) and ask-ai-panel.tsx (the actual chat UI, useChat, Streamdown),
loaded via next/dynamic(..., { ssr: false }) only when the user opens
the widget. Verified: the panel's chunk now has zero network requests
on initial page load.
Measured (mobile, devtools throttling, /introduction):
- Performance: 69 -> 75
- LCP: 8.0s -> 6.4s
- TBT: 260ms -> 130ms
The remaining ~6.4s LCP delay traces to the same shared chunk, now
identified as core React/ReactDOM hydration cost for this page's
sidebar/TOC/breadcrumb tree rather than an isolated bug - a real,
larger initiative (hydration architecture, not a surgical fix),
documented here rather than rushed.
|
@cursor review |
The panel split unmounted AskAIPanel entirely on close, discarding useChat's message state - reopening always started an empty conversation, unlike the original single-component layout where useChat lived in a component that never unmounted. Fixed by keeping the panel mounted (via a hasOpened flag that never resets) once first opened, and having the panel itself return null when closed rather than being conditionally removed from the tree by its parent - hooks still run every render, so useChat's state persists across visibility toggles. The dynamic import still only fires on the first open, so the initial-load win is unchanged. Verified via a real click-through (open, type, close, reopen): input persists correctly, and the panel chunk still has zero network requests on initial page load. Performance unchanged at 75.
|
@cursor review |
handleOpen set open=true synchronously, hiding the trigger button before the dynamically imported panel had a chance to render anything (next/dynamic renders null by default with no loading option) - on a slow connection neither the button nor the panel was visible. Added a loading fallback in the same fixed position so there's no gap between the button disappearing and the real panel appearing.
|
@cursor review |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 16145dc. Configure here.
Summary
sidebar.prefetchoption through the custom sidebar components, which were bypassing it via plainnext/link<Video>component eagerly downloading full video files on mount (autoPlayforces a full fetch regardless ofpreload) - gated loading behind an IntersectionObserver, fixes every doc page that embeds one/icon.svg404ing in production - the i18n middleware matcher excluded favicon/robots.txt/etc but not this fileproductionBrowserSourceMaps(repo is already public, zero exposure, real debuggability win)shiki4.0.0 → 4.3.1 andfumadocs-core/fumadocs-ui/fumadocs-mdxto their latest compatible versions (verified via build + visual smoke test). Attempted thefumadocs-openapimajor bump too but reverted it - v11's page factory became client-only, a real breaking change beyond its declared peer deps that needs its own migration PR, not a rushed dependency bumpType of Change
Testing
Measured empirically under real trace-based (devtools) CPU/network throttling against the live site and a clean local production build:
Found and documented (not fixed, pre-existing, unrelated to this diff): a React hydration warning (#418) present on live production before any of these changes.
Checklist