WebAssembly 모듈의 효율적인 로드

WebAssembly로 작업할 때는 모듈을 다운로드하고 컴파일한 후 인스턴스화한 다음 자바스크립트로 내보내는 모듈을 사용하고자 하는 경우가 많습니다. 이 게시물에서는 최적의 효율성을 위해 권장되는 접근 방식을 설명합니다.

마티아스 비넨스
마티아스 바인스

WebAssembly로 작업할 때는 모듈을 다운로드하고 컴파일한 후 인스턴스화한 다음 자바스크립트로 내보내는 모듈을 사용하려는 경우가 많습니다. 이 게시물에서는 이러한 작업을 정확히 실행하는 일반적이지만 최적화되지 않은 코드 스니펫으로 시작하고, 가능한 여러 최적화를 설명하고, 최종적으로 JavaScript에서 WebAssembly를 실행하는 가장 간단하고 효율적인 방법을 보여줍니다.

다음 코드 스니펫은 최적이 아니지만 전체 download-compile-instantiate 댄스를 실행합니다.

사용하지 마세요.

(async () => {
  const response = await fetch('fibonacci.wasm');
  const buffer = await response.arrayBuffer();
  const module = new WebAssembly.Module(buffer);
  const instance = new WebAssembly.Instance(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

new WebAssembly.Module(buffer)를 사용하여 응답 버퍼를 모듈로 변환하는 방법에 유의하세요. 이는 동기식 API입니다. 즉, 완료될 때까지 기본 스레드를 차단합니다. Chrome은 이를 방지하기 위해 4KB보다 큰 버퍼에는 WebAssembly.Module를 사용 중지합니다. 크기 제한을 해결하려면 대신 await WebAssembly.compile(buffer)를 사용하면 됩니다.

(async () => {
  const response = await fetch('fibonacci.wasm');
  const buffer = await response.arrayBuffer();
  const module = await WebAssembly.compile(buffer);
  const instance = new WebAssembly.Instance(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

await WebAssembly.compile(buffer)여전히 최적의 접근 방법은 아니지만 이 내용은 잠시 후에 살펴보겠습니다.

await의 사용이 명확하기 때문에 수정된 스니펫의 거의 모든 작업이 이제 비동기식입니다. 유일한 예외는 new WebAssembly.Instance(module)로, Chrome에서 버퍼 크기 4KB로 동일하게 제한됩니다. 일관성을 유지하고 기본 스레드를 자유롭게 유지하기 위해 비동기 WebAssembly.instantiate(module)를 사용할 수 있습니다.

(async () => {
  const response = await fetch('fibonacci.wasm');
  const buffer = await response.arrayBuffer();
  const module = await WebAssembly.compile(buffer);
  const instance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

앞서 힌트를 드린 compile 최적화로 돌아가 보겠습니다. 스트리밍 컴파일을 사용하면 모듈 바이트가 계속 다운로드되는 동안 브라우저에서 이미 WebAssembly 모듈 컴파일을 시작할 수 있습니다. 다운로드와 컴파일이 동시에 이루어지므로 특히 페이로드가 큰 경우 속도가 더 빠릅니다.

다운로드 시간이 WebAssembly 모듈의 컴파일 시간보다 길면 마지막 바이트가 다운로드된 직후 WebAssembly.compileStreaming()에 컴파일이 완료됩니다.

이 최적화를 사용하려면 WebAssembly.compile 대신 WebAssembly.compileStreaming를 사용하세요. 이 변경으로 중간 배열 버퍼를 제거할 수도 있습니다. 이제 await fetch(url)에서 반환된 Response 인스턴스를 직접 전달할 수 있기 때문입니다.

(async () => {
  const response = await fetch('fibonacci.wasm');
  const module = await WebAssembly.compileStreaming(response);
  const instance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

WebAssembly.compileStreaming API는 Response 인스턴스로 확인되는 프로미스도 허용합니다. 코드의 다른 곳에서 response가 필요하지 않으면 결과를 명시적으로 await하지 않고 fetch에서 반환된 프로미스를 직접 전달할 수 있습니다.

(async () => {
  const fetchPromise = fetch('fibonacci.wasm');
  const module = await WebAssembly.compileStreaming(fetchPromise);
  const instance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

다른 곳에서도 fetch 결과가 필요하지 않다면 직접 전달할 수도 있습니다.

(async () => {
  const module = await WebAssembly.compileStreaming(
    fetch('fibonacci.wasm'));
  const instance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

개인적으로는 별도의 줄에 두는 것이 더 읽기 쉽습니다.

응답을 모듈로 컴파일한 다음 즉시 인스턴스화하는 방법을 알아보세요. WebAssembly.instantiate는 한 번에 컴파일하고 인스턴스화할 수 있습니다. WebAssembly.instantiateStreaming API는 스트리밍 방식으로 이를 실행합니다.

(async () => {
  const fetchPromise = fetch('fibonacci.wasm');
  const { module, instance } = await WebAssembly.instantiateStreaming(fetchPromise);
  // To create a new instance later:
  const otherInstance = await WebAssembly.instantiate(module);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

단일 인스턴스만 필요한 경우 module 객체를 유지하여 코드를 더 단순화하는 것은 의미가 없습니다.

// This is our recommended way of loading WebAssembly.
(async () => {
  const fetchPromise = fetch('fibonacci.wasm');
  const { instance } = await WebAssembly.instantiateStreaming(fetchPromise);
  const result = instance.exports.fibonacci(42);
  console.log(result);
})();

적용한 최적화를 요약하면 다음과 같습니다.

  • 비동기 API를 사용하여 기본 스레드 차단 방지
  • 스트리밍 API를 사용하여 WebAssembly 모듈을 더 빠르게 컴파일하고 인스턴스화
  • 필요 없는 코드는 작성하지 않음

WebAssembly를 즐겁게 사용해 보세요.