@vectorvesper/motion/reactReact · Scene Policy Engine

useSceneGate

A heavy scene requires four distinct runtime decisions: should it exist yet, should it be drawing, how much fidelity should it render, and is its graphics context still alive. A page that answers only the first leaves a WebGL canvas burning battery and frames for a visitor who scrolled past ten seconds ago.

Six scenes, one context loss

A driver reset or a GPU switch does not take one canvas. It takes all of them, silently.

renderer healthygen 0
waiting for headroom
northdormant · 0
waiting for headroom
deltadormant · 0
waiting for headroom
vectordormant · 0
waiting for headroom
signaldormant · 0
waiting for headroom
vesperdormant · 0
waiting for headroom
atlasdormant · 0

Nothing in these cards handles the loss. Each one reports it and is keyed on generation. Remove the key and they stay black.

The useSceneGate hook makes all four lifecycle decisions and reports them through a typed state snapshot. Applying them is your responsibility: nothing here directly touches a renderer, pixel ratio, or post-processing pass, giving you full control over what "full" and "reduced" look like in your application.

The hook composes useSafeToMount (headroom and core floor verification) and useAdaptiveQuality (device floor + render pressure attribution) with proximity intersection observing and WebGL context-loss recovery.

When a user scrolls past a scene, the gate enters "idle" state: the render loop pauses while GPU textures, compiled shaders, and buffers remain intact in memory. Once the user scrolls back, resuming rendering takes a single subscriber tick rather than a catastrophic multi-frame context rebuild.

Interactive Lifecycle & WebGL Context Playground

Demo loads on scroll

Quick start

Attach the returned ref to your scene container and conditionally render your WebGL canvas with the generation key.

The generation counter increments when WebGL context loss occurs, automatically remounting a fresh canvas.

The Seven Scene Lifecycle States

The complete state machine governing scene lifecycle, visibility, quality, and context restoration.

StateMountedQualityDescription
"dormant"falsenullToo far from viewport to warrant initialization.
"warming"falsenullWithin preload proximity threshold; waiting for frame headroom.
"active"true"full"Visible and running at full visual fidelity.
"constrained"true"reduced"Running with reduced geometry, resolution, or post-processing.
"idle"true"full" | "reduced"Started but currently off-screen. Render loop paused; assets retained.
"recovering"true"reduced"WebGL context lost. Increments generation to re-create canvas.
"poster"falsenullHardware floor or reduced-motion disables 3D scene entirely.

API Reference

Syntax

Monitors viewport proximity, main-thread headroom, render pressure, and WebGL context health.

Parameters

Parameters.fieldTypeDefaultDescription
options.cost"light" | "normal" | "heavy""heavy"Complexity tier passed through to useSafeToMount to determine required headroom.
options.preloadnumber200Distance in pixels before the viewport to transition from dormant to warming.
options.labelstringundefinedDiagnostic label for DevTools and telemetry inspectors.
options.contentbooleanfalseFor a section the reader came for, like a chart, rather than decoration. It still waits to be near, for the scroll to settle and for frame headroom, then mounts in the constrained state even under reduced motion or on a device below the floor, where a scene would get its poster. Needs 4.2.0 or newer.

Return Value

SceneGate<T>

Contains ref, state, mounted, quality, generation, cause, and reason.

Properties & State

Properties.fieldTypeDescription
scene.refRefObject<T | null>Hybrid ref attached to the container element whose position determines proximity.
scene.stateSceneState"dormant" | "warming" | "active" | "constrained" | "idle" | "recovering" | "poster"
scene.mountedbooleanWhether the scene should exist in the DOM (true for active, constrained, idle, recovering).
scene.quality"full" | "reduced" | nullFidelity setting when mounted; null when dormant, warming, or poster.
scene.generationnumberContext recovery counter. Pass to <Canvas key={scene.generation}>.
scene.causeSceneCause"ok" | "not-near" | "waiting-for-headroom" | "off-screen" | "render-bound" | "frame-rate" | "device-floor" | "reduced-motion" | "context-lost"
scene.reasonstringHuman-readable diagnostic string explaining the current verdict.

WebGL Context Loss Recovery

Automatic recovery mechanism for driver crashes, backgrounded tabs, or GPU context eviction.

Generation Key PatternClean React Tree Reconstruction

Lifecycle & Invariant Guarantees

  • Zero unmount thrashing: Once a scene starts, scrolling off-screen transitions to idle rather than tearing down WebGL contexts.
  • One reset is one rebuild: A driver reset takes every canvas on the page at once. Losses arriving together are treated as the same reset, so generation increments once and the page rebuilds once, however many scenes were affected. Requires 4.0.3 or newer. From 4.2.0 a replacement lost moments after that rebuild counts as a new failure and rebuilds again, at most three times in a row.
  • A forgotten reportHealthy costs a warning, not the scene: If nothing reports healthy within 5 seconds of a rebuild, the runtime assumes it worked, leaves recovering and warns once, naming the call to add. Before, the next loss was swallowed and the canvas stayed black. Needs 4.2.0 or newer.
  • Automatic context healing: Recovers seamlessly from WebGL context losses by incrementing generation without requiring page reloads.
  • Attribution-accurate quality: Only degrades quality when rendering is the bottleneck; ignores unrelated main-thread CPU blocking. From 4.2.0 the worst tier reduces quality too, where it used to fall through to full.
  • Hydration safety: Starts dormant/poster during SSR and initial hydration, preventing hydration mismatch errors.

Architectural Principles of Scene Gating

1. Scrolling past is never a reason to destroy resources

Unmounting a WebGL scene throws away the graphics context, vertex buffers, uploaded textures, and compiled shader programs. Coming back forces the browser and GPU driver to re-compile and re-upload everything from scratch at the exact moment the user is looking. On mobile devices, this causes severe multi-second stutter. The gate pauses the render loop in "idle" without dropping memory.

2. Reduced quality only fixes rendering bottlenecks

If third-party analytics scripts or JSON parsing blocks the main thread, reducing particle counts or shader resolution does not speed up the CPU; it only makes the page uglier while remaining just as slow. The gate uses frame pressure attribution to degrade only when "render" is the diagnosed bottleneck.

Production Examples

Battle-tested production patterns ready to copy directly into your codebase.

Render an accessible, lightweight WebP still poster until the scene is near and the thread is calm.

When NOT to use this

The visual element is lightweight (CSS animation, simple 2D canvas).
→ instead Render directly. Policy engines add skeleton states and observers that are unnecessary for cheap elements.
You only need basic visibility intersection without headroom or quality management.
→ instead Use a standard IntersectionObserver directly.
You need headroom gating without position or scroll tracking.
→ instead Use useSafeToMount directly.

Rules & Gotchas

  • Attach the ref to the element whose visibility decides display: On sticky or pinned sequences, observe the sticky child rather than the tall parent scroll container.
  • The ref target may be rendered conditionally: Hybrid refs safely re-arm when Suspense fallbacks or dynamic loaders swap in the real DOM element.
  • Branch on state and cause, never on reason: reason is a human-readable string subject to phrasing changes. state, quality, and cause are strictly typed.
  • Always pass generation to the Canvas key: This ensures clean automatic recovery if the WebGL context is lost due to GPU driver reset or memory reclamation.

Related Motion Components

See it live in the lab: /lab/context-loss