Skip to content

Commit fef9274

Browse files
committed
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.
1 parent 73f33db commit fef9274

7 files changed

Lines changed: 61 additions & 67 deletions

File tree

apps/docs/app/[lang]/layout.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,12 @@ export default async function Layout({ children, params }: LayoutProps) {
110110
collapsible: false,
111111
footer: null,
112112
banner: null,
113+
// The sidebar renders every page in the doc tree as a link at
114+
// once, so Next's default viewport-prefetch behavior fires an
115+
// RSC payload fetch for every one of them on initial load -
116+
// dozens of concurrent requests competing with the actual
117+
// page's own content for bandwidth on a real connection.
118+
prefetch: false,
113119
components: {
114120
Item: SidebarItem,
115121
Folder: SidebarFolder,

apps/docs/components/docs-layout/sidebar-components.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { type ReactNode, useState } from 'react'
44
import type { Folder, Item, Separator } from 'fumadocs-core/page-tree'
5+
import { useSidebar } from 'fumadocs-ui/components/sidebar/base'
56
import Link from 'next/link'
67
import { usePathname } from 'next/navigation'
78
import { i18n } from '@/lib/i18n'
@@ -66,11 +67,13 @@ const FOLDER_ACTIVE = 'lg:bg-[var(--surface-active)] lg:text-[var(--text-body)]'
6667

6768
export function SidebarItem({ item }: { item: Item }) {
6869
const pathname = usePathname()
70+
const { prefetch } = useSidebar()
6971
const active = isActive(item.url, pathname, false)
7072

7173
return (
7274
<Link
7375
href={item.url}
76+
prefetch={prefetch}
7477
data-active={active}
7578
className={cn(
7679
ITEM_BASE,
@@ -97,6 +100,7 @@ function isApiReferenceFolder(node: Folder): boolean {
97100

98101
export function SidebarFolder({ item, children }: { item: Folder; children: ReactNode }) {
99102
const pathname = usePathname()
103+
const { prefetch } = useSidebar()
100104
const hasActiveChild = checkHasActiveChild(item, pathname)
101105
const isApiRef = isApiReferenceFolder(item)
102106
const isOnApiRefPage = stripLangPrefix(pathname).startsWith('/api-reference')
@@ -111,6 +115,7 @@ export function SidebarFolder({ item, children }: { item: Folder; children: Reac
111115
return (
112116
<Link
113117
href={item.index.url}
118+
prefetch={prefetch}
114119
data-active={active}
115120
className={cn(
116121
ITEM_BASE,
@@ -133,6 +138,7 @@ export function SidebarFolder({ item, children }: { item: Folder; children: Reac
133138
<>
134139
<Link
135140
href={item.index.url}
141+
prefetch={prefetch}
136142
data-active={active}
137143
className={cn(
138144
'flex flex-1 items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors',

apps/docs/components/ui/video.tsx

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useRef, useState } from 'react'
3+
import { useEffect, useRef, useState } from 'react'
44
import { cn, getAssetUrl } from '@/lib/utils'
55
import { Lightbox } from './lightbox'
66

@@ -30,6 +30,27 @@ export function Video({
3030
const videoRef = useRef<HTMLVideoElement>(null)
3131
const startTimeRef = useRef(0)
3232
const [isLightboxOpen, setIsLightboxOpen] = useState(false)
33+
const [isInView, setIsInView] = useState(false)
34+
35+
useEffect(() => {
36+
const el = videoRef.current
37+
if (!el) return
38+
39+
// `autoPlay` forces browsers to fetch the full file immediately on mount
40+
// regardless of `preload` - gate the actual load behind the viewport so a
41+
// page with several of these doesn't pull down every video up front.
42+
const observer = new IntersectionObserver(
43+
([entry]) => {
44+
if (entry.isIntersecting) {
45+
setIsInView(true)
46+
observer.disconnect()
47+
}
48+
},
49+
{ rootMargin: '200px' }
50+
)
51+
observer.observe(el)
52+
return () => observer.disconnect()
53+
}, [])
3354

3455
const openLightbox = () => {
3556
startTimeRef.current = videoRef.current?.currentTime ?? 0
@@ -39,17 +60,18 @@ export function Video({
3960
const video = (
4061
<video
4162
ref={videoRef}
42-
autoPlay={autoPlay}
63+
autoPlay={isInView && autoPlay}
4364
loop={loop}
4465
muted={muted}
4566
playsInline={playsInline}
67+
preload='none'
4668
width={width}
4769
height={height}
4870
className={cn(
4971
className,
5072
enableLightbox && 'cursor-pointer transition-opacity group-hover:opacity-[0.97]'
5173
)}
52-
src={getAssetUrl(src)}
74+
src={isInView ? getAssetUrl(src) : undefined}
5375
/>
5476
)
5577

apps/docs/next.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ const withMDX = createMDX()
55

66
const config: NextConfig = {
77
reactStrictMode: true,
8+
// Safe here since this repo's source is already fully public on GitHub -
9+
// no additional exposure versus Next's default (disabled to avoid leaking
10+
// source on the client).
11+
productionBrowserSourceMaps: true,
812
transpilePackages: ['@sim/emcn', '@sim/workflow-renderer'],
913
images: {
1014
unoptimized: true,

apps/docs/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"react": "19.2.4",
3737
"react-dom": "19.2.4",
3838
"remark-breaks": "^4.0.0",
39-
"shiki": "4.0.0",
39+
"shiki": "4.3.1",
4040
"streamdown": "2.5.0",
4141
"tailwind-merge": "^3.0.2",
4242
"reactflow": "^11.11.4",

apps/docs/proxy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,6 @@ export default function proxy(request: NextRequest, event: NextFetchEvent) {
2020

2121
export const config = {
2222
matcher: [
23-
'/((?!api|_next/static|_next/image|favicon|static|robots.txt|sitemap.xml|llms.txt|llms-full.txt).*)',
23+
'/((?!api|_next/static|_next/image|favicon|icon.svg|static|robots.txt|sitemap.xml|llms.txt|llms-full.txt).*)',
2424
],
2525
}

0 commit comments

Comments
 (0)