Web worker rendering
By default the SDK renders on the main thread. This works well for small-to-medium PDFs, but on large documents or low-powered devices (especially mobile), page rendering can block the UI thread and cause jank during scroll, zoom, or thumbnail generation. The SDK supports offloading PDF rendering to a Web Worker. When enabled, page rendering runs inside the worker, off the main thread, and the engine is loaded a second time there (see the memory considerations below).
What moves. Only rendering. Drawing, annotations, text editing, form filling and page management stay on the main thread, and all tools and the public API remain unchanged. After each edit (drawing a line, adding an annotation, editing text, etc.), the SDK brings the worker's copy of the document up to date automatically, in the background.
Example
import { Kernstra, PDFViewer } from '@kernstra/pdf-web-sdk'; // Option A — copy to your static assets (works with every bundler, and with none: // see Installation, Without a bundler): // cp node_modules/@kernstra/pdf-web-sdk/dist/render.worker.js public/assets/render.worker.js await Kernstra.init({ license: import.meta.env.KERNSTRA_LICENSE, workerUrl: '/assets/render.worker.js', }); // Option B — Vite ?url import (Vite emits the file and returns its URL), instead of A: // import RenderWorkerUrl from '@kernstra/pdf-web-sdk/render-worker?url'; // await Kernstra.init({ license: import.meta.env.KERNSTRA_LICENSE, workerUrl: RenderWorkerUrl }); // The worker is on by default when workerUrl is set. new PDFViewer(container, doc); // To keep rendering on the main thread (low-memory devices, or where Web Workers are restricted): // new PDFViewer(container, doc, { performance: { workerEnabled: false } });
What runs on the worker vs. main thread
| Operation | Thread | Why |
|---|---|---|
| Page rendering (scroll, zoom, initial load) | Worker | Frees the UI thread |
| Thumbnail rendering | Worker | Prevents blank thumbnails on large PDFs |
| Tool mutations (draw, annotate, edit text) | Main thread | Edits change the document on the main thread, synchronously |
| Immediate re-render after mutation | Main thread | The tool needs instant feedback |
| Print (300 DPI) | Main thread | Sequential per-page, acceptable |
| Region render (Edit Text overlays) | Main thread | Drawn from the pages the tool has open |
Notes
Setup: 1. Point the SDK at the worker bundle. The package ships a prebuilt, self-contained worker bundle at dist/render.worker.js, exposed via the ./render-worker export subpath. It is an ES module — it must be served as a same-origin URL (Web Workers cannot be loaded from bare package specifiers at runtime). The subpath carries an empty declaration file, so a compiler that resolves it finds types rather than untyped JavaScript; the script itself exports nothing. Option A copies it to your static assets and passes its path as workerUrl; Option B uses Vite's ?url import (both in the example above). The ?url suffix is Vite's: TypeScript knows it once vite/client is in your tsconfig.json types, or referenced from a .d.ts your program includes (/// <reference types="vite/client" /> in vite-env.d.ts — Vite's scaffold has it — or any .d.ts in your program); without that the import is the error "Cannot find module '@kernstra/pdf-web-sdk/render-worker?url'". A bundler without the suffix — webpack 5, the Angular CLI, esbuild, Rollup — serves the file as a static asset instead: see webpack 5 and Angular.
Setup: 2. Configure the viewer. The worker is enabled by default when a workerUrl is provided (see the fallback behavior below for what it is given under each license). No viewer configuration changes are needed: new PDFViewer(container, doc) — worker rendering is active automatically. To keep rendering on the main thread — the engine then loads once instead of twice (low-memory devices), or where Web Workers are restricted: performance: { workerEnabled: false } (Performance).
Fallback behavior. The SDK gracefully falls back to main-thread rendering in these scenarios. The fallback is transparent to the API — nothing else changes. The SDK never throws because of a worker failure.
No workerUrl provided — worker is never created. workerEnabled: false — worker is explicitly disabled. Worker API unavailable (older environments) — detected at runtime.
The license keeps edits from it — the worker renders from a copy of the document. Under a production license that includes export with clean output it holds the document as edited, synced after every change. Under any other license — a development token, a license without export, an unverified license after an outage — it holds only the bytes you handed over (with the password they were opened with), never the document as edited: at the first change to the document (a tool's edit, a page operation, an undo, a write through the document API) the worker is stopped and every page renders on the main thread from then on, and a document already changed — by an earlier viewer, or by the document API before the viewer was built — starts no worker. Viewing, the common case on a phone, runs on the worker under every license. See Licensing.
A progressively loaded document — renders on the main thread until every byte has arrived, then moves to the worker.
Worker initialization fails (the worker file cannot be loaded, or its content security policy refuses the engine) — reported as a non-fatal RENDER_FAILED on the error event and logged; rendering continues on the main thread. Individual render fails on worker — that page renders on the main thread instead, if it is still on screen (a page that scrolled away re-renders when it comes back).
The worker hangs, crashes or fails — a command that gets no answer for 30 s, an uncaught error in the worker, or an ENGINE_FAILED answer: the worker is stopped and dropped, a non-fatal RENDER_FAILED is reported once on the error event ("The render worker stopped; rendering continues on the main thread"), and every page renders on the main thread from then on.
After an edit. After each change to the PDF (drawing, annotation, text edit, page operation, etc.) the page is re-rendered on the main thread, for instant visual feedback, and the worker's copy of the document is updated in the background, without blocking — under a license with export and clean output; under any other the worker, which holds the bytes you handed over, is stopped and every page renders on the main thread from then on. Later scroll and zoom renders use the worker with the updated document. If a render is requested while that update is still in flight, it waits for the worker's copy to catch up and then renders on the worker.
Memory considerations. Enabling worker rendering keeps the engine (~4.4 MB) in memory twice — once on the main thread and once in the worker. It is downloaded only once: the worker takes the bytes from the page. The worker also holds its own copy of the loaded document. On low-memory devices, you can disable worker rendering to save memory: performance: { workerEnabled: false }.
Verifying worker is active. Open Chrome DevTools → Sources → Workers. When worker rendering is active, you'll see the worker script listed there; if it is not, check the conditions under the fallback behavior above.
Content security policy. The worker's policy is the one its own response carries: serve render.worker.js with a Content-Security-Policy that has script-src 'self' 'wasm-unsafe-eval', since the SDK's engine is compiled inside the worker too; the page's policy needs worker-src 'self' — see Installation and support.