Reuse the renderer
Keep one GPU device alive across renders with createRenderer, dispose it deliberately, and know when a single plan call is the better tool.
To bring up a GPU device costs more than most renders. The module-level
functions pay that cost once per process. The first call creates a shared
renderer, later calls reuse it, and calls run in call order.
That renderer uses the default 'high-performance' power preference and lives
until the process exits. createRenderer gives you a renderer whose device
you choose and release.
1. Create once, render many
import { createRenderer } from 'nanoraster';
const renderer = await createRenderer({ powerPreference: 'low-power' });
for await (const model of modelsAsTheyArrive) {
const image = await renderer.renderImage(model.glb, { format: 'webp', width: 512 });
await store(model.id, image);
}The handle's render methods mirror the module-level functions exactly: same
options, same results, byte-identical pixels on the same adapter. The handle's
device is separate from the shared one. Calls on the handle run in sequence, so
concurrent callers can share one handle. 'low-power' asks for the lower-power
adapter on dual-GPU machines, which suits small, frequent renders. The host may
ignore the hint.
2. Dispose deliberately
using renderer = await createRenderer();
const image = await renderer.renderImage(glb, { format: 'webp' });
// renderer.dispose() runs automatically at scope exit.The renderer holds a GPU device until you release it. The shared renderer has no
release, by design. Tie your renderer to the worker or process that owns it:
dispose on shutdown, or on an idle timer if the host keeps workers warm.
dispose() returns at once. It marks the renderer disposed and schedules the
device teardown behind the queued calls; nothing signals when that teardown
finishes. Every later call rejects with a RenderError whose code is 'gpu',
so recreate the renderer rather than retry the call.
3. Verify the reuse when it matters
const results = await renderer.renderImages(glb, {
format: 'webp',
timings: true,
views: [{ id: 'iso' }],
});
console.log(results.timings.capBuild, results.timings.views[0].render);timings: true on a plan call attaches stage timings: a warm renderer reports
zero adapterDeviceRequests, one shared presentationBuilds, and per-view
work.
Variations
One plan call beats a loop. If you can write the full list of images (a
contact sheet, a resolution ladder, a turntable), make one renderImages
call on whichever layer you hold. That call parses and uploads the GLB once
and pipelines the views, about three times faster than the same views as
one-shot renders. Render multiple views is that
guide.
Device loss. A driver reset, or a browser that reclaims the GPU, invalidates the device. The call that hits the loss fails with a GPU-class code. The next call rebuilds the device and re-uploads, so keep the last good image and retry on the same handle.
One renderer per worker. A renderer is single-realm: create it inside the worker that uses it and post bytes out, never the handle. Render in the browser shows the worker shape.
Live viewers. An orbit at 60 frames per second belongs to a canvas renderer such as three.js. nanoraster's job is stills whose bytes are the product: thumbnails, CI baselines, publications, agent captures.
Render multiple views
Declare an ordered plan of identified camera views — with per-view size and format overrides — and render it in one call from one GLB.
Format and annotate
Choose the encoder, background, and quality, then add the axis indicator, label, and scale bar that let a render stand on its own.