Web Workers
Use Web Workers in NukeJS with the standard new URL(..., import.meta.url) pattern. The framework automatically bundles and serves worker scripts — no configuration needed.
Overview
NukeJS supports the standard new URL(..., import.meta.url) pattern for creating Web Workers. At build time, the framework detects worker references, bundles them separately, and rewrites the URL to point to the built asset. No webpack config, no plugins — it just works.
"use client" component or guarded by a typeof window !== 'undefined' check.Basic usage
Create a worker file with a .worker.ts extension (optional but recommended for clarity), then instantiate it with the standard pattern:
// This file runs in a Web Worker context
self.addEventListener('message', (e) => {
const { numbers } = e.data
// Simulate expensive computation
const sum = numbers.reduce((acc: number, n: number) => {
for (let i = 0; i < 1000000; i++) {
acc += Math.sqrt(n)
}
return acc
}, 0)
self.postMessage({ sum })
})"use client"
import { useState } from 'react'
export default function Calculator() {
const [result, setResult] = useState<number | null>(null)
const [computing, setComputing] = useState(false)
function compute() {
setComputing(true)
// Create worker using the standard URL pattern
const worker = new Worker(
new URL('../workers/heavy-task.worker.ts', import.meta.url),
{ type: 'module' }
)
worker.postMessage({ numbers: [1, 2, 3, 4, 5] })
worker.addEventListener('message', (e) => {
setResult(e.data.sum)
setComputing(false)
worker.terminate()
})
}
return (
<div>
<button onClick={compute} disabled={computing}>
{computing ? 'Computing...' : 'Run Heavy Task'}
</button>
{result !== null && <p>Result: {result}</p>}
</div>
)
}How it works
- Detection — During the build, esbuild scans your code for
new URL(..., import.meta.url)patterns. - Bundling — Each detected worker file is bundled separately as an ESM module with content-based hashing (
worker-[hash].js). - URL rewriting — The
new URL(...)call is rewritten to point to/__worker/worker-[hash].js— a public URL served by the framework. - Module workers — Workers are bundled as ES modules, so you can use
importand modern syntax inside them.
Example: Image processing
Offload CPU-intensive image manipulation to a worker to keep the UI responsive:
self.addEventListener('message', async (e) => {
const { imageData, filter } = e.data
const processed = applyFilter(imageData, filter)
self.postMessage({ processed }, [processed.data.buffer])
})
function applyFilter(imageData: ImageData, filter: string): ImageData {
const data = imageData.data
const processed = new ImageData(
new Uint8ClampedArray(data),
imageData.width,
imageData.height
)
for (let i = 0; i < data.length; i += 4) {
switch (filter) {
case 'grayscale':
const avg = (data[i] + data[i + 1] + data[i + 2]) / 3
processed.data[i] = processed.data[i + 1] = processed.data[i + 2] = avg
break
case 'invert':
processed.data[i] = 255 - data[i]
processed.data[i + 1] = 255 - data[i + 1]
processed.data[i + 2] = 255 - data[i + 2]
break
}
}
return processed
}"use client"
import { useRef, useState } from 'react'
export default function ImageEditor() {
const canvasRef = useRef<HTMLCanvasElement>(null)
const [processing, setProcessing] = useState(false)
async function applyFilter(filter: string) {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')!
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)
setProcessing(true)
const worker = new Worker(
new URL('../workers/image-processor.worker.ts', import.meta.url),
{ type: 'module' }
)
worker.postMessage({ imageData, filter })
worker.addEventListener('message', (e) => {
ctx.putImageData(e.data.processed, 0, 0)
setProcessing(false)
worker.terminate()
})
}
return (
<div>
<canvas ref={canvasRef} width={800} height={600} />
<div>
<button onClick={() => applyFilter('grayscale')} disabled={processing}>
Grayscale
</button>
<button onClick={() => applyFilter('invert')} disabled={processing}>
Invert
</button>
</div>
</div>
)
}Example: Background data processing
Parse and validate large datasets in a worker without blocking the main thread:
self.addEventListener('message', (e) => {
const { csvText } = e.data
const lines = csvText.split('\n')
const headers = lines[0].split(',')
const rows = lines.slice(1).map((line: string) => {
const values = line.split(',')
return headers.reduce((obj: any, header: string, i: number) => {
obj[header.trim()] = values[i]?.trim() ?? ''
return obj
}, {})
})
// Validate rows
const valid = rows.filter((row: any) => row.email && row.name)
const invalid = rows.length - valid.length
self.postMessage({ valid, invalid, total: rows.length })
})"use client"
import { useState } from 'react'
export default function CSVImporter() {
const [result, setResult] = useState<any>(null)
const [loading, setLoading] = useState(false)
async function handleFile(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0]
if (!file) return
setLoading(true)
const csvText = await file.text()
const worker = new Worker(
new URL('../workers/csv-parser.worker.ts', import.meta.url),
{ type: 'module' }
)
worker.postMessage({ csvText })
worker.addEventListener('message', (e) => {
setResult(e.data)
setLoading(false)
worker.terminate()
})
}
return (
<div>
<input type="file" accept=".csv" onChange={handleFile} />
{loading && <p>Processing...</p>}
{result && (
<div>
<p>Total rows: {result.total}</p>
<p>Valid: {result.valid.length}</p>
<p>Invalid: {result.invalid}</p>
</div>
)}
</div>
)
}Using dependencies in workers
Workers are bundled with esbuild, so you can import npm packages just like in regular components. The framework automatically includes dependencies in the worker bundle:
import { marked } from 'marked'
import DOMPurify from 'isomorphic-dompurify'
self.addEventListener('message', (e) => {
const { markdown } = e.data
// Parse markdown to HTML
const rawHtml = marked(markdown)
// Sanitize to prevent XSS
const cleanHtml = DOMPurify.sanitize(rawHtml)
self.postMessage({ html: cleanHtml })
})Worker options
The Worker constructor accepts standard options:
const worker = new Worker(
new URL('./task.worker.ts', import.meta.url),
{
type: 'module', // Always use 'module' for NukeJS workers
name: 'task-worker', // Optional: appears in DevTools
credentials: 'omit', // Optional: fetch credentials mode
}
)Shared Workers
The same pattern works for Shared Workers, which can be accessed from multiple tabs or windows:
const ports: MessagePort[] = []
self.addEventListener('connect', (e: any) => {
const port = e.ports[0]
ports.push(port)
port.addEventListener('message', (e: any) => {
// Broadcast to all connected tabs
ports.forEach(p => {
if (p !== port) p.postMessage(e.data)
})
})
port.start()
})"use client"
import { useEffect, useState } from 'react'
export default function SyncedComponent() {
const [count, setCount] = useState(0)
useEffect(() => {
const worker = new SharedWorker(
new URL('../workers/sync.worker.ts', import.meta.url),
{ type: 'module' }
)
worker.port.addEventListener('message', (e) => {
setCount(e.data.count)
})
worker.port.start()
return () => worker.port.close()
}, [])
function increment() {
setCount(c => c + 1)
// Broadcast to other tabs via the shared worker
const worker = new SharedWorker(
new URL('../workers/sync.worker.ts', import.meta.url),
{ type: 'module' }
)
worker.port.postMessage({ count: count + 1 })
}
return <button onClick={increment}>Count: {count}</button>
}Limitations
- Client-only — Workers run in the browser. Make sure your code is in a
"use client"component. - Static specifiers only — The worker path must be a static string literal. Dynamic paths like
new URL(workerPath, import.meta.url)are not supported. - No DOM access — Workers run in a separate context without access to
window,document, or React. - Serialization overhead — Messages are cloned, not shared. Use Transferable objects (ArrayBuffer, MessagePort) for large data.
.worker.ts or .worker.jsas a suffix makes it clear which files are worker entry points. This is purely a convention — any .ts or .js file works.TypeScript
Enable worker types in your tsconfig.json to get full type checking in worker files:
{
"compilerOptions": {
"lib": ["ES2020", "DOM", "WebWorker"],
// ... other options
}
}// Strong typing for worker messages
interface InputMessage {
type: 'compute'
data: number[]
}
interface OutputMessage {
type: 'result'
sum: number
}
self.addEventListener('message', (e: MessageEvent<InputMessage>) => {
const { data } = e.data
const sum = data.reduce((a, b) => a + b, 0)
const response: OutputMessage = { type: 'result', sum }
self.postMessage(response)
})