앱의 자바스크립트에서 핫 경로를 WebAssembly로 대체

매우 빠릅니다.

이전 도움말에서는 WebAssembly를 사용하여 C/C++ 라이브러리 생태계를 웹에 가져오는 방법을 알아봤습니다. C/C++ 라이브러리를 광범위하게 사용하는 앱 중 하나로 squoosh가 있습니다. 이 웹 앱을 사용하면 C++에서 WebAssembly로 컴파일된 다양한 코덱으로 이미지를 압축할 수 있습니다.

WebAssembly는 .wasm 파일에 저장된 바이트 코드를 실행하는 하위 수준 가상 머신입니다. 이 바이트 코드는 자바스크립트보다 훨씬 빠르게 호스트 시스템에 맞게 컴파일되고 최적화될 수 있는 방식으로 강력하게 입력되고 구조화됩니다. WebAssembly는 처음부터 샌드박스와 삽입을 염두에 둔 코드를 실행할 수 있는 환경을 제공합니다.

제 경험에 비추어 볼 때 대부분의 웹 성능 문제는 강제 레이아웃과 과도한 페인트로 인해 발생하지만, 앱에서 계산 비용이 많이 드는 작업을 실행하느라 많은 시간이 걸리기도 합니다. 여기에서 WebAssembly가 도움이 될 수 있습니다.

더 핫 경로

squoosh에서는 이미지 버퍼를 90도의 배수로 회전하는 JavaScript 함수를 작성했습니다. 이때 OffscreenCanvas가 이상적이지만 타겟팅 중인 브라우저에서는 지원되지 않으며 Chrome에서 버그가 발생할 수 있습니다.

이 함수는 입력 이미지의 모든 픽셀을 반복하고 출력 이미지의 다른 위치에 복사하여 회전합니다. 4,094x4,096픽셀 (16메가픽셀) 이미지의 경우 '핫 경로'라고 하는 내부 코드 블록을 1,600만 번 이상 반복해야 합니다. 다소 많은 수의 반복에도 불구하고, 테스트한 브라우저 3개 중 2개가 2초 이내에 작업을 완료합니다. 이 유형의 상호작용에 허용되는 지속 시간입니다.

for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    const in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

그러나 브라우저 1개는 8초 이상 걸립니다. 브라우저가 JavaScript를 최적화하는 방법은 매우 복잡하며, 다양한 엔진에서 최적화 작업을 수행합니다. 원시 실행에 최적화되는 앱도 있고 DOM과의 상호작용에 최적화된 메서드도 있습니다. 이 경우에는 한 브라우저에서 최적화되지 않은 경로가 발생했습니다.

반면 WebAssembly는 원시 실행 속도를 전적으로 중심으로 구축되었습니다. 따라서 이와 같은 코드의 빠르고 예측 가능한 성능을 원하는 경우 WebAssembly가 도움이 될 수 있습니다.

예측 가능한 성능을 위한 WebAssembly

일반적으로 JavaScript와 WebAssembly는 동일한 최고 성능을 달성할 수 있습니다. 그러나 자바스크립트의 경우 '빠른 경로'에서만 이 성능에 도달할 수 있으며, '빠른 경로'를 유지하기가 어려운 경우가 많습니다. WebAssembly가 제공하는 한 가지 주요 이점은 여러 브라우저에서도 예측 가능한 성능입니다. 엄격한 입력 및 하위 수준 아키텍처 덕분에 컴파일러는 강력한 보장을 할 수 있으므로 WebAssembly 코드를 한 번만 최적화하면 되고 항상 '빠른 경로'를 사용할 수 있습니다.

WebAssembly 작성

이전에는 웹에서 기능을 사용하기 위해 C/C++ 라이브러리를 WebAssembly에 컴파일했습니다. 라이브러리의 코드는 실제로 다루지 않았습니다. 단지 소량의 C/C++ 코드를 작성하여 브라우저와 라이브러리 간의 브리지를 형성했습니다. 이번에는 동기가 다릅니다. WebAssembly의 장점을 활용할 수 있도록 WebAssembly를 염두에 두고 처음부터 작성하려고 합니다.

WebAssembly 아키텍처

