Efficient Web Workers with Bun and CBOR Serialization
When working with web technologies, I prefer a minimalist approach. Without heavy dependencies. That’s why I use Bun to transform my TypeScript code into handy bundles. However, when working with Web Workers, I face two main challenges:
The Challenge with Bun and Workers
- Bun can’t automatically adjust worker instantiation, which is a drawback when working with TypeScript
- Communication between the main thread and worker should be efficient and type-safe. JSON is simple but inefficient and untyped
Solving the Path Problem
To solve the first issue, I use a placeholder when calling new Worker(...)
that gets replaced during the build process
with the actual path. In TypeScript, I simply declare it with declare const WORKER_PATH: string;
making it type-safe.
A small build script creates my main file and worker file, then injects the actual worker file path into the main script. This approach lets me manage as many worker files and path constants as needed. Plus, I can add hashes to filenames to avoid caching issues.
Efficient Communication with CBOR
For the second challenge, I use CBOR as an efficient and type-safe serialization format. This solution is not only more compact than JSON but also faster to parse and supports complex data types.
Here’s a quick example of how the setup works:
Worker Code (worker.ts
)
import { decode, encode } from "cbor-x";
self.onmessage = (event: MessageEvent<Uint8Array>) => {
const msg = decode(new Uint8Array(event.data));
const result = { value: msg.value * 2 };
const encoded = encode(result);
self.postMessage(encoded);
};
Main Script (main.ts
)
import { encode, decode } from "cbor-x";
declare const WORKER_PATH: string;
const worker = new Worker(WORKER_PATH);
worker.onmessage = (event: MessageEvent<Uint8Array>) => {
const result = decode(new Uint8Array(event.data));
console.log("From Worker (CBOR decoded):", result);
};
const msg = { value: 21 };
const encoded = encode(msg);
worker.postMessage(encoded);
Build Script for Bun (build.ts
)
import { readdir, readFile, writeFile } from "node:fs/promises";
// 1. Build Worker and Main script
const build = await Bun.build({
entrypoints: ["./worker.ts", "./main.ts"],
outdir: "./dist",
target: "browser",
naming: { entry: "[name]-[hash].[ext]" }, // e.g., worker-abc123.js
minify: true,
});
// 2. Find Worker file in dist directory
const files = await readdir("./dist");
const workerFile = files.find((f) => f.startsWith("worker-") && f.endsWith(".js"));
const mainFile = files.find((f) => f.startsWith("main-") && f.endsWith(".js"));
if (!workerFile || !mainFile) throw new Error("Build failed: Files not found");
// 3. Inject Worker path into Main.js
const mainPath = `./${mainFile}`;
const mainContent = await readFile(mainPath, "utf-8");
const injected = `const WORKER_PATH = './${workerFile}';\n${mainContent}`;
await writeFile(mainPath, injected);
console.log("Build & Injection successful!");
Why CBOR?
- More efficient than JSON: More compact and faster to parse, especially with large/complex data
- Binary transfer:
postMessage
can work directly withArrayBuffer
/Uint8Array
(zero-copy!) - Type safety: With TypeScript and CBOR, you maintain complex structures and types better than with JSON
Conclusion & Extensions
- Worker paths remain stable even when hashes change - thanks to the build script
- CBOR makes communication between main thread and worker efficient and robust
- This strategy works with any number of workers and complex messages
Extendable for:
- Multiple workers and multiple path constants
- Stronger typing of messages (TypeScript interfaces)
- Integration into larger apps/frameworks
🧠 AI-generated: Simple Setup Guide for Web Workers
This section was automatically generated to help you implement the ideas from the article directly.
To get started quickly with this approach, follow these steps:
- Install the required dependencies:
bun add cbor-x
bun add -d @types/bun
-
Create the three files from the code examples above (
worker.ts
,main.ts
, andbuild.ts
) -
Run the build script:
bun run build.ts
- Include the generated main script in your HTML:
<script src="./main-[hash].js"></script>
The build process will automatically handle path injection and file hashing, while CBOR ensures efficient communication between your main thread and web workers.
Alternative Approaches
Build Tools:
- Vite: Use Vite’s built-in worker support with
new Worker(new URL('./worker.ts', import.meta.url))
- Webpack: Configure worker-loader or Webpack 5’s built-in worker handling
- esbuild: Similar setup to Bun but with different plugin ecosystem
Serialization Alternatives:
- Protocol Buffers: More structured but requires schema definition
- MessagePack: Similar to CBOR but with different encoding approach
- Avro: Schema-based binary format good for complex data structures
- BSON: Binary JSON format used by MongoDB
Worker Patterns:
- Worker pools for managing multiple instances
- Shared workers for cross-tab communication
- Service workers for offline capabilities and caching
Each alternative has different trade-offs in bundle size, performance, and developer experience - choose based on your specific needs!