docs: cut homepage weight on slow connections (logos, avatars, fonts, poster) (#11430)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@@ -16,47 +16,49 @@ export default async function HomePage() {
|
||||
{/* Left side — Hero title */}
|
||||
<div className="relative w-full lg:w-[40%] lg:h-dvh border-b lg:border-b-0 lg:border-r border-foreground/[0.06] px-5 sm:px-6 lg:px-7 lg:sticky lg:top-0 z-10 bg-background lg:overflow-clip">
|
||||
<LineFieldBackground />
|
||||
{/* 3D Logo */}
|
||||
{/* 3D Logo: WebPs are rendered from the original SVGs at 2x their
|
||||
display size with the grain baked in, so they're `unoptimized` to
|
||||
keep next/image from resampling the grain away. */}
|
||||
<div className="hidden lg:flex justify-center h-full absolute items-center left-1/2 -translate-x-1/2 w-full pointer-events-auto select-none animate-logo-reveal z-[1]">
|
||||
{/* 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
|
||||
width={311}
|
||||
height={400}
|
||||
className="h-auto max-h-[200px] object-contain z-10 animate-logo-snap-left transition-transform duration-300 ease-out group-hover:-translate-x-3 group-hover:-rotate-5"
|
||||
unoptimized
|
||||
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
|
||||
width={321}
|
||||
height={400}
|
||||
className="h-auto object-contain -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"
|
||||
unoptimized
|
||||
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
|
||||
width={309}
|
||||
height={400}
|
||||
className="h-auto max-h-[200px] object-contain z-10 animate-logo-snap-left transition-transform duration-300 ease-out group-hover:-translate-x-3 group-hover:-rotate-5"
|
||||
unoptimized
|
||||
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
|
||||
width={302}
|
||||
height={400}
|
||||
className="h-auto object-contain -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"
|
||||
unoptimized
|
||||
draggable={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -704,6 +704,29 @@ function ContributorsSection({
|
||||
contributors: ContributorInfo[];
|
||||
contributorCount: number;
|
||||
}) {
|
||||
const wallRef = useRef<HTMLDivElement>(null);
|
||||
const [wallNearViewport, setWallNearViewport] = useState(false);
|
||||
|
||||
// Hundreds of avatars scroll through the wall; the links always render,
|
||||
// but the avatar images are only mounted once the wall is 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 +758,7 @@ function ContributorsSection({
|
||||
|
||||
{contributors.length > 0 && (
|
||||
<div
|
||||
ref={wallRef}
|
||||
className="relative overflow-hidden h-[220px] rounded-md"
|
||||
style={{
|
||||
perspective: "600px",
|
||||
@@ -776,16 +800,21 @@ function ContributorsSection({
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={c.login}
|
||||
aria-label={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"
|
||||
/>
|
||||
{wallNearViewport ? (
|
||||
<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"
|
||||
/>
|
||||
) : (
|
||||
<span className="block size-8 rounded-sm bg-foreground/5" />
|
||||
)}
|
||||
<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>
|
||||
@@ -1017,23 +1046,25 @@ function DemoVideo() {
|
||||
|
||||
return (
|
||||
<div ref={containerRef}>
|
||||
{/* The poster is a CSS background rather than the `poster` attribute:
|
||||
hidden <video>s still fetch their poster, but browsers skip
|
||||
backgrounds on display:none elements, so only the active theme's
|
||||
poster loads and it's there on first paint. */}
|
||||
<video
|
||||
src="/demo-dark.mp4"
|
||||
poster="/demo-dark-poster.webp"
|
||||
preload="none"
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
className="w-full h-auto -mt-[2px] dark:block hidden"
|
||||
className="w-full h-auto aspect-[2528/1440] -mt-[2px] bg-[url(/demo-dark-poster.webp)] bg-cover dark:block hidden"
|
||||
/>
|
||||
<video
|
||||
src="/demo-light.mp4"
|
||||
poster="/demo-light-poster.webp"
|
||||
preload="none"
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
className="w-full h-auto -mt-[2px] dark:hidden"
|
||||
className="w-full h-auto aspect-[2544/1440] -mt-[2px] bg-[url(/demo-light-poster.webp)] bg-cover dark:hidden"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -73,7 +73,6 @@
|
||||
"fumadocs-mdx": "^15.0.12",
|
||||
"fumadocs-typescript": "^5.2.6",
|
||||
"fumadocs-ui": "16.12.0",
|
||||
"geist": "^1.7.2",
|
||||
"gray-matter": "^4.0.3",
|
||||
"hast-util-to-jsx-runtime": "^2.3.6",
|
||||
"input-otp": "^1.4.2",
|
||||
|
||||
|
Before Width: | Height: | Size: 836 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 836 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 379 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 379 KiB |
|
After Width: | Height: | Size: 53 KiB |
@@ -338,9 +338,6 @@ importers:
|
||||
fumadocs-ui:
|
||||
specifier: 16.12.0
|
||||
version: 16.12.0(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.12.0(@mdx-js/mdx@3.1.1)(@tanstack/react-router@1.170.10(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.21.0(react@19.2.7))(next@16.3.4(@babel/core@8.0.1)(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(@types/node@25.9.4)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(zod@4.5.4))(next@16.3.4(@babel/core@8.0.1)(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(@types/node@25.9.4)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(tailwindcss@4.3.1)
|
||||
geist:
|
||||
specifier: ^1.7.2
|
||||
version: 1.7.2(next@16.3.4(@babel/core@8.0.1)(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(@types/node@25.9.4)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))
|
||||
gray-matter:
|
||||
specifier: ^4.0.3
|
||||
version: 4.0.3
|
||||
@@ -10448,11 +10445,6 @@ packages:
|
||||
fzf@0.5.2:
|
||||
resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==}
|
||||
|
||||
geist@1.7.2:
|
||||
resolution: {integrity: sha512-Gu5lDFa3pLRyoBlBPf0QIFHVdWAnpco7fS1bJm41jyLPFoguBgiubseUN2oLXMgqZ7uxAxDoXcHMhCY/fOTTgg==}
|
||||
peerDependencies:
|
||||
next: '>=13.2.0'
|
||||
|
||||
generate-function@2.3.1:
|
||||
resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==}
|
||||
|
||||
@@ -26710,10 +26702,6 @@ snapshots:
|
||||
|
||||
fzf@0.5.2: {}
|
||||
|
||||
geist@1.7.2(next@16.3.4(@babel/core@8.0.1)(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(@types/node@25.9.4)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)):
|
||||
dependencies:
|
||||
next: 16.3.4(@babel/core@8.0.1)(@opentelemetry/api@1.9.1)(@playwright/test@1.58.2)(@types/node@25.9.4)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
|
||||
generate-function@2.3.1:
|
||||
dependencies:
|
||||
is-property: 1.0.2
|
||||
|
||||