WebAssembly 관련 문서를 작성할 때는 WebAssembly가 실제로 무엇인지 좀 더 알아두면 도움이 됩니다.

WebAssembly.org의 인용문:

C 또는 Rust 코드를 WebAssembly에 컴파일하면 모듈 선언이 포함된 .wasm 파일을 얻게 됩니다. 이 선언은 모듈이 환경에서 예상하는 '가져오기' 목록, 이 모듈이 호스트에 제공하는 내보내기 목록 (함수, 상수, 메모리 청크) 및 물론 그 안에 포함된 함수의 실제 바이너리 명령으로 구성됩니다.

이 문제를 들여다 보기 전까지는 발견하지 못했습니다. WebAssembly를 '스택 기반 가상 머신'으로 만드는 스택은 WebAssembly 모듈에서 사용하는 메모리 청크에 저장되지 않는다는 것입니다. 스택은 완전히 VM 내부에 있으며 웹 개발자가 액세스할 수 없습니다 (DevTools를 통한 경우 제외). 따라서 추가 메모리가 전혀 필요하지 않고 VM 내부 스택만 사용하는 WebAssembly 모듈을 작성할 수 있습니다.

여기서는 추가 메모리를 사용하여 이미지의 픽셀에 임의로 액세스하도록 허용하고 이미지의 회전된 버전을 생성해야 합니다. WebAssembly.Memory의 용도입니다.

메모리 관리

일반적으로 추가 메모리를 사용하면 어떻게든 메모리를 관리해야 합니다. 메모리의 어떤 부분이 사용 중입니까? 어떤 것이 무료인가요? 예를 들어 C에는 연속 n바이트의 메모리 공간을 찾는 malloc(n) 함수가 있습니다. 이러한 종류의 함수를 '할당자'라고도 합니다. 물론 사용 중인 할당자 구현은 WebAssembly 모듈에 포함되어야 하며 파일 크기가 늘어납니다. 이러한 메모리 관리 함수의 크기와 성능은 사용된 알고리즘에 따라 상당히 다를 수 있습니다. 따라서 많은 언어에서 'dmalloc', 'emmalloc', 'wee_alloc' 등 여러 구현 중에서 선택할 수 있습니다.

이 경우에는 WebAssembly 모듈을 실행하기 전에 입력 이미지의 크기 (및 출력 이미지의 크기)를 알고 있습니다. 여기서 기회를 발견했습니다. 기존에는 입력 이미지의 RGBA 버퍼를 WebAssembly 함수에 매개변수로 전달하고 회전된 이미지를 반환 값으로 반환했습니다. 이 반환 값을 생성하려면 할당자를 사용해야 합니다. 하지만 필요한 총 메모리 양을 알고 있으므로 (입력 이미지의 두 배, 입력에 한 번, 출력에 한 번) JavaScript를 사용하여 입력 이미지를 WebAssembly 메모리에 저장하고, WebAssembly 모듈을 실행하여 두 번째 회전된 이미지를 생성한 다음 JavaScript를 사용하여 결과를 다시 읽어올 수 있습니다. 메모리 관리를 전혀 사용하지 않고 떠날 수 있습니다.

선택의 기로에 있음

WebAssembly-fy를 사용하려는 원래 자바스크립트 함수를 살펴보면 JavaScript 전용 API가 없는 순수한 계산 코드임을 알 수 있습니다. 따라서 이 코드를 모든 언어로 포팅하는 것이 매우 간단해야 합니다. WebAssembly로 컴파일되는 3가지 언어(C/C++, Rust, AssemblyScript)를 평가했습니다. 각 언어에서 답해야 하는 유일한 질문은 메모리 관리 함수를 사용하지 않고 원시 메모리에 어떻게 액세스할 수 있느냐입니다.

C 및 Emscripten

Emscripten은 WebAssembly 타겟의 C 컴파일러입니다. Emscripten의 목표는 GCC 또는 clang과 같은 잘 알려진 C 컴파일러의 드롭인 대체로 기능하는 것이고 대부분의 플래그와 호환됩니다. 이는 기존 C 및 C++ 코드를 WebAssembly에 최대한 쉽게 컴파일할 수 있도록 하기 위해 Emscripten 사명의 핵심 부분입니다.

