원문: https://dassur.ma/things/is-postmessage-slow/
기타 참고: https://web.dev/articles/load-faster-like-proxx
그렇지 않다. (상황에 따라 다르다.)
'느리다'는 것은 무슨 의미인가?
이전에 언급했듯이, 무엇이든 측정을 해보지 않고 느리다고 '단정'지을 수는 없으며, 심지어 측정을 하더라도 그 발생 맥락을 알아내지 않으면 그 값들은 의미가 없다.
이는 달리 말하면, 사람들이 `postMessage()`가 왠지 느릴 것이라 '단정'짓고 Web Workers 사용을 꺼리기 때문에 역으로 이를 측정해볼 필요가 있다는 의미도 된다.
Worker를 주제로 하는 필자의 이전 게시글에서도 이에 대한 질문을 받았다. 이제부터 `postMessage()`의 실질적인 성능을 측정하고 사람들이 성능이 떨어진다고 지목하는 원인들을 분석해 보자. `postMessage()`가 실사용에서 성능이 너무 낮다면 무엇을 해야 할까?
postMessage의 작동 원리
성능 측정을 수행하기 전에 우선 `postMessage()`가 정확히 무엇이며, 어떤 부분을 측정하고자 하는지 짚고 넘어가자. 그렇지 않으면 무의미한 데이터를 수집해서 무의미한 결과를 도출하는 행위로 그칠 것이기 때문이다.
`postMessage()`는 HTML 명세의 일부이다 (ECMA-262 아님!).
필자의 깊은 복사 게시글에서 언급했듯이, 한 JS 실행 영역에서 다른 실행 영역으로 Message를 복사하기 위해 실행되는 `postMessage()`는 '구조화 복제(Structured Cloning)'에 기반한다. 여기서 '구조화 복제'는 두 단계의 과정으로 이뤄진다.
Structured Clone 알고리즘
- Message에 대해 `StructuredSerialize()`를 수행한다
- 수신 영역에서 아래의 작업들을 실행하는 작업 하나를 큐에 넣는다
2-1. 직렬화된 Message에 대해 `StructedDeserialize()`를 수행한다
2-2. `MessageEvent` 를 생성하고 수신 port에서 역직렬화된 Message에 `MessageEvent`를 발생시킨다
본 요약이 기술적으로 정확하지는 않다. 그 일례로, `StructedSerialize()` 및 `StructuredDeserialize()` 는 사실 JS에서 실행 가능하도록 노출된 함수 따위는 아니다 (현재로서는).
여기서의 `StructuredSerialize()`와 `StructuredDeserialize()`는 각각 `JSON.stringify()`와 `JSON.parse()`의 보다 스마트한 버전이라고 생각할 수 있다. 스마트한 이유는 이 함수들이 Map, Set, ArrayBuffer 등의 내장 자료구조나 순환 참조 구조를 다루는 것이 가능하기 때문이다.
(이런 스마트한 특성으로 인해 성능에 손실이 있을 수도 있을까? 이는 뒤에서 자세히 다룰 예정이다.)
다만 위의 알고리즘만으로는 나타나지 않는 부분이 있다.
직렬화 작업은 전송 영역에 블로킹을 발생시키고, 역직렬화 작업은 수신받는 영역에 블로킹을 발생시킨다.
또한 Chrome과 Safari는 `StructuredDeserialize()`역직렬화 작업을 바로 실행하지 않고, MessageEvent의 `.data` 속성에 실질적으로 접근하는 시점에 실행한다.
반면에 Firefox는 MessageEvent를 발생시키기 이전에 미리 역직렬화를 수행한다.
참고: 이러한 동작들은 명세에 부합하고 합당한 것들이다. 필자는 이와 관련으로 Mozilla에 버그를 제기해 큰 페이로드의 역직렬화로 인한 '블로킹' 시점을 개발자가 제어 가능하도록 구현이 가능한지 의견을 제시해보기도 했다.
이를 종합적으로 판단하여 성능 측정의 대상을 결정해야 한다.
엔드-투-엔드 방식으로 Worker에서 메인 스레드로 Message가 도착하기까지의 경과 시간을 측정할 수 있다. 하지만 그렇게 측정한 시간은 각각 다른 영역에서 실행되는 직렬화와 역직렬화 시간을 합해서 나타내게 된다.
Worker를 다루는 본문에서의 노력들은 근본적으로 메인 스레드를 빠릿하게 유지하기 위함임을 명심해야 한다.
그 대신에 측정 대상을 Firefox를 제외한 Chrome과 Safari로 한정한다면, `.data` 속성 접근 경과 시간을 측정함으로써 `StructuredDeserialize()`, 즉 역직렬화 시간만을 알아내는 것이 가능해진다.
다만 `StructuredSerialize()`, 즉 직렬화 시간만을 알아낼 방법은 찾지 못했다.
이 두 가지 방식 모두 이상적이지는 못하지만, 유연한 웹 앱을 구축한다는 목표를 위해, 필자는 엔드-투-엔드 방식으로 `postMessage()`의 실행 비용을 정확히 측정해 내기로 했다.
이러한 `postMessage()`의 개념적 이해를 충분히 갖춘 채로, 필자는 💀마이크로밴치마크💀를 이용할 것이다. 부디 측정된 수치들과 실사용에서의 차이를 명심하기 바란다.
측정 1: Message 전송은 얼마나 시간이 걸릴까?

