성능
2-캔버스 Canvas2D 파이프라인은 바뀐 부분만 다시 그리며, 프레임마다 하는 모든 작업은
불러온 과거 데이터의 양이 아니라 화면에 보이는 것만큼으로 제한됩니다. 아래 수치는 pnpm bench(단일 코어)와 실제 위젯 프로파일링에서 얻은 것이며, Feature Lab에서는 클릭할 때마다 실제 전환 시간을 측정합니다.
프레임: 500봉에서 100,000봉까지 일정
- 두 개의 캔버스 — 장면 캔버스(격자, 시리즈, 지표, 차트 개체, 축)와 십자선, 범례 등 포인터를 따라가는 요소를 위한 얇은 상단 캔버스로 나뉩니다. 마우스를 올리면 상단 캔버스만 다시 그리며(~0.2 ms), 브라우저는 네 개가 아니라 두 개의 표면만 합성합니다.
- 보이는 범위만 렌더링 — 모든 렌더러, 자동 눈금, 지표 가격 범위 계산이 화면에 보이는 봉만 순회합니다.
- 프레임별 가비지 없음 — 뷰포트 스냅샷은 변경이 있을 때까지 캐시되며, 숫자와 날짜 포매터는 라벨마다 새로 만들지 않고 재사용합니다.
- 자동 크기 가격 축 — 가장 긴 라벨에 축 너비를 맞추는 비용은 ~0.05 ms이며, 너비가 실제로 바뀔 때만 레이아웃을 다시 계산합니다.
실시간 틱: 증분 지표 계산
틱은 형성 중인 봉만 바꾸므로, update()를 구현한 지표는 그 봉만 다시
계산합니다(SMA, EMA, WMA, VWMA, Bollinger, Envelope, RSI, MACD, ATR, OBV, Stochastic). 나머지는
전체 재계산을 사용합니다. BB + EMA + RSI + MACD 기준:
| 과거 데이터 | 전체 재계산 | 증분 update() |
|---|---|---|
| 20,000봉 | ~5 ms | ~0.0005 ms |
| 100,000봉 | ~27 ms | ~0.001 ms |
사용자 지정 지표도 이 방식을 사용할 수 있습니다 — 플러그인 → 빠른 실시간 업데이트를 참고하세요.
종목 또는 시간 단위 전환
- 가벼운 전체 로드 — 지표 값은
IndicatorValueMap(배열 기반으로, 타임스탬프를 키로 쓰는Map보다 만드는 비용이 ~3배 저렴)에 저장되며,setData는 이미 유효한 봉을 복사하지 않고 재사용합니다. - 느릴 때만 로딩 표시 — 이전 차트가 그대로 유지되고, 전환이 200 ms를 넘길 때만 가림막이 나타나므로 일반적인 ~130 ms 네트워크 전환에서는 화면이 깜박이지 않습니다.
- 오래된 데이터 없음 — 대체된 과거 데이터 요청은 버리고 이전 소켓은 분리하므로, 항상 마지막 클릭이 반영됩니다.
- 로컬 리샘플링 — 정적 데이터는 다시 가져오지 않고 시간 단위를 전환하며, 느린 리샘플링은 메인 스레드가 바빠지기 전에 가림막부터 그립니다.
LTTB 다운샘플링
라인 및 영역 차트는 픽셀보다 봉이 훨씬 많을 때 Largest-Triangle-Three-Buckets로 보이는 범위를 픽셀당 ~2개의 점으로 다운샘플링합니다. 그리는 점은 수십 배 줄어도 선은 시각적으로 똑같이 유지됩니다. 일반적인 확대 수준에서는 아무 일도 하지 않습니다. 알고리즘은 직접 사용할 수 있도록 export되어 있습니다.
import { lttbDownsample } from '@tradecanvas/chart'
// indices preserving the shape of a 100k series, reduced to 1600 points
const idx = lttbDownsample(series.length, 1600, (i) => series[i].close) | 보이는 점 수 → 1600 | 프레임당 시간 | 처리량 |
|---|---|---|
| 10,000 | ~0.025 ms | 39,600 / s |
| 100,000 | ~0.32 ms | 3,100 / s |
| 1,000,000 | ~2.6 ms | 380 / s |
축소했을 때의 지표
봉 하나가 1픽셀보다 좁아지면 지표의 선, 밴드, 히스토그램은 픽셀 열마다 하나의 구간으로 그려집니다. 열의 최저점부터 최고점까지, 앞 열과 이어지고, 폭은 선 두께와 같습니다. 수천 개의 점을 지나는 선과 거의 같아 보이면서도 래스터 작업은 훨씬 적습니다. 200,000개 봉을 축소해 볼린저 밴드, EMA, RSI, MACD를 함께 띄우면 내장 GPU에서 한 프레임이 약 54 ms에서 약 21 ms로 줄었습니다. node scripts/bench-render.mjs로 내 컴퓨터에서도 측정할 수 있습니다.
WebGL 렌더러 (미리 보기)
renderer: 'webgl'을 쓰면 플롯 영역과 지표 패널을 WebGL 2로 2D 장면 아래의 캔버스에 그립니다. 그리드, 세션, 캔들, 거래량은 직접 그리고, 지표·비교선·대부분의 차트 유형의 Canvas 2D 그리기는 GPU의 선, 채우기, 사각형으로 기록되어 Canvas 2D처럼 가장자리가 안티앨리어싱됩니다. 텍스트, 드로잉, 주문, 축, 크로스헤어는 Canvas 2D로 남고, GPU가 똑같이 그릴 수 없는 것도 Canvas 2D가 그립니다(순서대로 그리므로 겹침 순서는 그대로입니다). 직접 만든 지표 플러그인도 수정 없이 동작합니다. 캔들은 Canvas 2D와 2/255 이내로 일치하고, 선과 채우기는 안티앨리어싱된 가장자리의 일부 픽셀만 다릅니다. WebGL 코드는 별도 청크(gzip 약 17 KB)로, 처음 쓸 때 불러옵니다. WebGL 2가 없거나 컨텍스트를 잃으면 차트는 Canvas 2D로 계속 그립니다.
const chart = new Chart(el, { renderer: 'webgl' })
chart.on('rendererChange', (e) => console.log(e.payload))
// { renderer: 'webgl' }, or { renderer: 'canvas', reason: 'unsupported' | 'contextLost' }
await chart.setRenderer('canvas') // resolves to what draws now 이동 중 프레임 시간, 내장 GPU(Intel UHD, 16.7 ms가 60 fps):
| 차트 | 픽셀 비율 | Canvas 2D | WebGL |
|---|---|---|---|
| 1600×900, 캔들 500개 + 지표 4개 | 2 | 27.4 ms | 19.6 ms |
| 1600×900, 봉 200,000개를 축소 + 지표 4개 | 2 | 34.5 ms | 20.2 ms |
| 차트 6개, 각각 캔들 500개와 지표 2개 | 2 | 23.5 ms | 17.2 ms |
| 2560×1400, 캔들 2,000개 + 지표 4개 | 1 | 41.6 ms | 17.7 ms |
| 2560×1400, 캔들 2,000개 + 지표 4개 | 1.5 | 70.8 ms | 17.6 ms |
| 2560×1400, 캔들 2,000개 + 지표 4개 | 2 | 114.5 ms | 29.1 ms |
| 2560×1400, 캔들 2,000개 | 2 | 33.1 ms | 20.9 ms |
위 WebGL 프레임 대부분은 16.7 ms이며, 평균에는 몇몇 긴 프레임이 포함됩니다. 픽셀 비율 2의 2560×1400 차트에서는 이 GPU에서 브라우저가 전체 크기 레이어를 합성하는 데만 약 23 ms가 걸립니다. node scripts/bench-render.mjs --renderer=webgl로 내 컴퓨터에서 측정할 수 있습니다.
메인 스레드 밖에서 계산
IndicatorWorkerHost는 Promise 기반 calculate(), 요청별 타임아웃,
SSR과 테스트를 위한 동기식 대체 경로를 갖추고 Web Worker에서 지표 계산을 실행합니다.