원시 메모리 액세스는 C의 본질이며 포인터는 바로 그러한 이유 때문에 존재합니다.

uint8_t* ptr = (uint8_t*)0x124;
ptr[0] = 0xFF;

여기서는 숫자 0x124를 부호 없는 8비트 정수 (또는 바이트)를 가리키는 포인터로 변환합니다. 이렇게 하면 ptr 변수를 메모리 주소 0x124에서 시작하는 배열로 효과적으로 전환하여 다른 배열과 마찬가지로 사용할 수 있는 배열로 전환하여 읽기 및 쓰기를 위한 개별 바이트에 액세스할 수 있습니다. 여기서는 회전을 위해 순서를 변경하려고 하는 이미지의 RGBA 버퍼를 보고 있습니다. 픽셀을 이동하려면 실제로 한 번에 연속 4바이트(R, G, B, A 각 채널당 1바이트)를 이동해야 합니다. 이를 더 쉽게 하기 위해 부호 없는 32비트 정수 배열을 만들 수 있습니다. 규칙에 따라 입력 이미지는 주소 4에서 시작하고 출력 이미지는 입력 이미지가 끝난 직후에 시작합니다.

int bpp = 4;
int imageSize = inputWidth * inputHeight * bpp;
uint32_t* inBuffer = (uint32_t*) 4;
uint32_t* outBuffer = (uint32_t*) (inBuffer + imageSize);

for (int d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (int d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    int in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

전체 자바스크립트 함수를 C로 포팅한 후 emcc를 사용하여 C 파일을 컴파일할 수 있습니다.

$ emcc -O3 -s ALLOW_MEMORY_GROWTH=1 -o c.js rotate.c

항상 그렇듯이 emscripten은 c.js라는 글루 코드 파일과 c.wasm라는 Wasm 모듈을 생성합니다. Wasm 모듈은 약 260바이트로 gzip으로 압축되지만, 글루 코드는 gzip 후 약 3.5KB입니다. 약간의 수정 후에 글루 코드를 없애고 vanilla API를 사용하여 WebAssembly 모듈을 인스턴스화할 수 있었습니다. C 표준 라이브러리의 어떤 것도 사용하지 않는 한 Emscripten에서 이 작업이 가능한 경우가 많습니다.

Rust

Rust는 풍부한 유형 시스템을 사용하며 런타임이 없고 메모리 안전과 스레드 안전을 보장하는 소유권 모델을 사용하는 새로운 최신 프로그래밍 언어입니다. Rust는 또한 WebAssembly를 핵심 기능으로 지원하며 Rust팀은 WebAssembly 생태계에 훌륭한 도구를 다양하게 기여했습니다.

이러한 도구 중 하나는 rustworthm 실무 그룹wasm-pack입니다. wasm-pack는 코드를 가져와 webpack과 같은 번들러와 함께 즉시 사용할 수 있는 웹 친화적인 모듈로 변환합니다. wasm-pack는 매우 편리한 환경이지만 현재는 Rust에서만 작동합니다. 이 그룹은 다른 WebAssembly 타겟팅 언어에 대한 지원을 고려하고 있습니다.

Rust에서 슬라이스는 C에 있는 배열입니다. 그리고 C에서와 마찬가지로 시작 주소를 사용하는 슬라이스를 만들어야 합니다. 이는 Rust에서 시행하는 메모리 안전 모델에 어긋나므로 unsafe 키워드를 사용하여 이 모델을 준수하지 않는 코드를 작성할 수 있도록 해야 합니다.

let imageSize = (inputWidth * inputHeight) as usize;
let inBuffer: &mut [u32];
let outBuffer: &mut [u32];
unsafe {
    inBuffer = slice::from_raw_parts_mut::<u32>(4 as *mut u32, imageSize);
    outBuffer = slice::from_raw_parts_mut::<u32>((imageSize * 4 + 4) as *mut u32, imageSize);
}

for d2 in 0..d2Limit {
    for d1 in 0..d1Limit {
    let in_idx = (d1Start + d1 * d1Advance) * d1Multiplier + (d2Start + d2 * d2Advance) * d2Multiplier;
    outBuffer[i as usize] = inBuffer[in_idx as usize];
    i += 1;
    }
}

다음을 사용하여 Rust 파일 컴파일

$ wasm-pack build

gzip 이후 약 100바이트의 글루 코드가 포함된 7.6KB wasm 모듈을 생성합니다.

AssemblyScript

AssemblyScript는 TypeScript-to-WebAssembly 컴파일러가 되는 것을 목표로 하는 상당히 새로운 프로젝트입니다. 하지만 TypeScript만 사용하지는 않는다는 점에 유의해야 합니다. AssemblyScript는 TypeScript와 동일한 구문을 사용하지만 자체 표준 라이브러리를 전환합니다. 표준 라이브러리는 WebAssembly의 기능을 모델링합니다. 즉, WebAssembly 주위에 있는 TypeScript를 컴파일할 수는 없지만, WebAssembly를 작성하기 위해 새 프로그래밍 언어를 배울 필요가 없다는 의미입니다.

    for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
      for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
        let in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
        store<u32>(offset + i * 4 + 4, load<u32>(in_idx * 4 + 4));
        i += 1;
      }
    }