측정을 위해 1~6 범위의 고유한 '폭'과 '깊이'를 가진 객체를 생성한다. 이러한 객체 1000개를 `postMessage()`를 통해 Worker에서 메인 스레드로 전송한다.
객체 속성의 키는 길이 16의 16진수인 무작위 난수의 문자열이고,
값은 무작위 불리언 or 무작위 부동소수점 or 길이 16의 16진수인 무작위 난수의 문자열이다.
전송 시간을 측정하여 백분위 95%까지 계산한다.
결과
벤치마크는 각각 2018 MacBook Pro을 기반으로 Firefox/Safari/Chrome에서, Pixel 3XL을 기반으로 Chrome에서, Nokia 2를 기반으로 Chrome에서 실행되었다.
참고: 시각화를 위한 코드는 이 gist에서 확인할 수 있다. 필자는 Python을 이용하는 것이 난생처음이기 때문에 코드 품질에 대해서는 좋게 봐주었으면 한다.
Pixel 3, Safari의 지표에서 위화감이 느껴질 수 있다.
왜냐하면 '스펙터&멜트다운'이 발견되었을 당시에 모든 브라우저가 (SharedArrayBuffer 사용을 차단하고) performance.now() 등 타이머의 정확도를 감소시킨 것이 영향을 미쳤기 때문이다. 오직 Chrome 데스크톱 버전만이 사이트 격리를 도입하면서부터 이런 조치를 되돌릴 수 있었다.
브라우저들은 `performance.now()`의 정확도를 아래의 수치만큼 제한해 두었다.
- Chrome (데스크톱): 5µs
- Chrome (안드로이드): 100µs
- Firefox (데스크톱): 1ms (플래그를 통해 제한을 비활성화함)
- Safari (데스크톱): 1ms
이 지표를 확인하면 객체의 복잡도가 직렬화/역직렬화 작업에 큰 영향을 미치는 것을 알 수 있다. 이는 이 작업들이 객체를 통으로 처음부터 끝까지 변환하는 작업임을 감안하면 당연한 것이다.
또한 객체의 JSON 표현의 크기를 알면 대략적으로 객체 전송 시간을 가늠해 볼 수 있다는 것도 알 수 있다.
측정 2: postMessage를 느리게 하는 요인은 무엇인가?
위의 추론을 확실히 하기 위해 벤치마크를 수정했다.
넓이와 폭이 1~6인 고유한 객체들을 생성한다.
이에 더해 모든 leaf 속성(중첩된 객체와 속성 중 가장 말단에 있는 속성)의 값을 16B ~ 2KiB 정도 크기의 문자열로 정한다.
결과

