nanoraster
0.7.1

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.

Open Markdown

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.

On this page