rotate() 함수의 작은 유형 노출 영역을 고려할 때 이 코드를 AssemblyScript로 포팅하는 것은 상당히 쉬웠습니다. load<T>(ptr: usize)store<T>(ptr: usize, value: T) 함수는 원시 메모리에 액세스하기 위해 AssemblyScript에서 제공됩니다. AssemblyScript 파일을 컴파일하려면 AssemblyScript/assemblyscript npm 패키지를 설치하고

$ asc rotate.ts -b assemblyscript.wasm --validate -O3

AssemblyScript는 약 300바이트의 Wasm 모듈을 제공하며 글루 코드는 없습니다. 모듈은 일반적인 WebAssembly API로만 작동합니다.

WebAssembly 포렌식

Rust의 7.6KB는 다른 두 언어에 비해 놀라울 정도로 큽니다. WebAssembly 생태계에는 (생성한 언어에 관계없이) WebAssembly 파일을 분석하고 진행 상황을 알려주고 상황을 개선하는 데 도움이 되는 몇 가지 도구가 있습니다.

나뭇가지

Twiggy는 Rust WebAssembly팀의 또 다른 도구로, WebAssembly 모듈에서 유용한 데이터를 다양하게 추출합니다. 이 도구는 Rust 전용이 아니며, 모듈의 호출 그래프와 같은 항목을 검사하고 사용되지 않거나 불필요한 섹션을 확인하고 모듈의 총 파일 크기에 기여하는 섹션을 파악할 수 있도록 지원합니다. 후자는 Twiggy의 top 명령어를 사용하여 실행할 수 있습니다.

$ twiggy top rotate_bg.wasm
Twiggy 설치 스크린샷

여기서는 대부분의 파일 크기가 할당자에서 비롯된 것을 알 수 있습니다. 우리의 코드는 동적 할당을 사용하지 않기 때문에 이는 놀랄 일이었습니다. 또 다른 중요한 기여 요소는 '함수 이름' 하위 섹션입니다.

비밀 스트립

wasm-stripWebAssembly 바이너리 툴킷(줄여서 wabt)의 도구입니다. 여기에는 WebAssembly 모듈을 검사하고 조작할 수 있는 몇 가지 도구가 포함되어 있습니다. wasm2wat는 바이너리 wasm 모듈을 사람이 읽을 수 있는 형식으로 변환하는 디스어셈블러입니다. Wabt에는 사람이 읽을 수 있는 형식을 다시 바이너리 wasm 모듈로 변환할 수 있는 wat2wasm도 포함되어 있습니다. 이 두 가지 보완 도구를 사용하여 WebAssembly 파일을 검사했지만 wasm-strip가 가장 유용하다는 것을 확인했습니다. wasm-strip는 WebAssembly 모듈에서 불필요한 섹션과 메타데이터를 삭제합니다.

$ wasm-strip rotate_bg.wasm

이렇게 하면 rust 모듈의 파일 크기가 7.5KB에서 6.6KB (gzip 후)로 줄어듭니다.

wasm-opt

wasm-optBinaryen의 도구입니다. 이 모듈은 WebAssembly 모듈을 가져와 바이트 코드만을 기반으로 크기와 성능 모두를 위해 최적화하려고 합니다. Emscripten과 같은 일부 도구는 이미 이 도구를 실행하고 있지만, 그렇지 않은 도구도 있습니다 일반적으로 이러한 도구를 사용하여 바이트를 추가로 절약할 수 있습니다.

