From ac85a202712a85244022a2701657e59ed5acba61 Mon Sep 17 00:00:00 2001 From: lebaudantoine Date: Sat, 13 Jun 2026 12:38:35 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F(frontend)=20lazy-load=20@lib?= =?UTF-8?q?reaudio/la-call=20via=20dynamic=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The noise-suppression processor is now imported on demand with a dynamic import() instead of a top-level static import, so it lands in its own code-split chunk rather than the main bundle. Why this is necessary: * @libreaudio/la-call is fully self-contained. It inlines *everything* as JavaScript: two WASM binaries (SIMD and non-SIMD variants), the Emscripten glue, the worklet processor source, and the noise model plus its weights — the model is compiled into the .wasm, so we ship effectively two copies of it. * The WASM is inlined as numeric array literals (new Uint8Array([...])), the least compact representation possible (~2-4 source chars/byte) and not meaningfully minifiable. The result is a large module that also can't be stream-compiled the way an external .wasm asset would be. * A static import would pull all of that into the initial bundle, inflating critical-path download size and lengthening build time (parsing/minifying the big literal) — for a feature that's only used when the user actually turns on noise suppression. * Dynamic import() isolates the whole payload in a separate, content-hashed, browser-cached chunk. The cost (chunk fetch + WASM instantiation) becomes a one-time hit deferred to first activation, and is fully off the page's initial load path. Notes: * The library self-bundles its AudioWorklet at runtime from a Blob URL and inlines the WASM, so it needs no build-tool asset plumbing (no ?worker&url / ?url, no Vite asset config). The dynamic import is therefore the only splitting mechanism required. * Audio processing still runs off the main thread on the AudioWorklet path (desktop/most browsers); only the Android ScriptProcessor fallback runs on the main thread. Import style does not affect this. * To hide the first-activation latency, the chunk can be prefetched (e.g. import() on idle or ) so it's warm before the user enables suppression. --- .../livekit/processors/RnnNoiseProcessor.ts | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/features/rooms/livekit/processors/RnnNoiseProcessor.ts b/src/frontend/src/features/rooms/livekit/processors/RnnNoiseProcessor.ts index 195ab439..56c6f819 100644 --- a/src/frontend/src/features/rooms/livekit/processors/RnnNoiseProcessor.ts +++ b/src/frontend/src/features/rooms/livekit/processors/RnnNoiseProcessor.ts @@ -1,11 +1,30 @@ - import type { Track, TrackProcessor, ProcessorOptions } from 'livekit-client' -import { createWasmProcessor } from '@libreaudio/la-call' // Use Jitsi's approach: maintain a global AudioContext variable // and suspend/resume it as needed to manage audio state let audioContext: AudioContext +/** + * Lazily load the WASM processor factory. + * + * '@libreaudio/la-call' pulls in a WebAssembly payload, so we defer importing + * it until a processor is actually initialized rather than at module load. + * The import promise is cached so repeated init() calls reuse the same module. + */ +type CreateWasmProcessor = + (typeof import('@libreaudio/la-call'))['createWasmProcessor'] + +let wasmProcessorPromise: Promise | undefined + +function loadWasmProcessor(): Promise { + if (!wasmProcessorPromise) { + wasmProcessorPromise = import('@libreaudio/la-call').then( + (mod) => mod.createWasmProcessor + ) + } + return wasmProcessorPromise +} + export interface AudioProcessorInterface extends TrackProcessor { name: string } @@ -36,6 +55,7 @@ export class RnnNoiseProcessor implements AudioProcessorInterface { new MediaStream([this.source]) ) + const createWasmProcessor = await loadWasmProcessor() this.noiseSuppressionNode = await createWasmProcessor(audioContext, { intensity: 90, })