website: move placeholders to src/assets for Vite base64 inlining, add LazyVideo component

Placeholders are now generated into src/assets/placeholders/ instead of
public/ so Vite's asset pipeline processes them. Since all are < 4KB,
Vite auto-inlines them as base64 data URIs via assetsInlineLimit — zero
extra HTTP requests for placeholders.

- Static imports resolve to data:image/png;base64,... at build time
- JSDoc on placeholder props documents why static imports are required
  (synchronous availability, Vite inlining) and warns against dynamic
  imports and public/ paths
- New LazyVideo component: same pixelated poster pattern, uses native
  <video preload="none" loading="lazy"> for zero-JS lazy loading
This commit is contained in:
Tommy D. Rossi
2026-02-21 15:58:21 +01:00
parent 3b3959f5ab
commit ee948930fe
7 changed files with 155 additions and 5 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

+141
View File
@@ -434,6 +434,20 @@ export function PixelatedImage({
style,
}: {
src: string;
/**
* URL of the tiny pixelated placeholder image. Use a static import so Vite
* inlines it as a base64 data URI (all placeholders are < 4KB, well under
* Vite's default assetsInlineLimit of 4096 bytes). This makes the
* placeholder available synchronously on first render with zero HTTP
* requests. Do NOT use dynamic imports or public/ paths — dynamic imports
* add a microtask delay, and public/ files bypass Vite's asset pipeline.
*
* @example
* ```tsx
* import placeholderScreenshot from "../assets/placeholders/placeholder-screenshot@2x.png";
* <PixelatedImage placeholder={placeholderScreenshot} src="/screenshot@2x.png" ... />
* ```
*/
placeholder: string;
alt: string;
width: number;
@@ -504,6 +518,133 @@ export function PixelatedImage({
);
}
/* =========================================================================
Lazy video with pixelated poster placeholder
Same visual pattern as PixelatedImage but for <video> elements.
Poster layers (pixelated → real) show through the transparent video element.
Video uses native loading="lazy" + preload="none" so zero bytes are
downloaded until the element is near the viewport and the user clicks play.
No custom IntersectionObserver needed — all native HTML attributes.
========================================================================= */
export function LazyVideo({
src,
poster,
placeholderPoster,
width,
height,
type = "video/mp4",
className = "",
style,
}: {
src: string;
poster: string;
/**
* URL of the tiny pixelated poster placeholder. Use a static import so Vite
* inlines it as a base64 data URI (all placeholders are < 4KB, well under
* Vite's default assetsInlineLimit of 4096 bytes). This makes the
* placeholder available synchronously on first render with zero HTTP
* requests. Do NOT use dynamic imports or public/ paths — dynamic imports
* add a microtask delay, and public/ files bypass Vite's asset pipeline.
*
* @example
* ```tsx
* import placeholderPoster from "../assets/placeholders/placeholder-demo-poster.png";
* <LazyVideo placeholderPoster={placeholderPoster} poster="/demo-poster.png" ... />
* ```
*/
placeholderPoster: string;
width: number;
height: number;
type?: string;
className?: string;
style?: React.CSSProperties;
}) {
const [posterLoaded, setPosterLoaded] = useState(false);
// Handles cached poster images (same pattern as PixelatedImage)
const posterRef = useCallback((img: HTMLImageElement | null) => {
if (img?.complete && img.naturalWidth > 0) {
setPosterLoaded(true);
}
}, []);
return (
<div
className={className}
style={{
position: "relative",
width: "100%",
maxWidth: `${width}px`,
aspectRatio: `${width} / ${height}`,
overflow: "hidden",
...style,
}}
>
{/* Pixelated poster placeholder: loads instantly (~500 bytes) */}
<img
src={placeholderPoster}
alt=""
aria-hidden
width={width}
height={height}
style={{
position: "absolute",
inset: 0,
width: "100%",
height: "100%",
objectFit: "cover",
imageRendering: "pixelated",
zIndex: 0,
}}
/>
{/* Real poster: fades in over the pixelated placeholder */}
<img
ref={posterRef}
src={poster}
alt=""
aria-hidden
width={width}
height={height}
onLoad={() => {
setPosterLoaded(true);
}}
style={{
position: "absolute",
inset: 0,
width: "100%",
height: "100%",
objectFit: "cover",
opacity: posterLoaded ? 1 : 0,
transition: "opacity 0.4s ease",
zIndex: 1,
}}
/>
{/* Video: transparent until playing, native lazy + no preload.
Controls float on top of poster layers. No poster attr needed
because the img layers handle the visual placeholder.
loading="lazy" is a newer HTML attr not yet in React's TS types. */}
<video
controls
preload="none"
{...{ loading: "lazy" } as React.VideoHTMLAttributes<HTMLVideoElement>}
width={width}
height={height}
style={{
position: "relative",
width: "100%",
height: "100%",
objectFit: "cover",
zIndex: 2,
background: "transparent",
}}
>
<source src={src} type={type} />
</video>
</div>
);
}
/* =========================================================================
Chart placeholder (dark box with animated line)
========================================================================= */
+2 -1
View File
@@ -20,6 +20,7 @@ import {
Li,
PixelatedImage,
} from "website/src/components/markdown";
import placeholderScreenshot from "../assets/placeholders/placeholder-screenshot@2x.png";
export const meta: MetaFunction = () => {
const title = "Playwriter - Control your Chrome with Playwright API";
@@ -73,7 +74,7 @@ export default function IndexPage() {
<div className="bleed" style={{ display: "flex", justifyContent: "center" }}>
<PixelatedImage
src="/screenshot@2x.png"
placeholder="/placeholder-screenshot@2x.png"
placeholder={placeholderScreenshot}
alt="Playwriter controlling Chrome with accessibility labels overlay"
width={1280}
height={800}