docs: cut homepage weight on slow connections

Under Fast 3G the homepage still took ~19s to LCP. The main costs:

- The hero 3D logos were SVGs wrapping large embedded PNGs (256-590KB
  each), and all four (both themes) were `priority`, so every visit
  preloaded 1.69MB, including on mobile where they're hidden. They're now
  WebP (21-59KB) served through next/image with `sizes`, lazy-loaded so
  only the visible theme's pair is fetched (~37KB).
- The contributor wall rendered 473 GitHub avatars (~1.6MB) that the
  browser's lazy-loading threshold pulled in right after first paint. The
  wall now mounts its avatars only when it nears the viewport.
- `geist/font/pixel` declared and preloaded all five Geist Pixel fonts;
  only Square is used, so load just that one.
- Both demo video posters were fetched even though one is hidden; only
  the active theme's poster is set now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Bekacru
2026-09-26 22:43:32 -07:00
co-authored by Claude Opus 5.5
parent d1e74989f7
commit f396ec6c39
8 changed files with 81 additions and 43 deletions
+8 -8
View File
@@ -21,42 +21,42 @@ export default async function HomePage() {
{/* Dark mode logos */}
<div className="group max-w-[300px] w-full max-h-[200px] -mt-[30%] hidden dark:flex justify-center opacity-100">
<Image
src="/left-3d-logo.svg"
src="/left-3d-logo.webp"
alt=""
width={518}
height={667}
className="h-auto max-h-[200px] z-10 animate-logo-snap-left transition-transform duration-300 ease-out group-hover:-translate-x-3 group-hover:-rotate-5"
priority
sizes="160px"
draggable={false}
/>
<Image
src="/right-3d-logo.svg"
src="/right-3d-logo.webp"
alt=""
width={518}
height={667}
className="h-auto -ml-28 -mt-3 max-h-[200px] animate-logo-snap-right transition-transform duration-300 ease-out group-hover:translate-x-3 group-hover:rotate-5"
priority
sizes="160px"
draggable={false}
/>
</div>
{/* Light mode logos */}
<div className="group max-w-[300px] w-full max-h-[200px] -mt-[30%] flex dark:hidden justify-center opacity-100">
<Image
src="/left-3d-logo-light.svg"
src="/left-3d-logo-light.webp"
alt=""
width={518}
height={667}
className="h-auto max-h-[200px] z-10 animate-logo-snap-left transition-transform duration-300 ease-out group-hover:-translate-x-3 group-hover:-rotate-5"
priority
sizes="160px"
draggable={false}
/>
<Image
src="/right-3d-logo-light.svg"
src="/right-3d-logo-light.webp"
alt=""
width={518}
height={667}
className="h-auto -ml-28 -mt-3 max-h-[200px] animate-logo-snap-right transition-transform duration-300 ease-out group-hover:translate-x-3 group-hover:rotate-5"
priority
sizes="160px"
draggable={false}
/>
</div>
Binary file not shown.
+62 -34
View File
@@ -704,6 +704,28 @@ function ContributorsSection({
contributors: ContributorInfo[];
contributorCount: number;
}) {
const wallRef = useRef<HTMLDivElement>(null);
const [wallNearViewport, setWallNearViewport] = useState(false);
// Hundreds of avatars scroll through the wall; only fetch them once it's
// close to the viewport instead of relying on the browser's generous
// native lazy-loading distance.
useEffect(() => {
const wall = wallRef.current;
if (!wall) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setWallNearViewport(true);
observer.disconnect();
}
},
{ rootMargin: "200px" },
);
observer.observe(wall);
return () => observer.disconnect();
}, []);
if (contributors.length === 0) return null;
const colCount = 18;
@@ -735,6 +757,7 @@ function ContributorsSection({
{contributors.length > 0 && (
<div
ref={wallRef}
className="relative overflow-hidden h-[220px] rounded-md"
style={{
perspective: "600px",
@@ -761,39 +784,40 @@ function ContributorsSection({
transformOrigin: "center center",
}}
>
{columns.map((col, i) => (
<div key={i} className="relative overflow-hidden h-full">
<div
className="flex flex-col gap-1 items-center"
style={{
animation: `vertical-marquee ${speeds[i]}s linear infinite`,
}}
>
{[...col, ...col].map((c, j) => (
<a
key={`${c.login}-${j}`}
href={c.html_url}
target="_blank"
rel="noopener noreferrer"
title={c.login}
className="relative group shrink-0"
>
<img
src={`${c.avatar_url}&s=64`}
alt={c.login}
width={32}
height={32}
loading="lazy"
className="rounded-sm grayscale opacity-50 hover:grayscale-0 hover:opacity-100 transition-all duration-200 hover:scale-125 hover:z-10 relative"
/>
<div className="absolute -top-7 left-1/2 -translate-x-1/2 px-1.5 py-0.5 bg-foreground text-background text-[8px] font-mono rounded-sm opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20">
{c.login}
</div>
</a>
))}
{wallNearViewport &&
columns.map((col, i) => (
<div key={i} className="relative overflow-hidden h-full">
<div
className="flex flex-col gap-1 items-center"
style={{
animation: `vertical-marquee ${speeds[i]}s linear infinite`,
}}
>
{[...col, ...col].map((c, j) => (
<a
key={`${c.login}-${j}`}
href={c.html_url}
target="_blank"
rel="noopener noreferrer"
title={c.login}
className="relative group shrink-0"
>
<img
src={`${c.avatar_url}&s=64`}
alt={c.login}
width={32}
height={32}
loading="lazy"
className="rounded-sm grayscale opacity-50 hover:grayscale-0 hover:opacity-100 transition-all duration-200 hover:scale-125 hover:z-10 relative"
/>
<div className="absolute -top-7 left-1/2 -translate-x-1/2 px-1.5 py-0.5 bg-foreground text-background text-[8px] font-mono rounded-sm opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap z-20">
{c.login}
</div>
</a>
))}
</div>
</div>
</div>
))}
))}
</div>
<div className="absolute inset-y-0 left-0 w-8 bg-linear-to-r from-background to-transparent pointer-events-none z-10" />
<div className="absolute inset-y-0 right-0 w-8 bg-linear-to-l from-background to-transparent pointer-events-none z-10" />
@@ -1019,7 +1043,9 @@ function DemoVideo() {
<div ref={containerRef}>
<video
src="/demo-dark.mp4"
poster="/demo-dark-poster.webp"
// Hidden <video>s still fetch their poster, so only set the
// poster for the active theme (known after hydration).
poster={resolvedTheme === "dark" ? "/demo-dark-poster.webp" : undefined}
preload="none"
loop
muted
@@ -1028,7 +1054,9 @@ function DemoVideo() {
/>
<video
src="/demo-light.mp4"
poster="/demo-light-poster.webp"
poster={
resolvedTheme === "light" ? "/demo-light-poster.webp" : undefined
}
preload="none"
loop
muted
+11 -1
View File
@@ -1,5 +1,5 @@
import { GeistPixelSquare } from "geist/font/pixel";
import { Geist, Geist_Mono } from "next/font/google";
import localFont from "next/font/local";
import { cn } from "@/lib/utils";
const fontSans = Geist({
@@ -12,6 +12,16 @@ const fontMono = Geist_Mono({
variable: "--font-geist-mono",
});
// Only the Square variant is used. Importing from `geist/font/pixel` would
// declare (and preload) all five Geist Pixel fonts on every page.
const GeistPixelSquare = localFont({
src: "../assets/GeistPixel-Square.woff2",
variable: "--font-geist-pixel-square",
weight: "500",
fallback: ["Geist Mono", "ui-monospace", "monospace"],
adjustFontFallback: false,
});
export const fontVariables = cn(
fontSans.variable,
fontMono.variable,
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB