Skip to content

Commit ad4a048

Browse files
committed
perf(monochrome-mask): offload mask generation to worker
1 parent c83d940 commit ad4a048

4 files changed

Lines changed: 580 additions & 6 deletions

File tree

src/hooks/useMonochromeMask.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { useState, useEffect, useMemo } from 'react';
2-
import { createMonochromeMask, type MonochromeMaskOptions, type PipelineHook } from '@/lib/canvas';
2+
import {
3+
createMonochromeMask,
4+
createMonochromeMaskViaWorker,
5+
type MonochromeMaskOptions,
6+
type PipelineHook,
7+
} from '@/lib/canvas';
38

49
export type { MonochromeMaskOptions, PipelineHook };
510

@@ -87,11 +92,13 @@ export function useMonochromeMask(iconUrl: string | undefined, options: Monochro
8792
}
8893

8994
idleHandle = requestIdleWork(() => {
90-
createMonochromeMask(iconUrl, opts).then((url) => {
91-
if (!cancelled) {
92-
setMaskUrl(url);
93-
}
94-
});
95+
createMonochromeMaskViaWorker(iconUrl, opts)
96+
.catch(() => createMonochromeMask(iconUrl, opts))
97+
.then((url) => {
98+
if (!cancelled) {
99+
setMaskUrl(url);
100+
}
101+
});
95102
}, { timeout: 300 });
96103

97104
return () => {

src/lib/canvas/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,5 @@ export {
1212
type MonochromeMaskOptions,
1313
type PipelineHook,
1414
} from './monochrome-mask'
15+
16+
export { createMonochromeMaskViaWorker } from './monochrome-mask-worker-client'
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import type { MonochromeMaskOptions } from './monochrome-mask'
2+
3+
interface CreateMaskRequest {
4+
type: 'create-mask'
5+
id: number
6+
iconUrl: string
7+
options: MonochromeMaskOptions
8+
}
9+
10+
interface CreateMaskResponse {
11+
type: 'create-mask-result'
12+
id: number
13+
dataUrl: string | null
14+
error?: string
15+
}
16+
17+
type PendingRequest = {
18+
resolve: (dataUrl: string | null) => void
19+
reject: (error: Error) => void
20+
}
21+
22+
type WorkerLike = {
23+
postMessage: (message: unknown) => void
24+
terminate: () => void
25+
onmessage: ((event: MessageEvent<CreateMaskResponse>) => void) | null
26+
onerror: ((event: ErrorEvent) => void) | null
27+
}
28+
29+
let worker: WorkerLike | null = null
30+
let workerInitError: string | null = null
31+
let nextRequestId = 1
32+
const pending = new Map<number, PendingRequest>()
33+
34+
function ensureWorker(): WorkerLike | null {
35+
if (worker) {
36+
return worker
37+
}
38+
if (workerInitError) {
39+
return null
40+
}
41+
42+
try {
43+
worker = new Worker(new URL('./monochrome-mask.worker.ts', import.meta.url), { type: 'module' })
44+
45+
worker.onmessage = (event) => {
46+
const data = event.data
47+
if (!data || data.type !== 'create-mask-result') {
48+
return
49+
}
50+
51+
const callback = pending.get(data.id)
52+
if (!callback) {
53+
return
54+
}
55+
56+
pending.delete(data.id)
57+
if (data.error) {
58+
callback.reject(new Error(data.error))
59+
} else {
60+
callback.resolve(data.dataUrl)
61+
}
62+
}
63+
64+
worker.onerror = () => {
65+
workerInitError = 'worker error'
66+
worker?.terminate()
67+
worker = null
68+
for (const [id, req] of pending) {
69+
pending.delete(id)
70+
req.reject(new Error('worker error'))
71+
}
72+
}
73+
74+
return worker
75+
} catch (error) {
76+
workerInitError = error instanceof Error ? error.message : 'worker init failed'
77+
return null
78+
}
79+
}
80+
81+
export async function createMonochromeMaskViaWorker(
82+
iconUrl: string,
83+
options: MonochromeMaskOptions,
84+
): Promise<string | null> {
85+
const workerInstance = ensureWorker()
86+
if (!workerInstance) {
87+
throw new Error(workerInitError ?? 'worker is unavailable')
88+
}
89+
90+
const id = nextRequestId++
91+
const request: CreateMaskRequest = {
92+
type: 'create-mask',
93+
id,
94+
iconUrl,
95+
options,
96+
}
97+
98+
return await new Promise<string | null>((resolve, reject) => {
99+
pending.set(id, { resolve, reject })
100+
workerInstance.postMessage(request)
101+
})
102+
}
103+

0 commit comments

Comments
 (0)