wasm-opt -O3 -o rotate_bg_opt.wasm rotate_bg.wasm

wasm-opt를 사용하면 몇 바이트를 줄여 gzip 후 총 6.2KB의 크기를 확보할 수 있습니다.

#![없음_std]

약간의 상담과 연구 끝에 Google은 Rust의 표준 라이브러리를 사용하지 않고 #![no_std] 기능을 사용하여 Rust 코드를 다시 작성했습니다. 이렇게 하면 동적 메모리 할당도 모두 사용 중지되고 모듈에서 할당자 코드가 삭제됩니다. 이 Rust 파일

$ rustc --target=wasm32-unknown-unknown -C opt-level=3 -o rust.wasm rotate.rs

wasm-opt, wasm-strip 및 gzip 이후에 1.6KB wasm 모듈을 생성했습니다. 이 모듈은 C 및 AssemblyScript에서 생성된 모듈보다 여전히 크지만 가볍다고 간주할 만큼 작습니다.

성능

파일 크기만을 기준으로 결론을 내리기 전에, 파일 크기가 아닌 성능을 최적화하기 위해 이 여정을 진행했습니다. 그렇다면 실적을 어떻게 측정했고 그 결과는 어땠을까요?

벤치마킹 방법

WebAssembly는 하위 수준의 바이트 코드 형식이지만 호스트별 기계어 코드를 생성하려면 컴파일러를 통해 전송해야 합니다. JavaScript와 마찬가지로 컴파일러도 여러 단계로 작동합니다. 간단히 말해서: 첫 번째 단계는 컴파일 속도가 훨씬 빠르지만 일반적으로 코드 생성 속도가 느립니다. 모듈 실행이 시작되면 브라우저는 자주 사용되는 부분을 관찰하고 더 최적화되지만 더 느린 컴파일러를 통해 전송합니다.

이 사용 사례는 이미지 회전 코드가 한 번, 어쩌면 두 번 사용된다는 점에서 흥미롭습니다. 따라서 대부분의 경우 컴파일러 최적화의 이점을 누릴 수 없습니다. 이는 벤치마킹할 때 유의해야 합니다. WebAssembly 모듈을 10,000번 루프로 실행하면 비현실적인 결과를 얻게 됩니다. 현실적인 수치를 얻으려면 모듈을 한 번 실행한 다음 단일 실행의 숫자를 기반으로 결정을 내려야 합니다.

실적 비교

언어별 속도 비교
브라우저별 속도 비교

이 두 그래프는 동일한 데이터에 대해 서로 다른 관점을 나타냅니다. 첫 번째 그래프에서는 브라우저별로 비교하고, 두 번째 그래프에서는 사용된 언어별로 비교합니다. 저는 대수적 시간 척도를 선택했습니다. 또한 동일한 머신에서 실행할 수 없는 브라우저 1개를 제외하고 모든 벤치마크가 동일한 16메가픽셀 테스트 이미지와 동일한 호스트 머신을 사용하는 것도 중요합니다.

이 그래프를 너무 많이 분석하지 않아도 기존의 성능 문제를 해결했다는 것을 알 수 있습니다. 모든 WebAssembly 모듈은 최대 500ms 이내에 실행됩니다. 이를 통해 WebAssembly가 예측 가능한 성능을 제공했으며, 이 부분을 처음에 설명한 것이 바로 확인되었습니다. 어떤 언어를 선택하든 브라우저와 언어 간의 차이는 최소화됩니다. 정확히 말해서 모든 브라우저에서 자바스크립트의 표준 편차는 약 400ms인 반면, 모든 브라우저에서 모든 WebAssembly 모듈의 표준 편차는 약 80ms입니다.

난이도

또 다른 측정항목은 WebAssembly 모듈을 만들어 스쿼오시에 통합하기 위해 들인 노력의 정도입니다. 노력에 숫자 값을 할당하기가 어렵기 때문에 그래프를 만들지 않겠지만 몇 가지 주의할 사항이 있습니다.

