2024년, 2025년, 2026년 무렵 출시된 WebAssembly 런타임에서 libsodium 벤치마크를 비교해, 런타임이 실제로 얼마나 빨라지고 있는지와 CPU 기능 지원이 성능에 미치는 영향을 살펴본다.
WebAssembly 런타임이 더 빨라지고 있는지 알고 싶었다.
이 글은 앞서 진행한 libsodium WebAssembly 벤치마크 2019, 2021, 2023의 후속편이다.
“최신 버전이 어떤 마이크로벤치마크 하나에서 네이티브 코드를 이기느냐?”도 아니고, “어느 런타임의 벤치마크 차트가 가장 예쁘냐?”도 아니다. 더 지루하지만 더 유용한 질문은 이것이다.
같은 C 암호화 코드를 가져와 WebAssembly로 컴파일한 뒤, 최신 런타임, 1년 전 런타임, 2년 전 런타임에서 실행했을 때 실제로 상황이 나아지고 있는가?
그래서 2024년 6월, 2025년 6월, 2026년 6월 무렵에 출시된 WebAssembly 런타임에서 libsodium을 벤치마크했다.
짧게 요약하면:
WAVM과 WasmEdge는 매우 빠를 수 있다. WasmEdge 0.17.0은 명시적으로 --run-mode=aot가 필요했다. 이것이 없으면 컴파일된 모듈이 인터프리터 모드 Wasm처럼 실행됐다.WAMR도 매우 빠르며, WAVM과 최고의 Wasmtime 결과 바로 옆에 자리한다.wasm2c, Wasmer, Wasmtime은 모두 CPU 바운드 암호화 작업에 흥미로울 만큼 네이티브에 가깝다.Wazero는 더 느리지만 안정적이다.Node와 Bun 행은 더 긴 벤치마크 루프로 전체 재실행이 필요하다. 스모크 테스트에서는 짧은 루프 실행이 JIT를 충분히 워밍업하지 못한다는 점이 드러났다.wide_arithmetic 명령어는 런타임이 이를 지원할 때 암호화 코드에서 매우 큰 의미가 있다.테스트 프로그램은 libsodium의 벤치마크 스위트이며, libsodium 커밋 8e3be8615ba6adcd7babaecf5e76f516890ba5fb에서 빌드했다.
네이티브 기준선 하나와 여러 WebAssembly 변형을 빌드했다.
lime1을 사용한 WebAssemblylime1과 simd128을 사용한 WebAssemblylime1, simd128, wide_arithmetic을 사용한 WebAssembly네이티브 기준에서는 libsodium을 -Dcpu=native로 빌드했다. wasm2c의 경우 생성된 C를 zig cc -O3 -march=native로 컴파일했다.
WAMR에는 AOT 모드를 사용했다. wamrc가 각 .wasm 파일을 .aot 파일로 컴파일하고, iwasm이 결과 AOT 파일을 실행했다. wamrc는 --cpu=native를 받지 않으므로, --target=x86_64 --cpu=x86-64-v4 --opt-level=3를 사용했다. 이것은 호스트에서 사용 가능한 x86-64 기능 수준과 일치하며, 이 모듈들을 컴파일할 수 있었던 WAMR 버전들 전반에서 동작한다.
네이티브 명령은 다음과 같았다.
zig build -Denable_benchmarks -Doptimize=ReleaseFast -Dcpu=native -Diterations=3
WebAssembly 명령도 같은 형태였고, wasm32-wasi 타깃과 기능별 CPU 문자열을 사용했다.
zig build -Denable_benchmarks -Dtarget=wasm32-wasi -Doptimize=ReleaseFast -Diterations=3
zig build -Denable_benchmarks -Dtarget=wasm32-wasi -Doptimize=ReleaseFast -Dcpu=lime1 -Diterations=3
zig build -Denable_benchmarks -Dtarget=wasm32-wasi -Doptimize=ReleaseFast -Dcpu=lime1+simd128 -Diterations=3
zig build -Denable_benchmarks -Dtarget=wasm32-wasi -Doptimize=ReleaseFast -Dcpu=lime1+simd128+wide_arithmetic -Diterations=3
호스트는 AMD Ryzen AI 9 HX 470, 12코어 24스레드였다. CPU 부스트는 비활성화했고 최대 CPU 주파수는 2 GHz였다. 운영체제는 Linux 7.1.0-rc7, Zig는 0.17.0-dev.948+e949341b7였다.
아래 수치는 네이티브 빌드 대비 각 벤치마크 감속 비율의 기하평균이다. 낮을수록 좋다. 이 머신에서 2.0은 “네이티브보다 두 배 느림”을 뜻한다.
ITERATIONS=3를 사용했기 때문에 매우 작은 libsodium 테스트는 잡음이 크고 양자화되어 있다. 시간이 0으로 보고된 행은 집계에서 제외했다. 벤치마크 프로세스를 특정 코어에 고정하지도 않았다. 그럼에도 전반적인 런타임 동작을 비교하는 데는 여전히 유용하지만, 소수점 마지막 자리까지 의미를 부여해서는 안 된다.
WAVM을 제외한 모든 런타임에 대해 2026년 6월 23일 시점의 최신 안정 릴리스와, 대략 1년 전 및 2년 전의 안정 릴리스를 사용했다.
| Runtime | 2024 | 2025 | 2026 |
|---|---|---|---|
Bun | 1.1.16 | 1.2.17 | 1.3.14 |
Node | 22.3.0 | 24.2.0 | 26.3.1 |
WAMR | 2.1.0 | 2.3.1 | 2.4.4 |
WABT wasm2c | 1.0.35 | 1.0.37 | 1.0.41 |
WasmEdge | 0.14.0 | 0.14.1 | 0.17.0 |
Wasmer | 4.3.2 | 6.0.1 | 7.1.0 |
Wasmtime | 22.0.0 | 34.0.0 | 46.0.0 |
WAVM | n/a | n/a | nightly/2026-04-05 |
Wazero | 1.7.3 | 1.9.0 | 1.12.0 |
WAVM은 과거 비교가 까다롭다. 예전의 사용 가능한 nightly가 2024와 2025 슬롯 모두에서 2022 바이너리로 귀결됐고, 그 바이너리는 이 머신에서 실행을 거부했다. 그래서 2026 nightly만 남겼다.
선정한 2024 릴리스인 WAMR 2.1.0은 설치는 잘 되었지만, 그 AOT 컴파일러가 이 Zig 생성 모듈들에서 invalid WASM stack data type 오류와 함께 실패했다. 이 버전은 표에는 남겨 두었지만, 집계에는 포함하지 않았다.
이것은 lime1, SIMD, wide arithmetic 없이 빌드한 일반 WebAssembly이다.
| Runtime | 2024 | 2025 | 2026 |
|---|---|---|---|
WAVM | n/a | n/a | 1.41 |
WAMR AOT | n/a | 1.59 | 1.57 |
WasmEdge | 1.66 | 1.98 | 1.74 |
wasm2c | 2.01 | 2.08 | 1.86 |
Wasmer | 2.13 | 2.56 | 2.08 |
Wasmtime | 2.67 | 2.54 | 2.41 |
Wazero | 4.84 | 4.70 | 4.72 |
Node | 8.60 | 8.22 | 7.95 |
Bun | 27.41 | 26.42 | 8.77 |
보편적으로 적용되는 단 하나의 추세가 있는 것은 아니다.
Wasmtime은 꾸준히 개선됐다. 2024년에는 네이티브의 2.67배 느렸고, 2025년에는 2.54배, 2026년에는 2.41배였다. 혁명적인 변화는 아니지만 실제 진전이다.
Node도 8.60배에서 7.95배로 천천히 개선됐다.
Wazero는 사실상 평평했다. 4.84배, 4.70배, 4.72배였다. 나쁘지는 않지만, 이 벤치마크에서는 지난 2년 동안 큰 속도 향상이 보이지 않는다.
AOT 모드의 WAMR은 2025년에 이미 빨랐고 2026년에 약간 더 빨라졌다. 1.59배 느림에서 1.57배 느림으로 갔다. WAMR 2.1.0이 이 모듈들을 컴파일하지 못했기 때문에 2024년의 완전한 WAMR 수치는 없다.
Wasmer는 내가 테스트한 2025 릴리스에서 퇴보했다가 2026년에 회복했다. 2026년 기준선은 2024년 기준선보다 약간 더 빠르지만, 차이는 크지 않다.
wasm2c는 2026년에 완만하게 개선됐다. 배포 모델상 WebAssembly를 미리 네이티브 C로 변환하는 것이 허용된다면, 여전히 가장 좋은 선택지 중 하나다.
Bun은 예외적인 사례다. 2024년과 2025년 결과는 한참 뒤처졌지만, 2026년 결과는 2025년보다 약 3배 빨랐다. 이 벤치마크에서는 여전히 Node보다 느리지만, 방향성은 매우 좋다.
WasmEdge도 빠르지만, 명령행 동작이 충분히 달라져서 결과에 영향을 줬다. 내가 처음 실행한 0.17.0은 실수로 컴파일된 모듈에 인터프리터 모드를 사용했고, 재앙적으로 느려 보였다. 컴파일된 모듈을 --run-mode=aot로 실행하자 문제가 해결됐다. 2026년 기준선은 네이티브 대비 1.74배로, 2024년과 2025년 기준선 결과 사이에 위치했다.
기준선 표는 모든 곳에서 같은 WebAssembly 타깃을 비교하기 때문에 유용하다.
하지만 실제 배포를 위해 런타임을 고른다면, 그 런타임이 실제로 실행할 수 있는 가장 빠른 빌드에 더 관심이 있을 가능성이 크다.
그래서 각 런타임과 연도별로, 지원되는 빌드들인 기준선, lime1, lime1+simd128, lime1+simd128+wide_arithmetic 가운데 가장 좋은 완전한 결과도 골랐다.
| Runtime | 2024 best | 2025 best | 2026 best |
|---|---|---|---|
WAVM | n/a | n/a | 1.41 (baseline) |
WAMR AOT | n/a | 1.42 (lime1+simd128) | 1.42 (lime1+simd128) |
WasmEdge | 1.62 (lime1+simd128) | 1.64 (lime1) | 1.64 (lime1) |
wasm2c | 2.01 (baseline) | 2.08 (baseline) | 1.86 (baseline) |
Wasmer | 2.09 (lime1) | 2.49 (lime1) | 1.33 (lime1+simd128+wide_arithmetic) |
Wasmtime | 2.60 (lime1+simd128) | 1.52 (lime1+simd128+wide_arithmetic) | 1.46 (lime1+simd128+wide_arithmetic) |
Wazero | 4.84 (baseline) | 4.64 (lime1) | 4.71 (lime1+simd128) |
Node | 8.60 (baseline) | 7.99 (lime1) | 7.95 (baseline) |
Bun | 27.35 (lime1) | 26.23 (lime1) | 8.77 (baseline) |
최고 지원 빌드 기준으로 현재 연도 전체 결과를 순위 매기면 다음과 같다.
| 2026 rank | Runtime | Best build | Slowdown vs native |
|---|---|---|---|
| 1 | Wasmer | lime1+simd128+wide_arithmetic | 1.33 |
| 2 | WAVM | baseline | 1.41 |
| 3 | WAMR AOT | lime1+simd128 | 1.42 |
| 4 | Wasmtime | lime1+simd128+wide_arithmetic | 1.46 |
| 5 | WasmEdge | lime1 | 1.64 |
| 6 | wasm2c | baseline | 1.86 |
| 7 | Wazero | lime1+simd128 | 4.71 |
| 8 | Node | baseline | 7.95 |
| 9 | Bun | baseline | 8.77 |
WebAssembly 기능 지원 이야기는 연도별 런타임 이야기보다 더 흥미롭다.
2026년 릴리스들에서 집계된 감속 비율은 다음과 같았다.
| Runtime | baseline | lime1 | lime1+simd128 | lime1+simd128+wide_arithmetic |
|---|---|---|---|---|
WAVM | 1.41 | 1.59 | 1.43 | unsupported |
WAMR AOT | 1.57 | 1.44 | 1.42 | unsupported |
WasmEdge | 1.74 | 1.64 | 1.76 | unsupported |
Wasmer | 2.08 | 2.02 | 2.03 | 1.33 |
Wasmtime | 2.41 | 2.30 | 2.37 | 1.46 |
Wazero | 4.72 | 4.77 | 4.71 | unsupported |
Node | 7.95 | 8.05 | 8.25 | unsupported |
Bun | 8.77 | 11.05 | 9.53 | unsupported |
여기서는 lime1과 simd128만으로는 마법 같은 효과가 없다. 때로는 도움이 되고, 때로는 손해가 되며, 때로는 차이가 벤치마크 잡음 속에 묻힌다.
wide_arithmetic은 다르다.
내가 테스트한 완전한 안정 릴리스 행 가운데 전체 wide_arithmetic 빌드를 실행할 수 있었던 것은 Wasmtime과 Wasmer뿐이었다. WAMR은 지원되지 않는 opcode 0xfc13와 함께 이를 거부했다. 그러나 wide_arithmetic이 동작했을 때는 실험 전체에서 가장 큰 속도 향상을 보였다.
Wasmtime 46.0.0: 이것이 없을 때는 네이티브의 2.41배 느렸고, 있을 때는 1.46배 느렸다.Wasmer 7.1.0: 이것이 없을 때는 네이티브의 2.08배 느렸고, 있을 때는 1.33배 느렸다.이런 변화는 마음에 든다. libsodium의 많은 고비용 연산은 산술 연산 비중이 크다. WebAssembly ISA가 그 산술을 직접 표현할 수 있다면, 런타임은 C 컴파일러가 이미 알고 있던 것을 다시 찾아내기 위해 훨씬 적은 일을 하면 된다.
대부분의 실행은 문제 없이 완료됐지만, 전부 그런 것은 아니었다.
Bun 1.2.17은 기준선 빌드에서 box_easy에 실패했다. Bun 1.1.16은 lime1과 lime1+simd128 빌드에서 pwhash_argon2i에 실패했다. Node 22.3.0은 기준선, lime1, lime1+simd128 빌드에서 pwhash_argon2i, pwhash_argon2id, pwhash_scrypt에 실패했다.
Node 22.3.0의 패스워드 해싱 실패는 Node의 JavaScript 힙이나 스택 설정을 늘려도 해결되지 않았다. Wasm 모듈에 명시적인 최대 선형 메모리를 지정하자 해결됐다. 기준선 빌드에서는 최대 1024페이지, 즉 64 MiB를 주면 pwhash_argon2i, pwhash_argon2id, pwhash_scrypt가 완료됐다. pwhash_scrypt는 512페이지에서는 실패했고, 1536페이지 이상에서는 다시 세그폴트가 났으므로, 이것은 단순한 “메모리가 많을수록 좋다” 설정이라기보다 V8의 메모리 모드 임계값처럼 보인다.
2024 슬롯의 WAMR 2.1.0은 AOT 모드에서 기준선 모듈조차 컴파일할 수 없었다. WAMR 2.3.1과 2.4.4는 기준선, lime1, lime1+simd128 빌드는 컴파일하고 실행했지만 wide_arithmetic은 지원하지 못했다.
그러한 실패는 집계에서 제외했다. 보고된 median이 0인 벤치마크 행도 마찬가지로 제외했다.
일부는 그렇다.
Wasmtime은 가장 깔끔하게 그렇다고 답할 수 있다. 이 벤치마크에서 매년 더 빨라졌다. 엄청나게 빨라진 것은 아니지만, 일관되게 빨라졌다.
Node도 그렇지만 기울기는 완만하다.
Bun은 2025년에서 2026년 사이에 매우 분명하게 그렇다. 이 작업 부하에서는 아직 따라잡아야 할 거리가 많지만, 개선 폭이 너무 커서 무시할 수 없다.
Wazero는 대체로 평평하다.
WAMR도 여기서 동작한 버전들 사이에서는 대체로 평평하지만, 네이티브의 약 1.4배에서 1.6배 수준에서의 “평평함”은 매우 좋은 위치다.
기준선만 보면 Wasmer는 혼재된 모습이지만, 2026년 릴리스의 wide_arithmetic 지원은 암호화 코드에 대한 실질적인 답을 바꾼다. 이 기능을 활성화하면, 정상적인 최신 릴리스들 사이에서 비교 가능한 완전한 2026년 결과 중 가장 빨랐다.
wasm2c는 여전히 좋다. WebAssembly를 미리 C로 변환해 호스트용으로 컴파일할 수 있다면, 이를 이기기 어렵다.
WAVM은 2026년 기준선에서 가장 빠른 수치를 냈지만, 2024년이나 2025년과의 공정한 비교는 없다.
WasmEdge는 AOT 모드가 강제되면 여전히 뛰어나다. 실수로 인터프리터 모드로 실행했던 사례는 명령행 기본값도 벤치마크의 일부라는 좋은 상기였다.
WebAssembly에서 CPU 집약적 암호화를 실행한다면, 런타임 선택은 여전히 매우 중요하다.
가장 빠른 최신 완전 결과와 가장 느린 최신 결과 사이의 격차는 크다. wide_arithmetic을 사용한 Wasmer는 네이티브의 1.33배 느린 수준이었지만, 최신 Bun 기준선은 8.77배 느렸다.
기능 지원도 중요하다. 같은 런타임이라도 WebAssembly 모듈이 더 나은 산술 명령을 사용할 수 있으면 “꽤 괜찮음”에서 “놀랄 만큼 네이티브에 가까움”으로 이동할 수 있다.
위안이 되는 부분은 주류 런타임들이 가만히 있지 않다는 점이다. Wasmtime은 꾸준히 개선됐다. Bun은 큰 도약을 했다. Wasmer는 실제 암호화 작업 부하에 중요한 기능을 얻었다. WasmEdge는 AOT 실행 모드가 명시되자 계속 빠른 모습을 보였다.
덜 위안이 되는 부분은 WebAssembly 성능이 여전히 하나의 단일한 것이 아니라는 점이다. 런타임, 릴리스, 활성화된 WebAssembly 기능, 코드가 JavaScript에서 WASI를 거치는지 여부, 그리고 사전 네이티브 컴파일이 허용되는지 여부에 따라 달라진다.
그러니 실제 작업 부하를 벤치마크하라.
하지만 작업 부하가 libsodium과 비슷하다면, 2026년의 답은 이렇다. WebAssembly는 네이티브에 가까울 수 있고, wide_arithmetic은 신경 쓸 가치가 있으며, 그리고 그렇다. 일부 런타임은 정말로 더 빨라지고 있다.