// filter-worker.js
self.onmessage = function(event) {
const {
imageData, filters, width, height
} = event.data;
const pixels =
new Uint8ClampedArray(imageData);
for (let i = 0;
i < pixels.length; i += 4) {
let [r, g, b] = [
pixels[i], pixels[i+1],
pixels[i+2]];
for (const filter of filters) {
[r, g, b] =
applyFilterFn(r, g, b, filter);
}
pixels[i] = clamp(r);
pixels[i+1] = clamp(g);
pixels[i+2] = clamp(b);
}
// Transfer buffer back (zero-copy)
self.postMessage(
{ pixels: pixels.buffer },
[pixels.buffer]
);
};
function applyFilterFn(
r, g, b, filter
) {
switch (filter.name) {
case 'brightness':
return [r + filter.value,
g + filter.value,
b + filter.value];
case 'grayscale': {
const avg = (r + g + b) / 3;
return [avg, avg, avg];
}
case 'invert':
return [255-r, 255-g, 255-b];
default: return [r, g, b];
}
}
function clamp(v) {
return Math.min(255,
Math.max(0, Math.round(v)));
}
function useFilterWorker() {
const workerRef =
useRef<Worker | null>(null);
useEffect(() => {
workerRef.current = new Worker(
new URL(
'./filter-worker.js',
import.meta.url),
{ type: 'module' }
);
return () =>
workerRef.current?.terminate();
}, []);
const processFilters = useCallback((
imageData: ImageData,
filters: Filter[]
): Promise<Uint8ClampedArray> => {
return new Promise((resolve) => {
const worker = workerRef.current!;
worker.onmessage = (event) => {
const result =
new Uint8ClampedArray(
event.data.pixels);
resolve(result);
};
// Transfer buffer (zero-copy)
const buffer =
imageData.data.buffer.slice(0);
worker.postMessage(
{
imageData: buffer,
filters,
width: imageData.width,
height: imageData.height,
},
[buffer] // transferable list
);
});
}, []);
return processFilters;
}
function CanvasArea({
image, filters
}: {
image: ImageData | null;
filters: Record<string, number>;
}) {
const canvasRef =
useRef<HTMLCanvasElement>(null);
const processFilters =
useFilterWorker();
useEffect(() => {
if (!image) return;
const filterList =
Object.entries(filters)
.map(([name, value]) =>
({ name, value }));
processFilters(image, filterList)
.then(pixels => {
const ctx = canvasRef.current!
.getContext('2d')!;
const result = new ImageData(
pixels,
image.width,
image.height);
ctx.putImageData(result, 0, 0);
});
}, [image, filters]);
return <canvas ref={canvasRef} />;
}
// Test: move sliders rapidly
// on a 4K image (3840 × 2160)
// BEFORE (main thread):
// Main thread blocked: ~200ms
// UI jank: visible stutter
// FPS during filter: 5fps
// AFTER (Web Worker):
// Main thread blocked: 0ms
// UI jank: none
// FPS during filter: 60fps
// Worker processing: ~200ms
// (same time, different thread)
git switch -c perf/PIXELCRAFT-078-web-workers
git add src/
git commit -m "Move filter processing to Web Worker (PIXELCRAFT-078)"
git push origin perf/PIXELCRAFT-078-web-workers
# PR → Review → Merge → Close ticket ✅
Concurrency is not parallelism.
JavaScript's event loop provides concurrency (juggling tasks on one thread). Web Workers provide parallelism (running on separate CPU cores simultaneously).