시리즈 3편: "Canvas가 빠르다"는 감이었을까, 사실이었을까
[1편]에서 "왜 Canvas를 선택했는지", [2편]에서 "D3+SVG와 코드 레벨에서 뭐가 달랐는지"를 다뤘다.
근데 스스로 한 가지가 계속 걸렸다.
우선, 대부분의 블로그 포스팅이 SVG기반을 옹호하고 있었다. 이유는 당연히 편의성 때문이었다. 순수 바닐라 자바스크립트로 만드는 캔버스 대신, SVG는 css를 적용하기 편하고, 애니메이션 사용도 편하고, 접근성 관리가 가능했기 때문이다.
다만, 추후 다른 프로젝트의 대시보드에서 여러 차트를 동시에 띄울 예정이라는 기획을 들었고, 이 라이브러리가 거기에도 활용될 수 있다는 얘기를 접했다. 그래서 내가 가장 신경 쓴 건 확장성이었다 — 인터랙션과 애니메이션을 모두 품으면서, 차트가 늘어나도 속도를 보장할 수 있는 구조가 뭔가?
"Canvas가 빠르다는 건 알겠는데, 실제로 얼마나 빠른 건데?"
DOM 65개 vs 1개, 코드 82줄 vs 45줄 — 2편에서 보여준 숫자들은 전부 빌드타임 지표였다. 런타임에서 실제로 FPS가 얼마나 차이 나는지, 클릭 반응 속도는 얼마나 다른지... 솔직히 안 재봤다.
그래서 시작된 비교기.
벤치마크를 브라우저 콘솔에서 돌리는 대신, 별도의 React 앱으로 만들었다. 이유는 두 가지다:
동일한 데이터, 동일한 시각적 결과 — 렌더링 방식만 다르게. Canvas/SVG/Recharts 버튼을 누르면 같은 차트가 다른 렌더러로 그려진다.

| 렌더러 | 설명 |
|---|---|
| Canvas | <canvas> + 2D Context API. 모든 도형을 ctx 메서드로 직접 그림 |
| SVG | React 컴포넌트로 <polygon>, <circle>, <text> 등 SVG 엘리먼트 생성 |
| Recharts | RadarChart 컴포넌트. 내부적으로 React Context 기반 상태 관리 + SVG DOM 생성 |
세 렌더러로 동일한 피부 분석 HexaChart(6~8축 레이더 차트)를 그리고, 차트 1개 / 5개 / 30개로 스케일 테스트했다.
requestAnimationFrame 기반 초당 프레임 수performance.mark() / performance.measure()querySelectorAll("*").lengthFPS 측정이 생각보다 까다로웠다. 처음에 setInterval(1000ms)로 프레임 수를 세는 단순한 방식을 썼는데, rAF 콜백이 밀릴 때 실제 FPS보다 높게 나오는 현상이 있었다. 브라우저가 rAF을 스킵하면 카운트 자체가 안 되니까 "측정 안 된 프레임 = 없었던 프레임"이 되어버린다.
결국 performance.now() 기반으로 프레임 간 시간 차이를 직접 측정하는 방식으로 바꿨고, 프레임 드롭은 16.67ms × 1.5 = 25ms 초과 여부로 판단했다. 완벽한 방법은 아니다 — 브라우저가 백그라운드 탭에서 rAF을 throttle하는 문제도 있고, 실제 화면 갱신(vsync)과 rAF 콜백 타이밍이 정확히 일치하지 않는 한계도 있다. 하지만 동일 조건에서 세 렌더러를 상대 비교하는 용도로는 충분했다.
💡 더 정확한 프레임 타임이 필요하면 Chrome DevTools의 Performance 탭에서 Frame 타임라인을 직접 보는 게 맞다. 이 벤치마크는 "세 방식의 상대적 차이"를 보려는 거지, 절대적인 FPS 수치를 주장하려는 게 아니다.
매 프레임마다 데이터 값을 ±5 범위로 교란(perturbation)하여 지속적인 리렌더를 유발했다. 피부 분석 차트의 전개 애니메이션, 항목 간 전환 등을 시뮬레이션한 것이다.
Chrome DevTools Performance 탭으로 교차 검증했다. Canvas는 Scripting 비율이 높고, SVG는 Rendering 비율이 높다. 이는 각 렌더러의 구조적 차이를 반영한다 — Canvas는 모든 드로잉을 JavaScript에서 처리하고 브라우저 레이아웃 엔진을 우회하는 반면, SVG는 매 프레임 DOM 속성 변경 → Style → Layout → Paint 파이프라인을 거친다.


| 렌더러 | 차트 수 | Avg FPS | Drop Rate | DOM Nodes | Initial Render |
|---|---|---|---|---|---|
| Canvas | 30 | 76 | 0.0% | 60 | 10.8ms |
| SVG | 30 | 76 | 0.0% | 1,320 | 6.3ms |
| Recharts | 30 | 35 | 97.3% | 1,830 | 25.9ms |

