Performance
Rendering performance comes from three levers: the right backend, aggressive culling, and moving work off the main thread. The editor auto-tunes all three via capability detection, but you can drive each one directly.
Pick a backendโ
detectCapabilities chooses between "webgl2", "offscreen", and
"canvas2d" by probing the runtime (WebGPU currently maps to webgl2;
OffscreenCanvas + Worker enables offscreen). Let it pick, or force a backend
via the capabilities prop for benchmarking. Low-level probes
(isWebGL2Available, pickAvailableBackend) live in
@oh-just-another/renderer-canvas.
import { Editor, detectCapabilities } from "@oh-just-another/editor";
const profile = await detectCapabilities();
console.log(profile.renderer); // "webgl2" | "offscreen" | "canvas2d"
// Force a backend:
<Editor capabilities={{ renderer: "canvas2d" }} />;
Cull and cacheโ
Pass a viewport, a shared boundsCache, and (for big scenes) a
spatialIndex to renderScene so frame cost tracks on-screen content rather
than scene size. See the culling page for the full set of options.
import { renderScene } from "@oh-just-another/renderer-core";
renderScene(scene, target, { viewport, boundsCache, spatialIndex });
Off-thread renderingโ
Hosts that wire up WorkerPool + transferCanvasToWorker can render layers in
OffscreenCanvas workers, and renderViaTiles pre-rasterizes tiles for very
large scenes. WORKER_AUTO_THRESHOLD (5,000 elements) is where off-thread work
starts to beat the postMessage overhead; LARGE_SCENE_WORKER_THRESHOLD
(50,000) marks where a full main-thread render starts dropping frames even
with culling.
import { WORKER_AUTO_THRESHOLD } from "@oh-just-another/renderer-core";
import { WorkerPool, LARGE_SCENE_WORKER_THRESHOLD } from "@oh-just-another/renderer-canvas";
if (scene.elements.size > WORKER_AUTO_THRESHOLD) {
// worker-backed rendering becomes worthwhile
}
Level of detailโ
LodOptions lets cheaper paths kick in at low zoom: below placeholder zoom,
shapes degrade to flat AABB fills; below hideText zoom, text layout and
glyph drawing are skipped. Pass it via RenderSceneOptions.lod; the defaults
are exported as DEFAULT_LOD (placeholder: 0.15, hideText: 0.4).
import { renderScene, type LodOptions } from "@oh-just-another/renderer-core";
const lod: LodOptions = { placeholder: 0.1, hideText: 0.4 };
renderScene(scene, target, { viewport, lod });