モバイル&ワイヤレスブロードバンドでインターネットへ

gwaw.jp
 
RENDERING BENCHMARK · 技術ノート

ティックは何点まで描けるか
— 4つの描画方式の限界を測る

刻々と流れ込む金融のティックデータ。それをリアルタイムに、滑らかに描き続けるには、どの描画技術を選ぶべきか。Canvas 2D、SVG、WebGL、WebGPU——4つの方式を、同一の土台の上で、点数を増やしながら限界まで追い込む。実機で測った一枚のFPS曲線が、その答えを描き出す。

Canvas 2DSVGWebGL WebGPUFPSベンチマークティックチャート

01流れ込むデータを、どう描くか

株価のティック、センサーの信号、ログのストリーム——現代のフロントエンドは、絶えず更新される時系列データをリアルタイムに描画する場面が多い。新しい点が右端に加わり、波形が左へ流れていく。この「動き続けるチャート」を、何点まで滑らかに(60fpsで)描けるかは、描画技術の選択で大きく変わる。

ブラウザには、線を描く方法が複数ある。Canvas 2D(命令的にピクセルを塗る)、SVG(DOM要素として図形を持つ)、WebGL(GPUに頂点を送る)、そしてWebGPU(次世代のGPU API)。それぞれ設計思想が異なり、得意な規模も違う。本稿では、この4つを同じ土台で公平に比較し、点数を1千から100万まで増やしながら、各方式が何点で崩れるかを実機で測る。

02公平な比較のための共通土台

比較の要は公平性だ。描画方式だけを入れ替え、それ以外——データ生成、更新モデル、性能計測——は完全に共通にする。

データはリングバッファで管理する。価格をランダムウォークで生成し、固定長の配列に循環的に書き込む。新しい点の追加が配列のシフトを伴わず O(1) で済むので、データ管理コストが描画性能の測定を汚さない。毎フレーム、新しいティックを1点加え、直近N点を描画する。