No Throttling에서는 Canvas와 SVG 모두 76fps로 동일하다. Recharts만 35fps로 떨어진다.
| 렌더러 | 차트 수 | Avg FPS | Drop Rate | DOM Nodes | Initial Render |
|---|---|---|---|---|---|
| Canvas | 5 | 76 | 0.0% | 10 | 1.2ms |
| Canvas | 30 | 76 | 0.2% | 60 | 6.7ms |
| SVG | 5 | 76 | 0.0% | 220 | 6.6ms |
| SVG | 30 | 36 | 83.6% | 1,320 | 26.6ms |
| Recharts | 5 | 42 | 17.4% | 305 | 20.6ms |
| Recharts | 30 | 11 | 84.1% | 1,830 | 108.6ms |
4x Throttling을 걸면 그때서야 Canvas와 SVG의 차이가 드러난다.

No Throttling에서 SVG 30개가 76fps를 찍었을 때 솔직히 당황했다. "Canvas가 빠르다"는 내 가설이 틀린 건가 싶었다.
근데 4x Throttling을 걸자 이야기가 달라졌다. Canvas는 여전히 76fps인데, SVG는 36fps로 떨어졌다.
이게 뭘 의미하냐면 — 현대 맥북의 CPU는 충분히 빨라서 Canvas든 SVG든 16.67ms 프레임 예산 안에 다 끝낸다. 하지만 실제로 하는 "일의 양"은 다르다. 그 차이가 CPU 여유분이 줄어들면 바로 드러나는 거다.
Recharts는 조건을 안 가린다. No Throttling에서도 30개를 버티지 못한다 — 35fps에 프레임 드롭 97%. 사실상 슬라이드쇼다.
구조적 차이다:
clearRect() → 다시 그리기. 픽셀 버퍼만 갱신하므로 브라우저 레이아웃 엔진이 개입하지 않는다.<polygon points="..."> 속성을 업데이트한다. 브라우저는 이걸 받아서 Style → Layout → Paint 파이프라인을 실행한다. 차트 30개면 DOM 변경이 수천 건이다.DOM 노드 수가 이를 명확히 보여준다. Canvas 30개는 <canvas> 요소 60개뿐이지만, SVG 30개는 1,320개, Recharts 30개는 1,830개의 DOM 노드를 만든다.
사실 이게 Canvas를 선택한 더 본질적인 이유였다. 피부 분석 차트에서 사용자가 특정 꼭짓점을 클릭하면 해당 항목이 하이라이트되는 인터랙션이 필요하다. Canvas와 SVG는 이 과정이 근본적으로 다르다.
Canvas는 "어떤 도형을 클릭했는지" 브라우저가 알려주지 않는다. 직접 좌표 거리를 계산해야 한다:
// Canvas: O(n) 거리 계산 루프
const handleClick = (e: MouseEvent) => {
const x = e.offsetX, y = e.offsetY;
for (let i = 0; i < positions.length; i++) {
const dx = x - positions[i].x;
const dy = y - positions[i].y;
if (Math.sqrt(dx * dx + dy * dy) < 30) {
onVertexClick(i);
return;
}
}
};
SVG는 각 <circle>에 onClick을 달면 끝이다. 브라우저가 알아서 어떤 요소를 클릭했는지 판별해준다:
<circle cx={point.x} cy={point.y} r={4}
onClick={() => onVertexClick(i)}
/>
코드만 보면 SVG가 압도적으로 편하다. 근데 편한 것과 빠른 것은 다른 문제다.
자동화된 50회 클릭으로 클릭 → 하이라이트가 화면에 반영되는 시점까지의 레이턴시를 측정했다. 화면 반영 확인은 requestAnimationFrame 두 번(double-rAF)을 사용하여 페인트 완료 시점을 포착했다.
Recharts는 per-vertex 클릭/하이라이트를 네이티브로 지원하지 않아서 비교 대상에서 제외했다. (영역 레벨 인터랙션만 지원)
| 렌더러 | 차트 수 | Avg Latency | P95 |
|---|---|---|---|
| Canvas | 1 | 5.71ms | 13.7ms |
| SVG | 1 | 7.94ms | 13.7ms |
| Canvas | 30 | 7.1ms | 13.8ms |
| SVG | 30 | 14.66ms | 22.1ms |

| 렌더러 | 차트 수 | Avg Latency | P95 |
|---|---|---|---|
| Canvas | 1 | 6.08ms | 7.2ms |
| SVG | 1 | 6.99ms | 12.8ms |
| Canvas | 30 | 8.8ms | 13.3ms |
| SVG | 30 | 27.51ms | 43.4ms |

