Notebook & chapters
Engine Components · live from GitHub
Pin a heading, or select text and use the floating pin to save your place.
View source ↗

EngineImage + EngineVideo

Schema types: "image" and "video".

These components defer network-heavy media until it is near the viewport while reserving layout space so media does not need to be mounted eagerly just to avoid CLS.

EngineImage

EngineImage wraps Next.js image optimization. The component itself uses an IntersectionObserver; priority: true bypasses that viewport gate.

ts
{
	type: "image",
	props: {
		src: "/hero.jpg",
		alt: "Product preview",
		width: 1600,
		height: 900,
		qualityPreset: "balanced",
		rounded: "1rem",
	},
}

Props

Prop

Type

Default

Notes

src

string

required

Next.js-compatible image source

alt

string

required

Accessible alternative text

width / height

number

800 / 600 fallback

Used for intrinsic size, preload-distance estimation, and automatic aspect-ratio reservation when both are supplied

fill

boolean

false

Uses Next Image fill mode

priority

boolean

false

Mount immediately instead of waiting for IntersectionObserver

quality

number

preset value

Explicit Next image quality

qualityPreset

`"performance" \

"balanced" \

"sharp"`

qualityMobile

number

resolved normal quality

Mobile quality for <768px; only overrides the mobile side

qualityDesktop

number

resolved normal quality

Desktop quality for >=768px; only overrides the desktop side

objectFit

CSS object-fit value

"cover"

Applied to the rendered image

aspectRatio

string

inferred from width / height when possible

Explicit layout reservation; overrides the inferred ratio

sizes

string

generated

Passed to Next image selection

blurDataURL

string

—

Optional blur-up placeholder

rounded

`boolean \

string`

—

caption

string

—

Wraps image in <figure> with <figcaption>

Layout reservation and fluid sizing

When a non-fill image provides positive numeric width and height, EngineImage uses those dimensions to reserve the same aspect ratio before the actual image mounts. This also applies to small images that do not receive an outer LazyMount, so known dimensions do not need a separate aspectRatio prop just to avoid a layout jump.

The rendered image remains fluid inside that reserved wrapper: intrinsic width/height values still drive Next.js image optimization and candidate selection, but the DOM image scales to the wrapper width instead of remaining at its intrinsic pixel width and being clipped by the wrapper.

An explicit aspectRatio still wins when the visual crop/container ratio should be different from the source dimensions.

For fill: true, the responsive-quality <picture> path preserves the geometry and positioning styles generated by Next's getImageProps(). Enabling qualityMobile or qualityDesktop therefore does not accidentally remove the fill behavior.

Responsive quality does not render two images

When qualityMobile or qualityDesktop is set, EngineImage uses a real <picture> with media-specific <source> elements and one fallback <img>. The browser selects one candidate. NE does not render two CSS-hidden Next Image elements, so responsive quality does not intentionally double-download the asset.

The generated mobile/desktop Next image candidate data is memoized from the request-defining props. Placeholder/opacity state changes therefore do not rebuild both srcset payloads just because the image finished loading.

Viewport quality overrides are independent. If only qualityMobile is supplied, the desktop side inherits the normal resolved quality/qualityPreset; if only qualityDesktop is supplied, mobile inherits the same normal resolved quality. Enabling one override therefore does not silently change compression on the other viewport.

ts
props: {
	src: "/feature.png",
	alt: "Feature",
	width: 1400,
	height: 900,
	qualityMobile: 65,
	qualityDesktop: 86,
}

Readiness is tied to the current src. Replacing the image source immediately returns the wrapper to its placeholder/transparent state until the new source finishes loading; it does not inherit the previous image's ready state.

Placeholder motion

The fallback shimmer is disabled when the user requests prefers-reduced-motion: reduce. Blur-data-URL placeholders remain static and continue to provide the same layout/readiness behavior.

Viewport distance

The built-in observer starts mounting larger images earlier:

Estimated area

Root margin

small

200px

around 800×600

400px

around 1280×720

600px

around 1920×1080+

800px

The schema renderer may additionally defer very large image nodes. Small images skip the outer LazyMount; EngineImage's own observer remains responsible for the media request.

EngineVideo

EngineVideo does not create the <video> element until its wrapper is within rootMargin of the viewport.

ts
{
	type: "video",
	props: {
		src: "/demo.mp4",
		poster: "/demo-poster.jpg",
		aspectRatio: "16/9",
		controls: true,
	},
}

Props

Prop

Type

Default

Notes

src

`string \

VideoSource[]`

required

poster

string

—

Poster shown before playback

aspectRatio

string

"16/9"

Reserves space before mounting

autoPlay

boolean

false

Passed to the native video element once mounted

muted

boolean

true

Browser-friendly autoplay default

loop

boolean

false

Native video loop

controls

boolean

true

Native controls

playsInline

boolean

true

Native inline playback hint

preload

`"none" \

"metadata" \

"auto"`

rootMargin

string

"800px 0px"

How early the video element mounts

eager

boolean

false

Mount immediately

Non-autoplay videos do not show a fake perpetual buffering spinner while using preload="none". With the default configuration NE loads metadata near the viewport and leaves playback to the user. Autoplay videos default to preload="auto" and show the loading indicator until playback is ready.

preload remains a browser hint, not a network guarantee. An explicit value is passed through unchanged, but autoPlay: true also sets the native autoplay attribute; browsers may therefore fetch enough media to honor the playback request even when preload="none" was supplied.

Poster ownership

Before the video mounts, EngineVideo may render its lightweight poster surface so the reserved media area is not blank. Once a normal non-autoplay <video> is mounted, the native poster attribute owns that surface instead of keeping a second poster <img> stacked over the media element.

Autoplay keeps the external poster only while the video is intentionally transparent and waiting to become playable.

Changing video sources

Updating src after EngineVideo has mounted creates a fresh native media element keyed by the ordered source list. This matters because changing React <source> children alone does not reliably make an existing <video> run the HTML media resource-selection algorithm again.

On a source change NE:

  1. stops/removes the old media element through React replacement;
  2. resets ready/buffering state for the wrapper;
  3. mounts the new ordered <source> list;
  4. lets the native element apply the current preload/autoplay policy.

The source key is based on source URL, MIME type, and order, so recreating an array with identical entries does not intentionally restart playback. Source normalization and the fingerprint are memoized so readiness/buffering rerenders do not repeatedly rebuild the same source list.

Automatic lazy policy

The schema renderer currently uses these additional rules:

  • video nodes are lazy-mounted with an 800px root margin.
  • large image nodes are lazy-mounted; small images rely on EngineImage's own observer.
  • nested canvas, manim, and manim3d nodes are lazy-mounted.
  • ordinary nested sections/heroes use content-visibility for sufficiently large subtrees instead of being removed from React solely because they are nested.
  • lazy: false, priority: true, or eager: true forces eager rendering.
  • lazy: true explicitly requests lazy mounting.

Schema nesting depth is not treated as proof that content is below the fold.

EngineImage + EngineVideo | Kastrick