노트

Web Worker

Web Workers

프론트엔드#browser#performance · 연결된 개념 8개

쉽게 말하면

Web Worker는 혼자 주문을 받는 바리스타가 오래 걸리는 원두 갈기를 뒷방 직원에게 맡기는 거예요. 그동안 바리스타는 계속 손님을 받으니 화면이 멈추지 않아요.

비유의 한계 뒷방 직원은 홀에 나올 수 없어요. Worker는 DOM을 못 만지고 결과를 메시지로만 돌려줘요. 원두를 가는 시간도 그대로라 계산 자체가 빨라지지는 않아요.

Web Worker는 페이지의 메인 스레드와 별개인 백그라운드 스레드에서 자바스크립트를 돌리는 브라우저 기능이다. 클릭에 반응하는 스크립트에 끼어들지 않고 긴 계산을 돌릴 수 있어서, 무거운 일을 하는 동안에도 페이지가 응답한다.

메인 스레드는 이벤트 루프 하나로 입력·렌더링·스크립트를 번갈아 처리한다. 긴 계산은 비동기로 바꿔도 같은 스레드에서 돌아서 루프를 붙잡는다(동기·비동기와 블로킹·논블로킹). 마이크로태스크로 잘게 나눠도 양보가 되지 않는다. 계산 자체가 무거우면 Worker로 옮긴다.

기본 사용법

new Worker(url, options)로 만들고, 양쪽이 postMessage()로 보내고 onmessage로 받는다. options.type을 "module"로 주면 Worker 스크립트를 ES 모듈로 실행해 import를 쓸 수 있다(기본값은 "classic").

// main.ts
const worker = new Worker("/sum.worker.js", { type: "module" });
worker.onmessage = (event: MessageEvent<number>) => {
  output.textContent = String(event.data); // 화면은 메인 스레드가 바꾼다
};
 
const data = new Float64Array(10_000_000).map(Math.random);
// postMessage(message, transfer): transfer에 넣은 버퍼는 복사 없이 소유권이 넘어간다
worker.postMessage(data, [data.buffer]);
console.log(data.byteLength); // 0, 보낸 쪽 버퍼는 분리(detached)되었다
 
// sum.worker.ts (빌드되어 /sum.worker.js로 제공된다고 가정)
self.onmessage = (event: MessageEvent<Float64Array>) => {
  let sum = 0;
  for (const x of event.data) sum += x;
  self.postMessage(sum);
};
  • Worker 안에서는 DOM을 직접 건드릴 수 없고 window의 일부 메서드·속성도 쓸 수 없다. 결과를 메시지로 돌려보내고 메인 스레드가 화면을 바꾼다
  • fetch()나 XMLHttpRequest로 네트워크 요청은 할 수 있다
  • 메인 스레드에서 worker.terminate()를 부르면 Worker 스레드가 바로 종료된다

복사와 소유권 이전

  • postMessage로 보낸 데이터는 공유되지 않고 복사된다. 브라우저는 구조화된 복제(structured clone) 알고리즘으로 보낸 쪽에서 직렬화하고 받는 쪽에서 되살린다. 순환 참조처럼 JSON이 못 담는 값도 담는다
  • 함수와 DOM 노드는 복제할 수 없어 DataCloneError가 난다. 프로토타입 체인과 getter·setter, 클래스 private 필드도 복제되지 않는다. 클래스 인스턴스는 메서드 없는 평범한 객체로 도착한다(깊은 복사와 비슷하지만 더 좁다)
  • 큰 데이터는 전송 가능 객체(transferable object)로 넘긴다. ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas 등이 있다. 자원이 한 번에 한 쪽에만 있도록 소유권을 옮기는 방식이라 복사 비용이 없다. 대신 보낸 쪽 객체는 더 쓸 수 없고 읽거나 쓰면 예외가 난다
  • Uint8Array 같은 형식화 배열은 그 자체가 아니라 밑의 .buffer를 transfer 목록에 넣는다

종류

종류누가 쓰나특징
Dedicated Worker (Worker)만든 스크립트 하나가장 흔한 형태
Shared Worker (SharedWorker)출처(origin)가 같은 여러 창·iframeport 객체로 통신한다
Service Worker등록 범위(scope) 안의 페이지앱과 네트워크 사이의 프록시

Service Worker도 Worker 문맥에서 돌아 DOM이 없지만 목적이 다르다. 계산을 옮기는 용도가 아니라 네트워크 요청을 가로채 오프라인 경험이나 푸시 알림을 만드는 별개 API다. HTTPS 같은 보안 문맥에서만 쓸 수 있다. API 모킹 도구인 MSW로 API 모킹가 이 기능을 쓴다.

언제 쓰나

  • 이미지·영상 처리, 큰 JSON 파싱, 압축·해시처럼 CPU 바운드 계산이 메인 스레드를 오래 붙잡아 INP가 나빠질 때
  • Worker 하나로 옮긴 계산은 같은 시간이 걸린다. 얻는 것은 메인 스레드의 응답성이다. 전체 시간을 줄이려면 작업을 쪼개 여러 Worker에 나눈다
  • 명세는 Worker가 비교적 무거워서 많이 띄우는 용도가 아니라고 말한다. 시작 비용과 인스턴스당 메모리 비용이 크다는 것이다. 픽셀마다 Worker를 하나씩 만드는 식은 맞지 않고, 오래 살려 두고 재사용하는 쪽이 낫다
  • 그래서 금방 끝나는 계산을 매번 새 Worker로 보내면 시작·복사 비용이 이득보다 클 수 있다. fetch처럼 기다리는 일은 옮겨도 빨라지지 않는다

명세: HTML Standard — Web workers · Dedicated workers and the Worker interface · Shared workers and the SharedWorker interface

출처: MDN — Using Web Workers · MDN — Worker() constructor · MDN — Transferable objects · MDN — The structured clone algorithm · MDN — Service Worker API · MDN — SharedWorker

연결된 개념

이 노트를 가리키는 문서

뜻이 가까운 노트

  • WebGPU

    웹페이지에서 GPU로 그래픽과 범용 계산을 하는 브라우저 API. WebGL의 후속으로, 브라우저 안에서 ML 모델을 돌리는 기반이 된다.

  • 웹뷰 브리지(웹↔네이티브 통신)

    앱 안의 웹뷰에서 웹과 네이티브가 서로 말을 거는 길. User-Agent 표식, 메시지 핸들러, 스크립트 실행, URL(딥링크)이 있다.

  • localStorage와 sessionStorage

    브라우저에 문자열 키-값을 저장하는 Web Storage API의 두 가지. 쿠키와 달리 요청에 자동으로 실리지 않고, 출처 단위로 나뉘며(same-origin-policy), 보통 출처당 5MB 안팎을 쓸 수 있다. 동기 API라 큰 데이터를 자주 읽고 쓰면 메인 스레드를 막는다.

  • 브라우저 캐시

    한 번 받은 리소스(HTML·CSS·JS·이미지)를 브라우저가 저장해 두었다가 다시 쓰는 기능. 서버가 응답 헤더로 얼마나, 어떻게 캐시할지 알려 주고 브라우저가 그대로 따른다.

  • 서버 전송 이벤트(SSE)

    서버 전송 이벤트(Server-Sent Events, SSE)는 HTTP(HyperText Transfer Protocol) 응답 하나를 닫지 않고 열어 둔 채, 서버가 원할 때마다 텍스트 이벤트를 흘려보내는 서버 → 클라이언트 단방향 푸시 방식이다. HTML 표준에 정의돼 있고 브라우저는 EventSource로 받는다.

보기 옵션