️(frontend) lazy-load @libreaudio/la-call via dynamic import

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 <link rel="modulepreload">) so it's warm
  before the user enables suppression.
This commit is contained in:
lebaudantoine
2026-06-13 12:38:35 +02:00
committed by aleb_the_flash
parent 13036f6ab7
commit ac85a20271
@@ -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<CreateWasmProcessor> | undefined
function loadWasmProcessor(): Promise<CreateWasmProcessor> {
if (!wasmProcessorPromise) {
wasmProcessorPromise = import('@libreaudio/la-call').then(
(mod) => mod.createWasmProcessor
)
}
return wasmProcessorPromise
}
export interface AudioProcessorInterface extends TrackProcessor<Track.Kind.Audio> {
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,
})