Severity: P2 · Dimension: Performance · From docs/frontend-ui-audit.md
Bundle/lazy-load discipline is strong (three.js, mermaid, d3, MarkdownChat all lazy-loaded; expensive components memoized). The gaps are hand-rolled runtime animation and images bypassing Next's pipeline.
Animation hot spots
- Landing hero canvas —
page.tsx:404-446 — 60fps RAF runs a nested O(N²) link pass over all nodes and sets ctx.shadowBlur per node (very expensive). No prefers-reduced-motion guard anywhere in page.tsx — reduced-motion users still burn full CPU/GPU. → gate RAF behind reduced-motion; drop shadowBlur for a pre-rendered glow sprite; cap the link pass. - Floating-cards RAF —
page.tsx:507-516 — calls querySelectorAll('.floating-card') + re-parses dataset floats every frame. → resolve the NodeList + parse once. - AtmosphericBackdrop —
AtmosphericBackdrop.tsx:93-139 (mounted app-wide via ShellFrame.tsx:30,66) — repaints 14 full-viewport radial gradients at 60fps on a CPU canvas, forever. Reduced-motion is handled; everyone else never throttles. → pre-bake each orb to an offscreen canvas + drawImage; consider ~30fps cadence. - KnowledgeGraph2D —
KnowledgeGraph2D.tsx:172-174,306 — d3 .on("tick", forceRerender) reconciles the whole SVG each tick; onPointerMove calls setTooltipPos on every move; edges do O(E·N) find() lookups (:397-398). → drive positions via refs/direct DOM writes; only set tooltip when hovered; precompute an id→node Map. - Spotlight card —
page.tsx:543-547 — getBoundingClientRect() on every mousemove (P3). → cache on mouseenter/resize.
Images
- No
next/image anywhere; user-content images are raw <img> with no loading="lazy" / dimensions: Avatar.tsx:46, AvatarFrame.tsx:23, screens/Social.tsx:420, screens/Settings.tsx:855,867, screens/Admin.tsx:988, TopNav.tsx:169, SideNav.tsx:109. → add loading="lazy" + intrinsic width/height; consider a Cloudflare Images loader for next/image.
framer-motion (P3)
- Eagerly imported into the landing bundle + Study:
HowItWorks.tsx:10 (rendered at page.tsx:917), screens/Study.tsx:5. → next/dynamic the motion subtrees or use the existing CSS keyframes.
Acceptance
Severity: P2 · Dimension: Performance · From
docs/frontend-ui-audit.mdBundle/lazy-load discipline is strong (three.js, mermaid, d3, MarkdownChat all lazy-loaded; expensive components memoized). The gaps are hand-rolled runtime animation and images bypassing Next's pipeline.
Animation hot spots
page.tsx:404-446— 60fps RAF runs a nested O(N²) link pass over all nodes and setsctx.shadowBlurper node (very expensive). Noprefers-reduced-motionguard anywhere inpage.tsx— reduced-motion users still burn full CPU/GPU. → gate RAF behind reduced-motion; dropshadowBlurfor a pre-rendered glow sprite; cap the link pass.page.tsx:507-516— callsquerySelectorAll('.floating-card')+ re-parsesdatasetfloats every frame. → resolve the NodeList + parse once.AtmosphericBackdrop.tsx:93-139(mounted app-wide viaShellFrame.tsx:30,66) — repaints 14 full-viewport radial gradients at 60fps on a CPU canvas, forever. Reduced-motion is handled; everyone else never throttles. → pre-bake each orb to an offscreen canvas +drawImage; consider ~30fps cadence.KnowledgeGraph2D.tsx:172-174,306— d3.on("tick", forceRerender)reconciles the whole SVG each tick;onPointerMovecallssetTooltipPoson every move; edges do O(E·N)find()lookups (:397-398). → drive positions via refs/direct DOM writes; only set tooltip when hovered; precompute an id→node Map.page.tsx:543-547—getBoundingClientRect()on every mousemove (P3). → cache onmouseenter/resize.Images
next/imageanywhere; user-content images are raw<img>with noloading="lazy"/ dimensions:Avatar.tsx:46,AvatarFrame.tsx:23,screens/Social.tsx:420,screens/Settings.tsx:855,867,screens/Admin.tsx:988,TopNav.tsx:169,SideNav.tsx:109. → addloading="lazy"+ intrinsic width/height; consider a Cloudflare Images loader fornext/image.framer-motion (P3)
HowItWorks.tsx:10(rendered atpage.tsx:917),screens/Study.tsx:5. →next/dynamicthe motion subtrees or use the existing CSS keyframes.Acceptance
prefers-reduced-motion; no per-nodeshadowBlur; no per-framequerySelectorAllloading="lazy"+ dimensions