AssemblyScript는 원활했습니다. TypeScript를 사용하여 WebAssembly를 작성할 수 있게 할 뿐만 아니라 제 동료들이 코드를 매우 쉽게 검토할 수 있게 할 뿐만 아니라, 성능은 양호하면서도 매우 작고 글루가 없는 WebAssembly 모듈을 생성할 수 있습니다. prettier 및 tslint와 같은 TypeScript 생태계의 도구는 대부분 작동할 것입니다.

Rust와 wasm-pack를 함께 사용하는 것도 매우 편리하긴 하지만, 더 큰 WebAssembly 프로젝트에서는 특히 바인딩이 필요하고 메모리 관리가 필요했습니다. 경쟁력 있는 파일 크기를 확보하려면 행복한 경로에서 약간 벗어나야 했습니다.

C와 Emscripten은 매우 작고 성능이 우수한 WebAssembly 모듈을 즉시 만들었지만, 글루 코드에 의존하여 필수적인 수준으로 줄일 용기가 없으면 전체 크기 (WebAssembly 모듈 + 글루 코드)가 상당히 커졌습니다.

결론

따라서 JS 핫 경로가 있고 WebAssembly와 더 빠르고 일관성 있게 만들고 싶다면 어떤 언어를 사용해야 할까요? 항상 성능에 관한 질문에서 그렇듯이 답변은 상황에 따라 다릅니다. 그래서 우리가 무엇을 배송했는가?

비교 그래프

사용한 다양한 언어의 모듈 크기 / 성능 절충점을 고려했을 때 C 또는 AssemblyScript 중 하나가 가장 좋은 선택입니다. Google은 Rust를 출시하기로 결정했습니다. 이러한 결정을 내리는 데는 여러 이유가 있습니다. 지금까지 Squoosh에서 제공된 모든 코덱은 Emscripten을 사용하여 컴파일됩니다. WebAssembly 생태계에 관한 지식을 넓히고 프로덕션에서 다른 언어를 사용하고 싶었습니다. AssemblyScript는 강력한 대안이지만, 프로젝트가 비교적 작고 컴파일러가 Rust 컴파일러만큼 완성되지 않았습니다.

Rust와 다른 언어의 파일 크기 차이는 분산형 그래프에서 크게 달라지지 않지만, 실제로는 그리 큰 문제가 아닙니다. 2G에서도 500B 또는 1.6KB를 로드하는 데는 1/10초도 채 걸리지 않습니다. Rust는 모듈 크기 측면에서 이 격차를 조만간 줄일 수 있을 것으로 기대합니다.

런타임 성능 측면에서 Rust는 브라우저 전반에서 AssemblyScript보다 평균이 더 빠릅니다. 특히 대규모 프로젝트에서는 Rust가 수동으로 코드를 최적화할 필요 없이 더 빠르게 코드를 생성할 가능성이 높습니다. 그렇다고 해서 자신에게 가장 익숙한 것을 사용하는 데 지장이 없어야 합니다.

그렇기 때문에 AssemblyScript는 훌륭한 발견이었습니다. 이 API를 사용하면 웹 개발자가 새로운 언어를 학습할 필요 없이 WebAssembly 모듈을 생성할 수 있습니다. AssemblyScript팀은 적극적으로 대응하여 도구 모음을 개선하기 위해 적극적으로 노력하고 있습니다. 앞으로 AssemblyScript를 계속 주시할 것입니다.

업데이트: Rust

이 기사를 게시한 후 Rust팀의 닉 피츠제럴드가 훌륭한 Rust Wasm 관련 책을 소개했습니다. 여기에는 파일 크기 최적화 섹션이 포함되어 있습니다. 이 도움말 (특히 링크 시간 최적화 및 수동 패닉 처리 사용)에 따라 '일반' Rust 코드를 작성하고 파일 크기를 늘리지 않고도 Cargo (Rust의 npm)을 다시 사용할 수 있었습니다. Rust 모듈은 gzip 후 3,70B로 끝납니다. 자세한 내용은 내가 Squoosh에서 연 PR을 참조하세요.

이 여정에 도움을 주신 애슐리 윌리엄스, 스티브 클라브닉, 닉 피츠제럴드, 맥스 그레이에게 진심으로 감사드립니다.