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.
{
type: "image",
props: {
src: "/hero.jpg",
alt: "Product preview",
width: 1600,
height: 900,
qualityPreset: "balanced",
rounded: "1rem",
},
}Props
Prop | Type | Default | Notes |
|---|---|---|---|
|
| required | Next.js-compatible image source |
|
| required | Accessible alternative text |
|
|
| Used for intrinsic size, preload-distance estimation, and automatic aspect-ratio reservation when both are supplied |
|
|
| Uses Next Image fill mode |
|
|
| Mount immediately instead of waiting for IntersectionObserver |
|
| preset value | Explicit Next image quality |
| `"performance" \ | "balanced" \ | "sharp"` |
|
| resolved normal quality | Mobile quality for |
|
| resolved normal quality | Desktop quality for |
| CSS |
| Applied to the rendered image |
|
| inferred from | Explicit layout reservation; overrides the inferred ratio |
|
| generated | Passed to Next image selection |
|
| — | Optional blur-up placeholder |
| `boolean \ | string` | — |
|
| — | Wraps image in |
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.
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 |
|
around 800×600 |
|
around 1280×720 |
|
around 1920×1080+ |
|
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.
{
type: "video",
props: {
src: "/demo.mp4",
poster: "/demo-poster.jpg",
aspectRatio: "16/9",
controls: true,
},
}Props
Prop | Type | Default | Notes |
|---|---|---|---|
| `string \ | VideoSource[]` | required |
|
| — | Poster shown before playback |
|
|
| Reserves space before mounting |
|
|
| Passed to the native video element once mounted |
|
|
| Browser-friendly autoplay default |
|
|
| Native video loop |
|
|
| Native controls |
|
|
| Native inline playback hint |
| `"none" \ | "metadata" \ | "auto"` |
|
|
| How early the video element mounts |
|
|
| 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:
- stops/removes the old media element through React replacement;
- resets ready/buffering state for the wrapper;
- mounts the new ordered
<source>list; - 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:
videonodes are lazy-mounted with an800pxroot margin.- large
imagenodes are lazy-mounted; small images rely on EngineImage's own observer. - nested
canvas,manim, andmanim3dnodes are lazy-mounted. - ordinary nested sections/heroes use
content-visibilityfor sufficiently large subtrees instead of being removed from React solely because they are nested. lazy: false,priority: true, oreager: trueforces eager rendering.lazy: trueexplicitly requests lazy mounting.
Schema nesting depth is not treated as proof that content is below the fold.