왜 프런트에서 프레임이 끊길까
대시보드 차트가 많아지거나 입자 시뮬레이션처럼 계산량이 커지면 메인 스레드가 막혀 스크롤과 줌이 함께 느려집니다. 사용자 입장에서는 60FPS가 20~30FPS로 떨어지는 순간 체감 품질이 급격히 나빠집니다.
이 글은 브라우저에서 고FPS를 유지하기 위한 첫걸음을 제공합니다. OffscreenCanvas로 렌더링을 메인 UI와 분리하고, Web Workers로 계산을 백그라운드로 보내며, WebAssembly SIMD로 수치 연산을 빠르게 처리하는 접근을 다룹니다.
목표는 어려운 최적화 이론이 아니라, “왜 필요한지 → 어디에 쓰는지 → 시작 방법”을 순서대로 이해하는 것입니다. 실무 입문자도 바로 적용할 수 있는 최소 설정과 예시 흐름을 제시합니다.
> 트레이드오프: 렌더링이 분리되면 디버깅과 데이터 전달 구조가 약간 복잡해집니다. 대신 메인 스레드의 입력 반응성과 애니메이션 안정성이 개선됩니다.
세 가지 조합을 한눈에: 어디에, 어떻게 쓰나
차트의 선 1만 개를 그리거나 파티클 5만 개를 돌릴 때, 메인 스레드 하나에 그리기와 계산을 몰면 금세 밀립니다. OffscreenCanvas는 그리기를 워커로 넘겨 UI와 분리해, 스크롤·줌 같은 입력이 끊기지 않도록 합니다.
Web Workers는 무거운 연산을 백그라운드 스레드에서 처리합니다. 예를 들어 “틱마다 입자 위치 계산”을 워커가 맡고, 결과만 캔버스로 전달하는 흐름입니다.
WebAssembly SIMD는 반복 수치 계산을 벡터 단위로 병렬 처리합니다. 동일한 루프라도 좌표 변환, 필터, 누적합처럼 수학 연산이 많은 구간에서 프레임 낙폭을 줄이는 데 유리합니다.
적용 흐름은 UI 입력 -> 워커에 파라미터 전송 -> 워커에서 계산(WASM SIMD) -> OffscreenCanvas로 그리기 -> 비트맵을 화면에 반영 순서입니다. 한 번 구조를 잡아두면 데이터 크기나 시리즈 수가 늘어도 확장하기 수월합니다.
> 안티패턴: 매 프레임에 대용량 객체를 JSON으로 보내면 복사 비용이 커집니다. Float32Array를 사용하고 가능한 경우 Transferable로 소유권을 이전하세요.
워커로 캔버스 렌더링 빠르게 분리하기
메인 스레드는 UI만 맡기고, 그리기·계산은 워커로 분리하면 초기 렉을 줄일 수 있습니다. 핵심은 OffscreenCanvas를 워커에 넘기고, 큰 데이터는 복사 대신 소유권 이전(transfer)으로 전달하는 것입니다.
- 메인: <canvas> 준비 → transferControlToOffscreen() → 워커로 캔버스·설정 전송
- 워커: 메시지 수신 → OffscreenCanvas에서 2D/WebGL 컨텍스트 생성 → 렌더 루프 시작
- WASM: SIMD로 빌드 → 워커에서 instantiateStreaming() → 계산 함수 준비
- 데이터: Float32Array 등 전송 가능 버퍼 → postMessage 두 번째 인자에 transfer 지정
- 루프: 입력 수신 → WASM으로 좌표 계산 → 캔vas draw → 다음 프레임 예약
먼저 최소 연결만 확인하세요. 메인에서 캔버스를 넘기고, 워커가 한 프레임이라도 그리면 성공입니다.
그다음 WASM SIMD를 붙이면 디버깅이 단순해집니다.
<!-- index.html -->
<canvas id="view" width="800" height="400"></canvas>
<script type="module">
const canvas = document.getElementById('view');
const off = canvas.transferControlToOffscreen();
const worker = new Worker(new URL('./render.worker.js', import.meta.url), { type: 'module' });
worker.postMessage({ canvas: off, width: canvas.width, height: canvas.height }, [off]);
// UI 이벤트는 가볍게 전달
window.addEventListener('mousemove', (e) => {
worker.postMessage({ mouseX: e.clientX });
});
</script>
// render.worker.js
let ctx;
const state = { t: 0, mouseX: 0 };
self.onmessage = (e) => { const { canvas, width, height, mouseX } = e.data; if (canvas) { ctx = canvas.getContext('2d'); ctx.canvas.width = width; ctx.canvas.height = height; tick(); }
else if (mouseX != null) { state.mouseX = mouseX; } };
function tick() { state.t += 1 / 60; ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.fillStyle = '#09f'; ctx.fillRect((state.mouseX % ctx.canvas.width) - 5, 180, 10,
10); self.requestAnimationFrame(tick); } ```
연결이 되면 워커 안에서 WebAssembly 모듈을 로드해 계산 루프만 교체하세요. 렌더 루프와 컨텍스트는 그대로 두고, 결과 배열만 바꾸면 통합이 쉽습니다.
> 팁: 큰 배열을 복사하면 프레임이 떨어집니다. ArrayBuffer를 transfer로 넘기고, 매 프레임 생성 대신 버퍼를 재사용하세요.
## 워커·WASM·OffscreenCanvas 빠른 체크
세 기술을 함께 쓰면 연결 지점에서 문제가 납니다. **가장 흔한 원인은 큰 배열 복사와 워커에서 DOM을 만지려는 시도입니다.**
- 매 프레임 postMessage 복사 금지 → ArrayBuffer 소유권 이전(transfer) 사용
- 워커는 document/window 없음 → 입력은 메인에서 처리해 최소 값만 전달
- OffscreenCanvas 컨텍스트는 2D·WebGL 중 하나로 고정, 재생성 반복 금지
- WASM SIMD 지원은 브라우저·CPU마다 다름 → 런타임 체크와 폴백 준비
- 타이밍 차이 주의: 메인은 rAF, 워커는 setInterval → 워커 루프는 dt로 보정
선택은 데이터 성격으로 나눕니다. 반복 계산이 크면 WASM, 드로우 호출이 많으면 OffscreenCanvas가 먼저입니다.
- 큰 데이터·무거운 연산: Workers + WASM SIMD → 계산 분리
- 드로우 병목: OffscreenCanvas 배치 렌더링 → 입력 반응성 유지
- 가벼운 차트·빈번한 상호작용: 메인 캔버스 + 경량 워커 전처리
> 안티패턴: 매 프레임 전체 데이터 전송. 변경분만 보내거나 워커 내부 버퍼를 재사용하세요.
구조는 단순하게 시작하세요. 메인(UI) -> 워커(계산) -> OffscreenCanvas(그리기)로 두고, 로깅·에러 처리는 단계별로 분리하면 추적이 쉽습니다.
## 다음 행동: 최소 구동 → 병목 측정 → 점진 최적화
먼저 핵심은 구조를 단순하게 고정하는 것입니다. **메인은 입력·UI, 워커는 계산·그리기**로 나누고 한 프레임이라도 돌아가게 만드세요.
바로 실행할 체크리스트입니다. 한 번에 모두 넣지 말고, 위에서 아래로 단계별로 적용하세요.
- 캔버스 전송: transferControlToOffscreen() → 워커로 OffscreenCanvas 전달
- 루프 분리: 워커에서 컨텍스트 생성(2D 또는 WebGL) → 렌더 루프 시작
- 데이터 경량화: Float32Array 등 ArrayBuffer를 transfer로 이동
- 계산 가속: WASM SIMD 런타임 체크 → 가능하면 해당 경로로 호출
- 측정: 프레임 시간, 포스트메시지 횟수, 드로우 호출 수를 로그로 기록
성공 기준은 간단합니다. 1) 스크롤·줌 입력이 끊기지 않는지, 2) 60FPS 근처에서 프레임 시간이 안정적인지, 3) 데이터 크기를 늘려도 메인 스레드 점유가 급증하지 않는지 확인하세요.
> 안티패턴: 매 프레임 전체 데이터셋을 postMessage로 복사하지 않습니다. 변경된 범위만 보내거나 버퍼를 재사용해 네트워크가 아닌 메모리 복사 비용을 줄이세요.'프로그래밍' 카테고리의 다른 글
| Playwright + browser-use로 웹 태스크 LLM 자동화: 셀프힐링 로케이터와 검증 루프 패턴 시작 가이드 (0) | 2026.07.14 |
|---|---|
| CSS Anchor Positioning + Popover로 셀렉트·드롭다운·툴팁 무자바스크립트 구현 가이드 (0) | 2026.07.10 |
| Speculation Rules API 실전 가이드: prerender/prefetch 전략과 안전 가드 정리 (0) | 2026.07.10 |
| bfcache 최적화 체크리스트: unload 제거, pagehide/pageshow 전환, DevTools 검사 방법 (0) | 2026.06.29 |
| Tauri 2 자동 업데이트 채널 설계: 서명·릴리스 파이프라인·롤백 전략 가이드 (0) | 2026.06.26 |