open-design/apps/landing-page/app/image-assets.ts
lefarcen 7fc4362ba8
perf(landing): edge-cache HTML and precise-load thumbnails (#2235)
* perf(landing): edge-cache HTML and precise-load thumbnails

Without `public/_headers` Cloudflare Pages serves every HTML with
`cf-cache-status: DYNAMIC` so each request roundtrips to the Pages
origin — observed TTFB 660–900ms from Seattle, worse from Asia.
With `s-maxage=3600, stale-while-revalidate=86400` HTML stays cached
at the edge between deploys (CF Pages auto-purges on every deploy so
freshness is unchanged in practice), and `_astro/` hash bundles flip
to `immutable` so the existing 4h+must-revalidate roundtrips go away.

For thumbnails, native `loading="lazy"` is browser-decided —
Chrome over-prefetches (1250–3000px), Safari fires near in-viewport.
A new `<LazyImg>` Astro component and global IntersectionObserver
(rootMargin 300px for images, 600px for videos) replaces all 10
site-wide `loading="lazy"` usages with precise control. Above-the-fold
slots (first 4 rows, detail-page hero previews) opt into `eager` or
`priority` to skip the IO roundtrip.

Homepage hero LCP gets `<link rel="preload" imagesrcset>`, a 4-step
`srcset` (768/1280/1920/2560) plus `fetchpriority="high"` so retina
devices stop repainting from the 1024-only variant — was the P99 long
tail.

Verified: `pnpm guard` 6/6, `pnpm typecheck` 0 errors, `pnpm build`
865 pages 28s, generated `out/index.html` contains the preload link
and 15 `data-precise-src` thumbnails, `out/plugins/index.html` has
95 precise-loaded thumbnails plus the IO script.

* perf(landing): logo to webp + parallelize Google Fonts load

Two HAR-validated wins on top of the edge-cache / precise-load commit:

logo: 500x500 192KB PNG → 200x200 7.5KB WebP. Footer/header actually
render at 36x36, so the source is 5x larger than necessary at the
display size and ships RGBA PNG bytes for what reads as a flat
graphic. WebP at q=85 keeps the gradient ring crisp at every DPR we
care about.

fonts: globals.css used `@import url(...)` for Google Fonts, which
serialized HTML → CSS → fonts.googleapis.com/css2 → fonts.gstatic.com/
woff2. HAR measured 953ms for the fonts CSS plus 400–800ms per woff2
× 4 — close to 3s before text could render in the intended family,
even with display=swap. Moving to `<link>` + `<link rel=preconnect>`
in each page's <head> lets the fonts CSS fetch race the HTML body
parse, and warms the TLS handshake to gstatic.com so woff2 requests
don't pay DNS+TLS at request time.

A shared `font-stylesheet.astro` keeps the four-family URL canonical
across all five entry points (index, sub-page-layout, plugins/index,
plugins/[slug], blog/index, blog/[slug]). og.astro already had this
treatment.
2026-05-19 19:14:25 +08:00

66 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const R2_PUBLIC_ORIGIN = 'https://static.open-design.ai';
const IMAGE_RESIZING_ORIGIN = R2_PUBLIC_ORIGIN;
const ASSET_PREFIX = 'landing/assets';
type ImageOptions = {
width: number;
quality?: number;
};
export function r2Asset(name: string): string {
return `${R2_PUBLIC_ORIGIN}/${ASSET_PREFIX}/${name}`;
}
export function imageAsset(name: string, { width, quality = 85 }: ImageOptions): string {
const options = `width=${width},quality=${quality},format=auto`;
return `${IMAGE_RESIZING_ORIGIN}/cdn-cgi/image/${options}/${r2Asset(name)}`;
}
/**
* Build a responsive `srcset` value. Each width gets its own Cloudflare
* Image Resizing variant; the browser picks the closest match to
* `sizes × devicePixelRatio`.
*
* Why this exists: a single 1024-wide variant was hurting both ends —
* retina desktops repaint when a higher-DPR copy arrives (LCP P99 long
* tail), and phones download more bytes than they need.
*/
export function imageAssetSrcset(
name: string,
widths: number[],
quality = 82,
): string {
return widths
.map((width) => `${imageAsset(name, { width, quality })} ${width}w`)
.join(', ');
}
export const heroImage = imageAsset('hero.png', { width: 1280, quality: 82 });
/**
* Responsive srcset for the homepage hero. Widths cover phones (768),
* laptops at 1x (1280), retina laptops (1920) and 4K / 2x retina (2560).
*/
export const heroImageSrcset = imageAssetSrcset(
'hero.png',
[768, 1280, 1920, 2560],
);
/**
* Default Open Graph card image. Used by every page that doesn't supply
* its own hero (most blog posts in the v1 layout). 1200 wide is what most
* social platforms render at; aspect ratio is whatever hero.png ships with
* — we omit explicit og:image:width/height so platforms can resolve it.
*/
export const ogDefaultImage = imageAsset('hero.png', { width: 1200, quality: 86 });
/**
* 1×1 transparent SVG used as the initial `src` for precise-lazyloaded
* `<img>` elements. Inline data URI (~120 bytes) so it parses zero-RTT
* regardless of cache state. The real image URL lives in
* `data-precise-src` and is swapped in by the global IntersectionObserver
* script (`precise-lazyload.astro`) once the element enters the rootMargin
* window.
*/
export const PRECISE_LAZY_PLACEHOLDER =
'data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201%201%22%2F%3E';