diff --git a/packages/frontend-embed/src/components/EmEmoji.vue b/packages/frontend-embed/src/components/EmEmoji.vue
index 9c821e586d..a32a02d129 100644
--- a/packages/frontend-embed/src/components/EmEmoji.vue
+++ b/packages/frontend-embed/src/components/EmEmoji.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed } from 'vue';
-import { colorizeEmoji } from '@/scripts/emojilist.js';
+import { colorizeEmoji } from '@/to-be-shared/emojilist.js';
 
 const props = defineProps<{
 	emoji: string;
diff --git a/packages/frontend-embed/src/components/EmImgWithBlurhash.vue b/packages/frontend-embed/src/components/EmImgWithBlurhash.vue
index 3ea7622f7f..fd1d291073 100644
--- a/packages/frontend-embed/src/components/EmImgWithBlurhash.vue
+++ b/packages/frontend-embed/src/components/EmImgWithBlurhash.vue
@@ -13,8 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts">
 import DrawBlurhash from '@/workers/draw-blurhash?worker';
 import TestWebGL2 from '@/workers/test-webgl2?worker';
-import { WorkerMultiDispatch } from '@/scripts/worker-multi-dispatch.js';
-import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash.js';
+import { WorkerMultiDispatch } from '@/to-be-shared/worker-multi-dispatch.js';
+import { extractAvgColorFromBlurhash } from '@/to-be-shared/extract-avg-color-from-blurhash.js';
 
 const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
 	// テスト環境で Web Worker インスタンスは作成できない
diff --git a/packages/frontend-embed/src/to-be-shared/extract-avg-color-from-blurhash.ts b/packages/frontend-embed/src/to-be-shared/extract-avg-color-from-blurhash.ts
new file mode 100644
index 0000000000..992f6e9a16
--- /dev/null
+++ b/packages/frontend-embed/src/to-be-shared/extract-avg-color-from-blurhash.ts
@@ -0,0 +1,14 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export function extractAvgColorFromBlurhash(hash: string) {
+	return typeof hash === 'string'
+		? '#' + [...hash.slice(2, 6)]
+			.map(x => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'.indexOf(x))
+			.reduce((a, c) => a * 83 + c, 0)
+			.toString(16)
+			.padStart(6, '0')
+		: undefined;
+}
diff --git a/packages/frontend-embed/src/to-be-shared/worker-multi-dispatch.ts b/packages/frontend-embed/src/to-be-shared/worker-multi-dispatch.ts
new file mode 100644
index 0000000000..6b3fcd9383
--- /dev/null
+++ b/packages/frontend-embed/src/to-be-shared/worker-multi-dispatch.ts
@@ -0,0 +1,82 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+function defaultUseWorkerNumber(prev: number, totalWorkers: number) {
+	return prev + 1;
+}
+
+export class WorkerMultiDispatch<POST = any, RETURN = any> {
+	private symbol = Symbol('WorkerMultiDispatch');
+	private workers: Worker[] = [];
+	private terminated = false;
+	private prevWorkerNumber = 0;
+	private getUseWorkerNumber = defaultUseWorkerNumber;
+	private finalizationRegistry: FinalizationRegistry<symbol>;
+
+	constructor(workerConstructor: () => Worker, concurrency: number, getUseWorkerNumber = defaultUseWorkerNumber) {
+		this.getUseWorkerNumber = getUseWorkerNumber;
+		for (let i = 0; i < concurrency; i++) {
+			this.workers.push(workerConstructor());
+		}
+
+		this.finalizationRegistry = new FinalizationRegistry(() => {
+			this.terminate();
+		});
+		this.finalizationRegistry.register(this, this.symbol);
+
+		if (_DEV_) console.log('WorkerMultiDispatch: Created', this);
+	}
+
+	public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: typeof defaultUseWorkerNumber = this.getUseWorkerNumber) {
+		let workerNumber = useWorkerNumber(this.prevWorkerNumber, this.workers.length);
+		workerNumber = Math.abs(Math.round(workerNumber)) % this.workers.length;
+		if (_DEV_) console.log('WorkerMultiDispatch: Posting message to worker', workerNumber, useWorkerNumber);
+		this.prevWorkerNumber = workerNumber;
+
+		// 不毛だがunionをoverloadに突っ込めない
+		// https://stackoverflow.com/questions/66507585/overload-signatures-union-types-and-no-overload-matches-this-call-error
+		// https://github.com/microsoft/TypeScript/issues/14107
+		if (Array.isArray(options)) {
+			this.workers[workerNumber].postMessage(message, options);
+		} else {
+			this.workers[workerNumber].postMessage(message, options);
+		}
+		return workerNumber;
+	}
+
+	public addListener(callback: (this: Worker, ev: MessageEvent<RETURN>) => any, options?: boolean | AddEventListenerOptions) {
+		this.workers.forEach(worker => {
+			worker.addEventListener('message', callback, options);
+		});
+	}
+
+	public removeListener(callback: (this: Worker, ev: MessageEvent<RETURN>) => any, options?: boolean | AddEventListenerOptions) {
+		this.workers.forEach(worker => {
+			worker.removeEventListener('message', callback, options);
+		});
+	}
+
+	public terminate() {
+		this.terminated = true;
+		if (_DEV_) console.log('WorkerMultiDispatch: Terminating', this);
+		this.workers.forEach(worker => {
+			worker.terminate();
+		});
+		this.workers = [];
+		this.finalizationRegistry.unregister(this);
+	}
+
+	public isTerminated() {
+		return this.terminated;
+	}
+
+	public getWorkers() {
+		return this.workers;
+	}
+
+	public getSymbol() {
+		return this.symbol;
+	}
+}