By default Sheetwrite paints on the main thread (renderer: "canvas"). For sheets
where a busy main thread could stall scrolling, you can move painting onto a Web
Worker with an OffscreenCanvas — same Renderer contract, drop-in.
Enabling it
Set renderer: "worker" and point workerUrl at the worker module as the
browser will fetch it. The worker entry ships at
@sheetwrite/core/dist/worker.js (export subpath @sheetwrite/core/worker),
but the platform Worker/URL constructors do not consult package exports —
new URL("@sheetwrite/core/worker", import.meta.url) treats the bare
specifier as a relative path and produces a 404 URL unless your bundler
happens to rewrite that exact form. Two reliable recipes:
Universal (any bundler, no bundler): the worker entry is an ES module with
relative imports (./canvas-paint.js, …), so a single-file copy breaks — copy
the package's whole dist/ directory into your public/static assets and pass
the served worker.js URL (a module worker fetches its sibling imports over
HTTP):
import { createGridfunction createGrid(host: HTMLElement, opts: GridOptions): GridfunctioncreateGrid(host:HTMLElement,opts:GridOptions,):GridfunctioncreateGrid(host:HTMLElement,opts:GridOptions,):GridCreates and mounts an imperative Grid in the supplied host element.API reference →, initSheetwritefunction initSheetwrite(source?: BufferSource | URL | string | Request | WebAssembly.Module): Promise<void>functioninitSheetwrite(source?:BufferSource|URL|string|Request|WebAssembly.Module,):Promise<void>functioninitSheetwrite(source?:|BufferSource|URL|string|Request|WebAssembly.Module,):Promise<void>Load the WASM data engine once. Must be awaited before createGrid.API reference → } from"@sheetwrite/core";
awaitinitSheetwritefunction initSheetwrite(source?: BufferSource | URL | string | Request | WebAssembly.Module): Promise<void>functioninitSheetwrite(source?:BufferSource|URL|string|Request|WebAssembly.Module,):Promise<void>functioninitSheetwrite(source?:|BufferSource|URL|string|Request|WebAssembly.Module,):Promise<void>Load the WASM data engine once. Must be awaited before createGrid.API reference →();
constgridconst grid: Gridconstgrid:Gridconstgrid:GridThe imperative core grid this controller owns.API reference →=createGridfunction createGrid(host: HTMLElement, opts: GridOptions): GridfunctioncreateGrid(host:HTMLElement,opts:GridOptions,):GridfunctioncreateGrid(host:HTMLElement,opts:GridOptions,):GridCreates and mounts an imperative Grid in the supplied host element.API reference →(hostconst host: HTMLElementconsthost:HTMLElementconsthost:HTMLElement, {
workbookGridOptions.workbook: WorkbookinterfaceGridOptions {workbook:Workbook;}interfaceGridOptions {workbook:Workbook;}Live workbook schema adopted by the store and updated by document operations.API reference →,
datasourceGridOptions.datasource?: DataSource | undefinedinterfaceGridOptions {datasource?:DataSource|undefined;}interfaceGridOptions {datasource?:DataSource|undefined;}Lazy row provider requested for visible windows; use instead of eager data.API reference →,
rendererGridOptions.renderer?: "worker" | "canvas" | undefinedinterfaceGridOptions {renderer?:"worker"|"canvas"|undefined;}interfaceGridOptions {renderer?:|"worker"|"canvas"|undefined;}Paint backend; defaults to main-thread canvas and falls back there if a worker fails.API reference →: "worker",
workerUrlGridOptions.workerUrl?: string | URL | undefinedinterfaceGridOptions {workerUrl?:string|URL|undefined;}interfaceGridOptions {workerUrl?:string|URL|undefined;}URL of the worker renderer module (renderer: "worker"), as served to the BROWSER — the platform Worker constructor does not consult package exports, so a bare specifier like new URL("@sheetwrite/core/worker", import.meta.url) is NOT reliable. Either copy @sheetwrite/core/dist/worker.js to your public assets and pass its URL string (works everywhere), or use your bundler's dependency-worker import if it has one (see /docs/guides/worker-rendering/). If omitted or the worker can't be constructed, the grid falls back to the main-thread canvas renderer and emits renderer-fallback once.API reference →: "/sheetwrite/worker.js",
});
Vite dependency-worker import (machine-verified): Vite bundles the whole
worker graph into one chunk and returns its URL via the ?worker&url query —
verified by the fixture build in test/bundler-fixtures/vite
(bun run verify:bundlers):
webpack 5 / Next.js have no equivalent URL-returning dependency-worker import
(webpack only bundles a worker graph for a literal
new Worker(new URL(...)) expression, which Sheetwrite constructs internally)
— use the public-copy recipe above there.
workerUrl accepts a string | URL. If you omit it, the worker renderer
resolves ./worker.js relative to its own module — that only works when your
bundler preserves module URLs (it usually does not after bundling to one file).
Verify it actually started
A worker that fails to construct falls back to the main-thread canvas renderer
(see below) — verify instead of assuming:
consolevar console: Consolevar console:Consolevar console:Console.warnConsole.warn(...data: any[]): void (+1 overload)Console.warn(...data: any[]): void (+1 overload)Console.warn(...data: any[]): void (+1 overload)The console.warn() static method outputs a warning message to the console at the 'warning' log level. MDN Reference("Sheetwrite worker renderer unavailable, using canvas:", errorerror: unknownlet error:unknownlet error:unknown);
});
if (gridconst grid: Gridconstgrid:Gridconstgrid:GridThe imperative core grid this controller owns.API reference →.rendererKindGrid.rendererKind(): "canvas" | "worker"Grid.rendererKind(): "canvas"|"worker"Grid.rendererKind(): "canvas"|"worker"Which renderer is actually active: "worker" when the OffscreenCanvas worker constructed successfully, "canvas" otherwise (including after a renderer-fallback).API reference →() !=="worker") {
// main-thread rendering is active
}
How it works
On mount the renderer constructs the Worker, creates a <canvas>, and
transfers control of it to the worker with transferControlToOffscreen(). The
worker paints into that OffscreenCanvas off the main thread.
Layout, theme, viewport, and each painted window are posted to the worker as
messages. The window snapshot is the same typed-array-backed VisibleWindowView
the store produces, so it is cheap to transfer.
Painting therefore continues smoothly even when the main thread is busy.
Automatic fallback
The worker is constructed first, before the canvas is transferred. If
construction throws — no OffscreenCanvas support, or a bundler/security
restriction on the worker URL — the grid silently falls back to the main-thread
CanvasRenderer. You always get a working grid; worker rendering is an
enhancement, never a hard requirement.
Limitation: no custom renderers
Custom cell renderers are functions, and functions can not cross the worker
boundary. Under renderer: "worker" the renderers option (and
defineCellRenderer) have no effect — the worker's renderer registry is always
empty. If you rely on custom CellRenderers,
use the main-thread canvas renderer.