상호 관계를 확정 지어도 될 수준이다. 객체의 전송시간은 그 객체의 JSON 표현 문자열 길이에 비례한다.
하지만 더 중요한 것은 이 비례관계가 오직 100KiB를 넘어가는 큰 객체에서만 영향을 끼친다. 비례관계가 수학적으로는 맞지만, 페이로드가 작을수록 변동이 심한 것으로 보인다.
평가: Message 전송 시간에 대해서
이제 측정값을 얻게 되었지만, 역시 이의 맥락을 파악하지 않는다면 소용이 없다. 의미 깊은 결론을 제시하고자 한다면 '느리다'는 것이 무엇인지 정의할 필요가 있다.
이는 budget을 정의함으로써 다룰 수 있기에, 본문에서는 다시 한번 RAIL 가이드를 활용할 것이다.
필자의 경험상 Worker는 최소한, 앱의 상태 객체를 관리하는 핵심적 책임이 있다. 상태는 주로 유저가 상호작용 했을 때만 변경된다.
RAIL 가이드의 '사용자 상호작용' 대응 시간은 100ms이다. 따라서 기기가 아무리 느리더라도, 객체의 크기 100KiB 까지는 `postMessage()`로 전송하더라도 budget 내에 처리할 수 있다.
이것은 JS로 애니메이션을 구현하는 경우라면 예외이다. 매 프레임마다 화면을 업데이트하기 때문에 애니메이션 RAIL budget은 16ms이다. 만약 Worker에서 전송하는 Message의 크기로 인해 이보다 오래 블로킹이 발생한다면 문제일 것이다. 앞선 측정 수치를 보아 10KiB 이상인 크기의 객체는 애니메이션 budget 내에 처리할 수가 없다.
이는 결국 메인 스레드 JS 애니메이션 구현보다 CSS 애니메이션/트랜지션이 나은 가장 큰 이유이다. CSS의 애니메이션/트랜지션은 '컴포지터 스레드'에서 별개로 실행되어 메인 스레드의 블로킹에 영향을 받지 않는다.
반드시. 더 많은. 데이터를. 보내라.
필자의 경험상, `postMessage()`는 off-main-thread 아키텍처를 채택하는 앱 대부분에서 병목지점이 아니다. 다만 보내려는 Message가 매우 크거나 잦은 빈도로 전송해야 하는 경우에는 그럴 수 있음을 필자도 인정한다.
독자 본인이 생각하기에 기본 `postMessage()`가 너무 느리다면 어떻게 해야 하겠는가?
패칭
상태 객체의 경우에, 그 객체 자체는 제법 클 수 있으나 변경되는 것은 몇몇 개의 깊이 중첩된 속성일 뿐인 경우가 많다.
필자는 해당 문제를 PWA 지뢰찾기 클론인 PROXX를 개발하다가 직면하게 되었다. 해당 게임은 2차원 배열로 게임 속의 셀 격자를 구현한다. 각 셀에서 지뢰가 있는지, 클릭으로 드러났는지, 깃발이 세워졌는지의 여부가 저장된다.
interface Cell {
hasMine: boolean;
flagged: boolean;
revealed: boolean;
touchingMines: number;
touchingFlags: number;
}
이에 따라 최대 크기 40x40 격자는 ~134KiB 까지 이르는 JSON 데이터가 된다. 이러한 상태 객체를 통으로 전송하는 것은 애초에 불가능하다. 따라서 그 대신, 필자는 객체의 변화를 분석하여 Patchset(변경점 모음)을 전송하는 방식을 채택했다.
이에 대해서 해당 프로젝트에서는 사용하지 않았지만, 불변 객체를 다루는 ImmerJS 라이브러리를 이용하면 Patchset을 간편하게 생성하고 적용할 수가 있다.
// worker.js
immer.produce(stateObject, draftState => {
// Manipulate `draftState` here
}, patches => {
postMessage(patches);
});
// main.js
worker.addEventListener("message", ({data}) => {
state = immer.applyPatches(state, data);
// React to new state
}
ImmerJS가 생성한 Patchset은 다음과 같다.
[
{
"op": "remove",
"path": [ "socials", "gplus" ]
},
{
"op": "add",
"path": [ "socials", "twitter" ],
"value": "@DasSurma"
},
{
"op": "replace",
"path": [ "name" ],
"value": "Surma"
}
]
이것의 의의는 즉, 전송되는 데이터의 크기가 객체 자체의 크기를 따르지 않고, 객체 변경 내용의 크기에 따라 비례하는 것이다.
청크화
앞서 말했듯이, 상태 객체가 변할 시에 일부 적은 수의 속성만 변하는 경우가 많다.
물론 그렇지 않은 경우도 있을 수 있다.
심지어 PROXX에서도 Patchset의 크기가 제법 커지는 상황이 존재했다. 게임 내에서 처음 클릭하여 드러낼 때 게임 격자의 80%가 변화하는데, 이때의 Patchset 크기는 ~70KiB 에 이른다. 당시 JS의 WebGL로 애니메이션을 구현할 예정이었기 때문에, 폴더폰/피쳐폰 대상 기기에서는 너무 큰 크기었다.
그렇게 아키텍처적 문제에 대해 스스로 반문하게 되었다. 해당 구조에서 부분적 업데이트를 구현할 수 있을까?
사실 Patchset이란 작은 변경점들을 모아놓은 것에 불과하다. 기존처럼 모든 변경점들을 Patchset에 전부 넣어 전송하는 대신, Patchset을 '청크' 덩어리로 쪼개어 순차적으로 적용하도록 구현이 가능했다. 변경점 #1~10을 첫 Message에 보내고, #11~20는 그다음으로, 이런 식으로 계속 말이다.
이를 극단적으로 말한다면, 변경점들을 전달하는 적절한 통신 방법을 설정한다면 '리액티브 프로그래밍' 관점에서 선호되는 패턴들을 모두 사용할 수가 있게 된다.
이는 물론 주의를 기울이지 않는다면 불완전하거나 전부 어긋나버릴 수도 있다.
이에 대해 '청크화' 방법 및 충돌을 피하기 위한 Patchset 순서 배열 규칙은 본인 스스로가 결정할 수 있다. 예를 들어 첫 번째 '청크'가 화면상의 요소에 영향을 주는 모든 변경점들을 포함하도록 하고, 나머지 변경점들은 두 개의 Patchset에 나눠 담아서 메인 스레드를 여유 있게 유지할 수 있을 것이다.
PROXX의 경우엔 '청크화'가 구현되었다. 유저가 화면을 클릭하면 Worker가 전체 격자를 순회하면서 변경점을 판별하여 리스트에 저장한다. 리스트의 크기가 일정 수치를 넘어서면 메인 스레드로 전송하고, 리스트를 비운 후에 다시 순회를 이어간다.
이를 통하여 Patchset의 크기가 충분히 작아지는 덕분에 `postMessage()`의 성능 손실은 폴더폰/피쳐폰 환경에서도 미세해질 만큼 완화되고, 메인 스레드의 UI 업데이트 budget은 남아도는 수준이 된다.
순회 알고리즘은 첫째 격자에서부터 외곽으로 범위가 확장되므로, 변경점의 순서도 이와 비슷한 순서를 따르게 된다.
만약 메인 스레드의 성능이 프레임 budget 내에서 고작 Message 하나만 처리 가능한 기기에서 구동된다면 (거진 80년대 컴퓨터처럼), 이런 부분적 업데이트가 서서히 드러나는 애니메이션처럼 보일 수도 있겠다.
만약 성능 좋은 기기에서 구동한다면, 메인 스레드는 JS 엔진의 이벤트 루프에 따라 budget 내에서 최대한 많은 수의 MessageEvent들을 처리하게 된다.
https://dassur.ma/assets/proxx-reveal.8b391892.mp4
유서깊은 구현 트릭: PROXX에서는 Patchset을 '청크화' 하는 것이 마치 애니메이션처럼 보이게 된다. 이는 저사양 모바일 기기 혹은 6배 CPU 스로틀링 활성화 데스크톱처럼 제한적인 상황에서만 나타나는 모습이다.
JSON을 활용해 볼까?
`JSON.parse()` 및 `JSON.stringify()` 는 굉장히 빠르다. JSON은 JS의 하위 요소로서 '파서'가 이를 특별히 처리해야 할 경우가 드물다. 또한 매우 광범위하게 사용되기 때문에 이미 강력하게 최적화되어 있다.
Mathias님이 최근 발견한 바로는, JS에서 대형 객체를 `JSON.parse()` 로 래핑하여 '파싱' 시간을 단축시킬 수 있는 경우가 가끔 존재한다. 그러면 `postMessage()`에 JSON을 응용하여 성능을 향상할 수도 있을까? 아쉽게도 아니었다.

둘의 성능에서 또렷한 차이는 없었으나, 기본 `postMessage()`가 최고의 상황에선 더 빠르고, 최악의 상황에선 둘 다 동등하게 느린 것으로 확인된다.
바이너리 형식
'구조화 복제'의 성능 영향에 타협하는 또 다른 방법은 그냥 원천적으로 쓰지 않는 것이다. 객체를 '구조화 복제'하는 방식 외로, `postMessage()`는 일부 제한적인 타입을 전송할 수 있다. 그 전송 가능한 타입들 중 하나가 `ArrayBuffer`이다.
'Buffer'라는 명칭에서 나타나듯이 `ArrayBuffer`를 전송할 때는 복사를 하지 않는다. 그 대신 전송하는 영역에서 Buffer의 접근권을 잃고 수신받는 영역이 이를 갖는 방식이다.
`ArrayBuffer`의 전송은 데이터 크기에 상관없이 굉장히 빠르다. 로우레벨 상에서는 그저 연속적인 메모리 덩어리일 뿐이기 때문이다.
`ArrayBuffer`를 유용하게 쓰려면 본인 스스로 데이터를 `ArrayBuffer` 형식으로 마샬링 하는 방식을 규정해놓아야 한다. 이는 번거롭긴 하지만, 빌드 시점에 데이터의 형태와 구조를 확정함으로써 평상적인 복사 알고리즘에서는 불가능했던 다양한 최적화의 여지를 만들어준다.
이러한 최적화를 가능케 하는 형식 중 하나가 FlatBuffers이다. FlatBuffers는 JS(및 기타 언어) 전용의 컴파일러가 마련되어서 스키마 구문을 코드로 변환한다. 변환된 코드에 데이터 직렬화/역직렬화 함수가 포함되어있다.
더 흥미로운 것은, Flatbuffers는 `ArrayBuffer`에서 포함된 값을 반환하기 위해 이를 통째로 파싱하거나 '언팩'할 필요가 없다.
웹어셈블리
모두가 선호하는 선택지인 웹어셈블리는 어떤가? 여타 언어 생태계의 직렬화 라이브러리를 이용하기 위해 Wasm을 활용하는 방안이 존재한다.
JSON을 상당 부분 차용한 바이너리 객체 형식인 CBOR은 다양한 언어들에서 구현되어 있다.
(앞서 언급한 FlatBuffers 및 ProtoBuffers 또한 다양한 언어를 지원한다.)
하지만 좀 더 극단적으로 갈 수 있다. 언어의 메모리 레이아웃을 자체적인 직렬화 형식으로 의존하는 것이다.
필자는 Rust를 통해 조그만 예시 하나를 작성했다. (본인 앱의 상태를 나타내는) `State` 구조체를 통하여 JS의 상태를 확인하고 조작할 수 있다.
상태 객체를 직렬화하려면 구조체가 할당된 메모리 구간을 복제하면 된다.
역직렬화하려면 새로운 `State` 구조체를 할당하여 역직렬화 함수에서 전달받은 데이터로 덮어쓴다.
두 경우 전부 동일한 Wasm 모듈을 사용하기에 메모리 레이아웃이 동일하다.
본 코드는 그저 해설을 위한 예시일 뿐이다.
예시의 구조체가 (`Vec`이나 `String`처럼) 포인터를 포함하게 되면 예상치 못한 작용이 발생하기 쉽다.
본 코드는 또한 몇몇 불필요한 복사도 발생시키고 있다. 주의하며 코딩하길 바란다!
pub struct State {
counters: [u8; NUM_COUNTERS]
}
#[wasm_bindgen]
impl State {
// Constructors, getters and setter...
pub fn serialize(&self) -> Vec<u8> {
let size = size_of::<State>();
let mut r = Vec::with_capacity(size);
r.resize(size, 0);
unsafe {
std::ptr::copy_nonoverlapping(
self as *const State as *const u8,
r.as_mut_ptr(),
size
);
};
r
}
}
#[wasm_bindgen]
pub fn deserialize(vec: Vec<u8>) -> Option<State> {
let size = size_of::<State>();
if vec.len() != size {
return None;
}
let mut s = State::new();
unsafe {
std::ptr::copy_nonoverlapping(
vec.as_ptr(),
&mut s as *mut State as *mut u8,
size
);
}
Some(s)
}
참고: Ingvar님이 필자에게 Abomonation을 권했는데, 무려 포인터를 다루던 이상한 직렬화 라이브러리었다. 그는 "이거 절대 사용하지 마세요!" 라며 조언해 줬다.
해당 Wasm 모듈은 최종적으로 gzip 압축 시 3KiB 크기에 달했다. 이중 대부분은 메모리 관리 기능과 핵심 라이브러리로 인한 것이다.
상태 객체는 변경이 발생할 때마다 통째로 전송되지만, `ArrayBuffers`의 가벼움 덕분에 성능 비용은 매우 적다. 즉 이 방식은 상태 객체의 크기에 상관없이 거의 일정한 전송 시간을 갖게 된다. 다만 상태 데이터로 접근하는 성능 비용이 상대적으로 크다. 언제나 장단점은 있는 것이다!
또한 이 방식은 상태 구조체에서 포인터와 같은 어떤 형식의 참조든 만들지 않아야만 하는데, 그러한 값들이 새로운 Wasm 모듈 인스턴스로 복제될 때 유효하지 않을 수도 있기 때문이다.
결국 이 방안을 하이레벨 언어들에서 활용하고자 한다면 필히 까다로울 것이다. 따라서 필자는 메모리 레이아웃의 직접적인 파악과 접근이 가능한 언어들인 C, Rust, AssemblyScript를 추천한다.
SharedArrayBuffer & 웹어셈블리
주의: 이번 목차는 Chrome 데스크톱 버전을 제외한 모든 브라우저에서 비활성화된 `SharedArrayBuffer`에 대해 다룬다. 작업은 진행 중이지만 어떠한 ETA도 제공되지 않는다.
필자는 특히 게임 개발자들로부터 JS에서 여러 스레드에서 객체를 공유하며 사용할 수 있도록 해달라는 요청을 많이 받았다. 그러나 개인적으로 이 기능이 완벽하게 JS에 추가되는 것은 불기능하다고 생각하는 것이, 이는 JS 엔진의 근본적인 원칙을 위배하기 때문이다. 하지만 SharedArrayBuffer("SAB")를 통해서라면 가능하다.
SAB는 `ArrayBuffer`와 동일하게 작동하나, 전송하는 영역에서 접근권을 잃지 않기에 송·수신 영역 양쪽에서 메모리 접근권을 갖게 된다. SAB가 JS 영역에서 '공유 메모리 모델'을 사용할 수 있게 해준다.
영역 간의 동기화에 대해서는 Atomics가 뮤텍스 및 원자적 연산을 지원한다.
SAB를 활용한다면 메모리 레이아웃을 앱 시작 시에 한 번만 전송해도 된다.
하지만 Binary representation 문제와 함께, `Atomics`를 이용하여 한 영역에서 상태 객체를 읽거나 쓰는 도중 다른 쪽에서 객체에 참조하는 것을 막아야 한다. 이는 성능에 영향이 클 수 있다.
데이터를 수동으로 직렬화/역직렬화하거나 SAB를 사용하는 대신에, Wasm의 스레딩을 사용할 수 있다. Wasm은 스레딩을 표준으로 지원하지만, 이는 SAB의 이용 가능 여부에 따라 사용이 제한된다.
Wasm의 스레딩을 이용한다면, 스레드가 구현된 프로그래밍 언어들에서 쓰던 패턴들을 동일하게 구현할 수 있게 된다. 물론 개발 복잡성, 오케스트레이션 필요성, 전송될 모듈들이 비대해지거나 모놀리식이 될 잠재성이 수반된다.
결론
기기가 아무리 느려도, `postMessage()`로 전송하는 객체가 최대 100KiB 정도이면 100ms 응답 budget을 충족할 수 있다.
만약 JS로 애니메이션을 구현했다면 10KiB 이하의 페이로드 정도는 문제가 없다. 이 정도면 웬만한 앱에서는 차고 넘칠 것이다.
`postMessage()`는 성능에 영향을 주긴 하지만, off-main-thread 아키텍처 구축의 이점을 무색하게 하는 정도는 아니다.
만약 페이로드가 이보다 큰 경우라면, 변경사항만을 전송하거나 바이너리 형식으로 전환할 수도 있겠다.
기초적이고 아키텍처적인 결정으로서 상태 레이아웃 · 송수신성 · 유연성을 고려하는 것은 보다 다양한 기기에서 앱이 원활히 실행될 수 있도록 해준다.
'공유 메모리 모델'이 가장 나아 보이는 사람들에게는, 가까운 미래에 웹어셈블리에서 이를 구현할 방법이 마련될 것이다.
이미 '액터 모델'을 주제로 하는 옛 블로그 글에서 미리 알렸듯이, 필자는 성능이 좋은 off-main-thread 아키텍처를 오늘날의 웹에서 구현할 수 있다고 강력하게 믿는다. 하지만 이는 우리가 안주하고 있는 스레드 언어의 영역과 웹의 '모든 것을 메인 스레드에서 실행하는' 특성에서 벗어나는 것이 필요하다. 웹과 JS의 제약을 받쳐줄 수 있는 대안적인 아키텍처와 모델을 탐구해야만 한다.
그러한 탐구는 값진 결과를 낳을 것이다.