JavaScript — リングバッファとティック生成
const CAP = 1050000; const prices = new Float32Array(CAP); let head = 0, count = 0, lastPrice = 100; function genTick() { // ランダムウォーク(たまに大きく動く) const shock = (Math.random() < 0.02) ? (Math.random()-0.5)*4 : 0; lastPrice += (Math.random()-0.5)*0.6 + shock; prices[head] = lastPrice; head = (head + 1) % CAP; // 循環(シフト不要でO(1)) if (count < CAP) count++; }

性能は実効FPS(requestAnimationFrameが実際に回った回数)と1フレームの描画時間で測る。価格レンジの走査(min/max)も全方式で共通に行うので、その分のCPUコストは全方式に等しく乗る。残る差は、純粋に「線をどう画面に出すか」だけになる。

なぜFPSと描画時間を分けるか 描画時間が短くてもFPSが出ないことがある。フレームの合成や提示(ブラウザ・OS側)にコストがかかるためだ。後で見るように、この2つの指標のズレ自体が、デバイスの特性を暴く手がかりになる。

03Canvas 2D — 命令的描画のベースライン

最も素直な方法がCanvas 2Dだ。パスを開き、点を線でつなぎ、ストロークする。命令的で分かりやすく、実務のチャートで最も広く使われる。

Canvas 2D — 全点を1本の線で
ctx.clearRect(0, 0, W, H); ctx.beginPath(); ctx.moveTo(0, H - (prices[start]-mn)*sy); for (let i=1; i<k; i++) { ctx.lineTo(i*sx, H - (prices[(start+i)%CAP]-mn)*sy); } ctx.stroke();

毎フレーム全点を描き直す。点数に比例して命令数が増えるので、描画時間は点数にほぼ線形で伸びる。数万点までは軽快だが、十数万点を超えると1フレーム16.7msの予算を食い潰し始める。これが後の曲線の「黄色い線」になる。

04SVG — DOMベースの意外な健闘

SVGは図形をDOM要素として持つ。各点を個別の要素にすると数千点でDOMが爆発するが、1本の<polyline>にまとめる実装なら話が違う。要素は1つで、毎フレーム更新するのはpoints属性の文字列だけだ。

SVG — points属性を組み立てて更新
let parts = new Array(k); for (let i=0; i<k; i++) { const y = H - (prices[(start+i)%CAP]-mn)*sy; parts[i] = (i*sx).toFixed(1) + ',' + y.toFixed(1); } poly.setAttribute('points', parts.join(' '));

ボトルネックは、巨大なpoints文字列の生成(JS側)と、それをブラウザが再パース・再描画する処理だ。それでも実測では、Canvasの2〜3倍遅い程度で健闘する。「SVGは遅い」という通説は、各点を個別要素にする素朴な実装の話であって、1本のpolylineなら十分実用になる——というのが、意外だが重要な発見だった。ただしCanvasには及ばず、点数を増やすと差は開く。

05WebGL — GPUが壁を破る

ここからGPU描画だ。WebGLは、点の座標を頂点バッファとしてGPUに送り、GPUが並列に処理して線を描く。CPUは「描け」と指示するだけで、実際の描画はGPUが一手に引き受ける。

WebGL2 — 頂点バッファを送ってLINE_STRIP
// 頂点データ(index, price)をバッファへ gl.bufferData(gl.ARRAY_BUFFER, vd.subarray(0, k*2), gl.DYNAMIC_DRAW); // 価格レンジをuniformで渡し、頂点シェーダーで正規化 gl.uniform4f(uLoc, k-1, mn, (mx-mn), 0); gl.drawArrays(gl.LINE_STRIP, 0, k); // 一括描画
頂点シェーダー(GLSL)
layout(location=0) in vec2 pos; // x=index, y=price uniform vec4 U; // レンジ情報 void main() { float nx = pos.x / U.x; float ny = (pos.y - U.y) / U.z; gl_Position = vec4(nx*2.0-1.0, ny*2.0-1.0, 0.0, 1.0); }

GPUは全頂点を同時に処理するので、点数が増えても描画時間がほとんど伸びない。CPU描画の「点数比例」の宿命を、GPUは並列処理で断ち切る。これが後の曲線で、CPU方式が急降下する中、WebGLが高い位置で横ばいを保つ理由だ。

06WebGPU — 最新APIの実力

WebGPUは、モダンなGPUアーキテクチャに合わせて設計された次世代API。WebGLと同じくGPU描画だが、より明示的で効率的な設計を持つ。頂点バッファに座標を載せ、WGSLシェーダーで変換し、line-stripで描く構造はWebGLと似ている。

WGSL — 頂点シェーダー
struct R { kx:f32, mn:f32, rng:f32, _p:f32, }; @group(0) @binding(0) var<uniform> U : R; @vertex fn vs(@location(0) pos: vec2<f32>) -> @builtin(position) vec4<f32> { let nx = pos.x / U.kx; let ny = (pos.y - U.mn) / U.rng; return vec4<f32>(nx*2.0-1.0, ny*2.0-1.0, 0.0, 1.0); }

実測では、WebGPUもWebGLも100万点を難なく描き切った。両者はほぼ互角。この「互角」であることが、実は最も重要な発見につながる(後述)。なお、どちらが上かはデバイスによって入れ替わる。

07実測:4方式のFPS曲線

2台の実機で、各方式のNを1千から100万まで上げながら実効FPSを測定した。横軸が点数(対数)、縦軸がFPS。線が高い位置で横ばいなら、大量データでも滑らか。急降下する点が、その方式の限界だ。

▍ iPad mini (A17 Pro, 60Hz)

60fps上限のデバイス。60fpsに張り付いていれば滑らか。

▍ Pixel 10 Pro (120Hz)

120fps対応デバイス。より高いFPSまで伸びる。

一目瞭然だ。GPU方式(WebGL・WebGPU)は点数が増えてもほぼ水平を保つのに対し、CPU方式(Canvas・SVG)は数万点から断崖のように落ちる。100万点での差は歴然:

▼ 100万点での実効FPS

方式iPad mini
(60Hz)
Pixel
(120Hz)
Canvas 2D112
SVG33
WebGL49121
WebGPU60119

GPU方式はCPU方式の、およそ数十倍のスループット。Pixelでは、WebGLがCanvasの60倍(121fps対2fps)に達した。「点数比例の壁」を、GPUが完全に破っている。

08数字の裏を読む

# WebGLとWebGPUが「互角」の意味

両GPU方式が、100万点でほぼ同じFPSになった。これは偶然ではない。GPUによる描画自体は、どちらも一瞬で終わる。すると残るボトルネックは、両者に共通のCPU側の処理——価格レンジの走査と、頂点データの詰め込みだ。描画がボトルネックでなくなった今、限界を決めるのはGPUに渡すのデータ準備になっている。だから両者が同じ速度に収束する。

次の最適化の在り処 GPU描画方式を選んだ時点で、「描画」はもはやボトルネックではない。さらに速くしたいなら、狙うべきはCPU側だ。レンジ走査の削減(固定スケールや差分更新)、頂点データの再利用、あるいはデータ準備自体をGPU(compute shader)やWASMに移す——それが次の一手になる。

# デバイスで入れ替わるWebGLとWebGPU

面白いことに、優劣はデバイスで逆転した。iPad mini(Apple GPU)では100万点でWebGPU 60fps > WebGL 49fps。Pixel(Adreno)ではWebGL 121fps ≧ WebGPU 119fps。まだWebGPUは新しく、各プラットフォームの実装成熟度に差がある。「最新APIが常に速い」とは限らない——実機で測って初めて分かることだ。

# 描画時間が短くてもFPSが出ない

Pixelでは、Canvas 2Dの描画時間がiPadより短いのに、FPSは低かった。描画の速さと、フレームを画面に提示する速さは別物で、後者はブラウザ・OS側の合成処理に依存する。FPSは描画時間だけでは決まらない。だからこそ、実効FPSという「最終的にユーザーが体感する滑らかさ」を測ることに意味がある。

測定の誠実な注記 本ベンチは各条件を短時間(約1秒)で測る単発測定のため、特に重い条件(100万点)ではFPSに揺らぎがある。同じWebGPU・100万点でも、測定回によって60fps台から120fps近くまでぶれることがあった。だが桁で見れば結論は揺るがない——GPU方式はCPU方式の数十倍速い。数字は目安として読み、傾向を掴んでほしい。

09結論と、自分で試す

実務の指針はシンプルだ。数千〜数万点なら、Canvas 2Dで十分。実装が素直で、この規模なら60fpsを楽に保つ。SVGも1本のpolylineなら選択肢になる。だが数十万点を超える大量データを滑らかに描くなら、WebGL / WebGPU が必須。GPU描画は点数の壁をほぼ感じさせない。

そして、GPU方式を選んだ後の世界では、ボトルネックは描画からデータ準備へと移る。「何で描くか」を解いた先に、「どうデータを用意するか」という次の問いが待っている。手のひらのデバイスで100万点の波形が滑らかに流れる——その裏側には、CPUとGPUの役割分担という、普遍的な設計の物語がある。

下のツールは、本稿のベンチマークそのものだ。あなたの端末で「自動計測」を押せば、4方式のFPS曲線が描かれる。手元のデバイスの限界地図を、自分の目で確かめてほしい。

実効FPS
描画時間
描画点数
各方式について、Nを段階的に上げながら実効FPSを測定し、下のグラフに曲線として描きます。実機で1回走らせるだけで、4方式の「限界地図」が得られます。計測中は画面に触れないでください。
横軸=表示点数N(対数)、縦軸=実効FPS。60fpsの線を上回っていれば滑らか。各方式が「何点で60fpsを割るか」が限界点です。

『ティックは何点まで描けるか — Canvas/SVG/WebGL/WebGPU 描画方式の限界』を公開しました。