애니메이션과 다른 패턴이 나왔다. 인터랙션은 No Throttling에서도 차이가 난다.
No Throttling, 30개 기준: Canvas 7.1ms vs SVG 14.66ms. 2배 차이. 4x Throttling에서는 Canvas 8.8ms vs SVG 27.51ms. 3.1배 차이. P95(상위 5% 최악 케이스)를 보면 더 벌어진다 — Canvas 13.3ms vs SVG 43.4ms.
애니메이션에서는 No Throttling이면 Canvas와 SVG가 둘 다 76fps로 같았는데, 인터랙션에서는 일반 환경에서도 Canvas가 확실히 빠르다.
역설적이었던 건, Canvas의 "단점"이라고 생각했던 수동 hit detection이 오히려 이점이었다는 것이다. Math.sqrt(dx*dx + dy*dy) 같은 단순 수학 연산은 n이 수십 개 수준에서는 무시할 수 있는 비용이다. 반면 SVG의 네이티브 이벤트 라우팅은 편리하지만, 그 뒤에 따라오는 DOM 업데이트 + 리플로우 비용이 병목이었다.
여기서 한 가지 짚고 넘어가야 할 게 있다.
애니메이션에서는 No Throttling 기준 Canvas 30개와 SVG 30개 모두 76fps로 동일하다. 차이가 전혀 없다.
"그러면 4x Throttling 결과가 의미 있나?"
실사용 의원 환경은 맥 기반 내부 상담 시스템이다. 일반 소비자용 앱이 아니라 상담실 내부에서만 쓴다. 솔직히 현재 환경에서는 Canvas와 SVG의 체감 차이가 거의 없다.
그럼에도 4x Throttling을 돌린 이유는 "저사양 시뮬레이션"이 아니라 "구조적 차이 가시화"를 위해서다.
일반 환경에서 브라우저는 충분히 빨라서 Canvas든 SVG든 모든 작업을 16.67ms 프레임 안에 끝낸다. 하지만 CPU를 4배 느리게 하면 각 렌더러가 실제로 얼마나 많은 작업을 하는지가 드러난다:
이것은 아키텍처 차이를 보여주는 것이지, 특정 환경에서의 실사용 성능을 예측하는 것이 아니다.
다만 인터랙션은 좀 다르다. No Throttling에서도 Canvas 7.1ms vs SVG 14.66ms로 2배 차이가 났다. 클릭 → DOM 업데이트 → 리플로우가 매번 발생하는 SVG 특성상, CPU 여유와 무관하게 구조적 오버헤드가 존재한다는 뜻이다.
솔직히 말하면, 현재 환경에서는 SVG로 해도 됐다.
차트 3~5개를 맥에서 돌리는 수준에서는 Canvas와 SVG의 체감 차이가 없다. No Throttling 결과가 그걸 증명했다.
그런데도 Canvas를 선택한 건 세 가지 이유가 있었다:
1. 애니메이션 + 인터랙션을 모두 직접 제어하고 싶었다
피부 분석 차트는 단순히 데이터를 보여주는 게 아니라, 전개 애니메이션과 꼭짓점 클릭 하이라이트가 핵심 UX다. Canvas는 두 가지를 모두 같은 렌더링 파이프라인(픽셀 버퍼)에서 처리하기 때문에, 애니메이션 중에 인터랙션이 발생해도 구조적으로 충돌이 없다.
2. 대시보드 확장을 대비했다
지금은 차트 3~5개지만, 환자 종합 대시보드처럼 여러 차트를 동시에 보여줘야 하는 화면이 예정되어 있다. 30개 실험은 과장이긴 하지만, "차트가 늘어날 때 어떤 구조가 버티는가"를 확인하고 싶었다. SVG는 차트 수에 비례해서 DOM 노드가 선형 증가하고, Canvas는 그렇지 않다.
3. AI 사용으로 진입장벽을 넘을 수 있었다
Canvas의 가장 큰 단점은 바닐라 자바스크립트로 모든 걸 직접 그려야 한다는 것이다. 좌표 계산, 이징 함수, hit detection, 그라데이션 — 혼자서 했으면 SVG를 골랐을 것이다.
하지만 이 프로젝트는 Claude을 적극 활용하며 진행했다. 수학적으로 복잡한 부분을 AI와 함께 설계하고 구현할 수 있었기 때문에, Canvas의 저수준 API가 오히려 자유도 높은 도구가 되었다. "AI가 있으니까 Canvas도 괜찮다"가 아니라, "AI와 함께하면 Canvas의 자유도를 활용할 수 있다"는 판단이었다.
| 편 | 질문 | 답 |
|---|---|---|
| 1편 | 왜 Canvas? | 기존 라이브러리로 안 되는 게 있었다 |
| 2편 | 코드가 달라? | 같은 그림, 다른 구조. DOM 65개 vs 1개 |
| 3편 | 진짜 빨라? | 일반 환경에선 같다. 부하가 걸리면 구조적 차이가 드러난다 |
"지금 괜찮으니까 괜찮다"가 아니라 "나중에도 괜찮은가"를 미리 확인해두는 게 기술 선택이라고 생각한다.
부족한 건 아직 많다. 접근성 처리도 안 했고, 레티나 디스플레이 대응도 남았다. 벤치마크 측정 방법 자체도 rAF 기반의 한계가 있다.
그래도 "왜 Canvas인데?"라는 질문에는 이제 숫자로 답할 수 있는 실험이 되었다..!