성능
2-캔버스 Canvas2D 파이프라인은 바뀐 부분만 다시 그리며, 프레임마다 하는 모든 작업은
불러온 과거 데이터의 양이 아니라 화면에 보이는 것만큼으로 제한됩니다. 아래 수치는 pnpm bench(단일 코어)와 실제 위젯 프로파일링에서 얻은 것이며, Feature Lab에서는 클릭할 때마다 실제 전환 시간을 측정합니다.
● 데모유동성 히트맵브라우저에서 생성한 시뮬레이션 시장 데이터프레임: 500봉에서 100,000봉까지 일정
- 두 개의 캔버스 — 장면 캔버스(격자, 시리즈, 지표, 차트 개체, 축)와 십자선, 범례 등 포인터를 따라가는 요소를 위한 얇은 상단 캔버스로 나뉩니다. 마우스를 올리면 상단 캔버스만 다시 그리며(~0.2 ms), 브라우저는 네 개가 아니라 두 개의 표면만 합성합니다.
- 보이는 범위만 렌더링 — 모든 렌더러, 자동 눈금, 지표 가격 범위 계산이 화면에 보이는 봉만 순회합니다.
- 프레임별 가비지 없음 — 뷰포트 스냅샷은 변경이 있을 때까지 캐시되며, 숫자와 날짜 포매터는 라벨마다 새로 만들지 않고 재사용합니다.
- 자동 크기 가격 축 — 가장 긴 라벨에 축 너비를 맞추는 비용은 ~0.05 ms이며, 너비가 실제로 바뀔 때만 레이아웃을 다시 계산합니다.
실시간 틱: 증분 지표 계산
틱은 형성 중인 봉만 바꾸므로, 자체 update()를 가진 지표는 그 봉만 다시 계산합니다: ATR, Bollinger Bands, Chande Kroll Stop, DEMA, EMA, Envelope, KAMA, MACD, MA Cross, McGinley Dynamic, OBV, RSI, SMA, SMMA, Stochastic, Ulcer Index, Volume Oscillator, VWMA, WMA. 이 밖에 31개가 PointwiseIndicator 위에 만들어져 같은 방식으로 마지막 봉부터 이어서 계산하고, 다른 지표의 선을 읽는 지표도 이어서 계산합니다. 기억하는 봉이 더 이상 맞지 않으면 전체를 다시 계산합니다. 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처럼 가장자리가 안티앨리어싱됩니다. 호가 깊이 히트맵, 볼륨 프로파일, 마켓 프로파일도 같은 방식으로 기록되어 캔들 아래에 그려집니다. 통계 상자나 TPO 문자를 표시하는 마켓 프로파일은 Canvas 2D로 남습니다. 텍스트, 드로잉, 주문, 축, 크로스헤어는 Canvas 2D로 남고, GPU가 똑같이 그릴 수 없는 것도 Canvas 2D가 그립니다(순서대로 그리므로 겹침 순서는 그대로입니다). 직접 만든 지표 플러그인도 수정 없이 동작합니다. 캔들은 Canvas 2D와 똑같은 디바이스 픽셀에 그려지고, 선과 채우기는 안티앨리어싱된 가장자리의 일부 픽셀만 다릅니다. WebGL 코드는 별도 청크(gzip 약 17 KB)로, 처음 쓸 때 불러옵니다. WebGL 2가 없거나 컨텍스트를 잃으면 차트는 Canvas 2D로 계속 그립니다. 컨텍스트가 돌아오면 WebGL로도 돌아옵니다. Chrome과 Safari는 페이지당 WebGL 컨텍스트를 16개 정도만 유지하고 넘치면 가장 오래된 것을 버리므로, 한 페이지에서 동시에 WebGL로 그리는 차트는 최대 8개입니다(setMaxWebGLCharts). 한도를 넘은 차트는 컨텍스트가 반환될 때까지 Canvas 2D로 그립니다. Chrome, Firefox, WebKit에서 확인했습니다.
const chart = new Chart(el, { renderer: 'webgl' })
chart.on('rendererChange', (e) => console.log(e.payload))
// { renderer: 'webgl', reason?: 'contextRestored' }, or { renderer: 'canvas', reason: 'unsupported' | 'contextLost' | 'limit' }
await chart.setRenderer('canvas') // resolves to what draws now
setMaxWebGLCharts(4) // from '@tradecanvas/chart': at most 4 charts on the page draw with WebGL (8 by default) 이동 중 프레임 시간, 내장 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 |
| 1600×900, 봉 1,000,000개를 축소 + 지표 4개 | 2 | 34.5 ms | 16.7 ms |
| 1600×900, 호가 깊이 히트맵, 스냅샷 240개 × 호가 80단계 | 2 | 25.5 ms | 16.8 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로 내 컴퓨터에서 측정할 수 있습니다.
봉은 CSS 픽셀의 4분의 1보다 좁아지지 않으므로, “봉 1,000,000개를 축소”해도 픽셀당 약 4개(1600 px 차트에서 약 6,400개)가 보이며, 측정 장면은 나머지 봉을 이동하며 지나갑니다.
휴대폰과 앱
휴대폰은 3배 화면, 느린 CPU, 그리고 배터리를 씁니다. 차트는 픽셀 비율 최대 2로 그립니다(maxPixelRatio). 3배보다 픽셀이 56% 적고, 선은 거의 부드러워지지 않습니다. 화면 밖에 있거나 페이지가 숨겨지면 그리기를 멈추고(pauseWhenHidden), WebView에 넣은 앱은 suspend()로 멈출 수 있습니다. 틱이 프레임보다 빨리 오는 실시간 피드에서는 지표를 읽기 전에 프레임마다 한 번만 다시 계산합니다. 페이지나 네트워크가 돌아오면 너무 오래 조용한 스트림은 하트비트를 기다리지 않고 바로 다시 연결하고, 제거된 차트는 캔버스 메모리를 바로 돌려줍니다.
new Chart(host, {
maxPixelRatio: 2, // the default; Infinity draws at the screen's own
pauseWhenHidden: true, // the default: no frames off screen or on a hidden page
})
chart.suspend() // your app went to the background (a WebView): stop drawing
chart.resume() // back: one frame draws what changed 휴대폰 화면에서 측정(390 × 844, 3배, CPU 4배 느리게, 내장 GPU), 지표 4개가 있는 차트 하나:
| 이전 | 현재 | |
|---|---|---|
| 장면 그리기, 봉 300개 | 34 ms | 25 ms |
| 실시간, 프레임당 틱 2개 | 18.5 ms | 11–13 ms |
| 캔버스 메모리 | 17.1 MB | 7.6 MB |
터치 화면의 위젯: 입력 칸은 16 px라 포커스될 때 페이지가 확대되지 않고, 시트는 화면 키보드 위에 있으며, 높이는 동적 뷰포트 단위입니다. 직접 측정하려면 node scripts/bench-render.mjs --mobile.
메인 스레드
차트는 메인 스레드에서 지표를 계산합니다. @tradecanvas/core의 IndicatorWorkerHost는 Promise 기반 API, 요청별 타임아웃, SSR과 테스트를 위한 동기식 대체 경로를 갖추고 여러분이 제공한 워커에서 calculate()를 실행할 수 있지만, 차트는 아직 지표를 이를 통해 계산하지 않으며 패키지에 워커 스크립트도 포함되어 있지 않습니다.