From fef927427c6fffec617d506d6286f2959a969929 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Sun, 12 Jul 2026 16:00:07 -0700 Subject: [PATCH 1/8] fix(docs): fix Core Web Vitals regressions on docs.sim.ai 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. --- apps/docs/app/[lang]/layout.tsx | 6 ++ .../docs-layout/sidebar-components.tsx | 6 ++ apps/docs/components/ui/video.tsx | 28 ++++++- apps/docs/next.config.ts | 4 + apps/docs/package.json | 2 +- apps/docs/proxy.ts | 2 +- bun.lock | 80 +++++-------------- 7 files changed, 61 insertions(+), 67 deletions(-) diff --git a/apps/docs/app/[lang]/layout.tsx b/apps/docs/app/[lang]/layout.tsx index f7dadc59afb..76dd266683d 100644 --- a/apps/docs/app/[lang]/layout.tsx +++ b/apps/docs/app/[lang]/layout.tsx @@ -110,6 +110,12 @@ export default async function Layout({ children, params }: LayoutProps) { collapsible: false, footer: null, banner: null, + // The sidebar renders every page in the doc tree as a link at + // once, so Next's default viewport-prefetch behavior fires an + // RSC payload fetch for every one of them on initial load - + // dozens of concurrent requests competing with the actual + // page's own content for bandwidth on a real connection. + prefetch: false, components: { Item: SidebarItem, Folder: SidebarFolder, diff --git a/apps/docs/components/docs-layout/sidebar-components.tsx b/apps/docs/components/docs-layout/sidebar-components.tsx index e023830bc9f..d4c279e255b 100644 --- a/apps/docs/components/docs-layout/sidebar-components.tsx +++ b/apps/docs/components/docs-layout/sidebar-components.tsx @@ -2,6 +2,7 @@ import { type ReactNode, useState } from 'react' import type { Folder, Item, Separator } from 'fumadocs-core/page-tree' +import { useSidebar } from 'fumadocs-ui/components/sidebar/base' import Link from 'next/link' import { usePathname } from 'next/navigation' import { i18n } from '@/lib/i18n' @@ -66,11 +67,13 @@ const FOLDER_ACTIVE = 'lg:bg-[var(--surface-active)] lg:text-[var(--text-body)]' export function SidebarItem({ item }: { item: Item }) { const pathname = usePathname() + const { prefetch } = useSidebar() const active = isActive(item.url, pathname, false) return ( (null) const startTimeRef = useRef(0) const [isLightboxOpen, setIsLightboxOpen] = useState(false) + const [isInView, setIsInView] = useState(false) + + useEffect(() => { + const el = videoRef.current + if (!el) return + + // `autoPlay` forces browsers to fetch the full file immediately on mount + // regardless of `preload` - gate the actual load behind the viewport so a + // page with several of these doesn't pull down every video up front. + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setIsInView(true) + observer.disconnect() + } + }, + { rootMargin: '200px' } + ) + observer.observe(el) + return () => observer.disconnect() + }, []) const openLightbox = () => { startTimeRef.current = videoRef.current?.currentTime ?? 0 @@ -39,17 +60,18 @@ export function Video({ const video = (