CtrlK
BlogDocsLog inGet started
Tessl Logo
SkillPublic

web-workers

Web Worker patterns used throughout the Lichtblick codebase: Comlink integration, ComlinkWrap lifecycle, transfer handlers, OffscreenCanvas, SharedWorker isolation, and testing utilities.

Quality

78%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide
SecuritybySnyk

Passed

No findings from the security scan

Fix and improve this skill with Tessl

tessl review fix ./.github/skills/web-workers/SKILL.md
SKILL.md
Quality
Evals
Security

Web Workers Skill

Standard Pattern: ComlinkWrap

All Worker communication in Lichtblick uses Comlink with the ComlinkWrap utility for safe lifecycle management.

Worker Creation (main thread)

import { ComlinkWrap } from "@lichtblick/den/worker";

const worker = new Worker(
  new URL("./MyWorker.worker", import.meta.url),  // webpack-compatible URL
);

const { remote, dispose } = ComlinkWrap<MyWorkerAPI>(worker);

// Use the remote API
const result = await remote.process(data);

// Cleanup when done
dispose(); // releases Comlink proxy + terminates worker

Worker Implementation (worker thread)

import * as Comlink from "@lichtblick/comlink";

class MyWorkerImpl {
  async process(data: Uint8Array): Promise<Result> {
    // Heavy computation here
    return result;
  }
}

Comlink.expose(new MyWorkerImpl());

Key file: packages/den/worker/ComlinkWrap.ts

FinalizationRegistry Cleanup

ComlinkWrap returns a dispose function, but the project also uses FinalizationRegistry as a safety net:

const registry = new FinalizationRegistry<() => void>((dispose) => {
  dispose(); // Worker terminated when wrapper is garbage collected
});

// In constructor:
registry.register(this, dispose);

This prevents Worker leaks if the wrapping object is GC'd without explicit disposal.

Transfer Handlers

AbortSignal Transfer

import { abortSignalTransferHandler } from "@lichtblick/comlink-transfer-handlers";

// Register BEFORE any Comlink communication
Comlink.transferHandlers.set("abortsignal", abortSignalTransferHandler);

Allows passing AbortSignal across Worker boundaries — used by WorkerIterableSource to cancel iteration.

OffscreenCanvas Transfer

const offscreenCanvas = canvas.transferControlToOffscreen();

const { remote, dispose } = ComlinkWrap<RendererService>(worker);
await remote.init(
  Comlink.transfer(
    { canvas: offscreenCanvas, devicePixelRatio: window.devicePixelRatio },
    [offscreenCanvas],  // Transfer list
  ),
);

Used by: Plot panel (OffscreenCanvasRenderer), Chart component.

ArrayBuffer Transfer

// Transfer large binary data to Worker (zero-copy)
await remote.processData(Comlink.transfer(buffer, [buffer.buffer]));
// After transfer: buffer.byteLength === 0 (detached)

Worker URL Pattern (Webpack)

All Worker URLs use the import.meta.url pattern for webpack compatibility:

new Worker(new URL("./MyWorker.worker", import.meta.url));
  • File must be named *.worker.ts (webpack recognizes this pattern)
  • babel-plugin-transform-import-meta handles the URL resolution
  • Each Worker file is bundled as a separate chunk

SharedWorker Pattern

Used by UserScriptPlayer for script execution:

new SharedWorker(new URL("./transformerWorker/index", import.meta.url), {
  name: uuidv4(),  // Unique name prevents sharing between tabs
});
  • SharedWorker chosen for memory efficiency (shared code across script instances)
  • Unique name per instance prevents cross-tab Worker sharing (intentional isolation)

Testing Workers

makeComlinkWorkerMock

import { makeComlinkWorkerMock } from "@lichtblick/den/testing";

// Replace global Worker constructor with a mock that uses in-process Comlink
Object.defineProperty(global, "Worker", {
  writable: true,
  value: makeComlinkWorkerMock(() => new ActualImplementation()),
});

Located in packages/den/testing/makeComlinkWorkerMock.ts:

  • Creates an in-process Comlink channel (no actual Worker thread)
  • Allows unit testing Worker-based code without spawning real threads
  • Uses EventEmitter to simulate postMessage / onmessage

Workers in the Codebase

LocationPurposePattern
IterablePlayer/WorkerIterableSource.tsData source parsingComlinkWrap + AbortSignal
Plot/OffscreenCanvasRenderer.tsChart.js renderingComlinkWrap + OffscreenCanvas
Plot/builders/TimestampDatasetsBuilder.tsDataset buildingComlinkWrap + FinalizationRegistry
ThreeDeeRender/renderables/Images/WorkerImageDecoder.tsImage decodingComlinkWrap
UserScriptPlayer/index.tsScript executionSharedWorker + unique name
FoxgloveWebSocketPlayer/WorkerSocketAdapter.tsWebSocket I/ORaw Worker + postMessage
components/Chart/index.tsxLegacy chart renderingWebWorkerManager + Rpc

Performance Considerations

  1. Transfer vs Copy: Always use Comlink.transfer() for large ArrayBuffers
  2. Worker startup: Workers are created lazily — first use incurs startup cost
  3. Proxy cleanup: Always call dispose() or rely on FinalizationRegistry
  4. Message overhead: Small frequent messages have higher overhead than batched large messages
  5. SharedWorker caveats: Debugging is harder (separate DevTools), errors may be silent
Repository

lichtblick-suite/lichtblick

Path

.github/skills/web-workers/SKILL.md

Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.