Skip to content

アーキテクチャ

単一のリクエストパス — HTTP 受付 → キャッシュ済みまたは新規レンダー → 画像の返却 — に、興味深いロジックのほとんどが詰まっています。

フル フロー

リクエストパイプライン: 認証 → ハッシュ算出 → キャッシュルックアップ → セマフォ取得 → キャッシュ再チェック → ブラウザ確保 → 新規ページ → チャート描画 → スクリーンショット → キャッシュ保存 → ページクローズ → セマフォ解放 → レスポンス。

各ステップの対応モジュール:

ステップモジュール補足
認証チェックserver.ts::checkAuthAPI_KEY 設定時は 401 で拒否
ハッシュ算出cache.ts::computeHashSHA-256(正規化(リクエスト))[0:16]
キャッシュルックアップcache.ts::getCacheヒット時は X-Cache-Hit: true で返却
セマフォ取得semaphore.ts::acquireactive == CONCURRENCY(既定 8)なら待機
キャッシュ再チェックcache.ts::getCache(再)待機中に並行リクエストが完了していれば拾う
エンジン分岐renderer.tsengine で分岐)既定 skia はブラウザ不要。以降のブラウザ関連ステップは browser エンジンのみ
— skia 描画engine-skia.ts::renderSkiaskia-canvas にインプロセス描画(下記 browser ステップ群を丸ごと置換)
ブラウザ確保 (browser)renderer.ts::ensureBrowser動いていなければ puppeteer を起動
新規ページ (browser)b.newPage() + schedulePageCleanupセーフティネットタイマーでリーク防御
チャート描画 (browser)template.ts::buildHtml + page.goto(dataUrl)window.__chartRendered === true を待機、__chartMessages を回収
スクリーンショット (browser)container.screenshot({ type, quality })PNG / JPEG バイトを含む Buffer
キャッシュ保存cache.ts::setCacheCACHE_MAX_ENTRIES 超過時は古いエントリを追い出し
ページクローズ (browser)clearPageCleanup + page.close()ブラウザタブを解放
セマフォ解放semaphore.ts::releaseキューの次のレンダリングを起床

上の図とテーブルの「ブラウザ確保 → ページクローズ」は browser エンジンのページパスを描いています。既定の skia エンジンは 「エンジン分岐」で分かれ、engine-skia.ts::renderSkia が skia-canvas にインプロセス描画するため、ensureBrowser よりに 分岐し、skia だけを使うプロセスは Chromium を一切起動しません。

レスポンスには画像本体に加え X-Cache-* ヘッダと、Chart.js が 警告を出した場合のみ X-Chart-Messages が付与されます。

キーとなる設計判断

レンダリングエンジンは 2 つ

すべてのレンダリングは 2 つのエンジンのいずれかで動きます。既定は skia です。

  • skia(既定)skia-canvas(ネイティブ Skia)でブラウザを 起動せずインプロセス描画。高速(1 チャート数十 ms)・軽量で、 Chart.js と同梱プラグインは npm から読み込みます。
  • browserpuppeteer-core 経由のヘッドレス Chromium。実 ブラウザとのピクセル一致・最大忠実度が必要な場合に使用。プラグインは ページ内に CDN から読み込みます。

エンジンはレンダリング毎に選択(ライブラリ engine、CLI --engine、 HTTP engine)。設計上のポイント:

  • 分岐は ensureBrowserに行い、skia だけを使うプロセスは Chromium を起動しません。エンジンモジュールは遅延 import されるため、 browser 専用・メタデータ専用の呼び出しは skia-canvas + Chart.js を 読み込みません。
  • 2 つのエンジンは別々にキャッシュされます(computeHash の入力に engine が含まれる)。ピクセルが異なりうるため、同じチャートでも エンジンごとに別エントリになります。
  • chartjs-plugin-zoomskia エンジンに含まれません(操作専用で 静的描画に影響しないため)。必要なら browser エンジンを使います。
  • bun build --compile の単体バイナリでのみ、euler / venn は skia-canvas のコンパイル時特有の制約により ellipse フォールバック描画に なります(交差部の塗りに軽微なアーティファクト)。bun run・npm ライブラリ・サーバーでは完全な忠実度で描画します。

キャッシュは意図的に粗粒度

computeHash{chart, width, height, devicePixelRatio, backgroundColor, format, quality} を JSON.stringify した正規化文字列 をハッシュします。つまり:

  • chart のキー順の違いはハッシュに影響 しません (JSON.stringify は キーを並べ替えませんが、同じ論理オブジェクトを同じように送るクライアント なら実運用上まず衝突しません)。
  • データセットの値がわずかでも変われば、新しいハッシュが出ます。

病的なハッシュチャーンが起きる場合はキャッシュを無効化してください (CACHE_MAX_ENTRIES=0)。

セマフォとキャッシュは独立

キャッシュヒットはセマフォを取得 しません — 即座にキャッシュバッファ を返します。これにより繰り返し同じリクエストがブラウザタブを消費しません。

1 ブラウザ、多ページ(browser エンジン)

以下は browser エンジンのみに当てはまります。browser はモジュール レベルのシングルトン。各レンダリングは独自のタブ (b.newPage()) を取ります。browser.on('disconnected') で参照を null 化し、 次のリクエストで再起動。タブは時間制御されており、PAGE_TIMEOUT_SECONDS を超えたレンダリングはクリーンアップタイマがタブを強制クローズ — ハング レンダーによる孤児ページリークを防ぎます。

Chromium は --no-sandbox で起動(browser エンジン)

Docker 内で root 実行するための必須。これなしでは Chromium が起動 拒否します。Docker 外で root 実行するケースは既にもっと大きな問題が あると思ってください。

HTML テンプレートは静的(browser エンジン)

browser エンジンでは template.ts::buildHtml が、全プラグインの CDN <script> タグ、インライン CSS、および次を行う IIFE を含む単一の HTML 文書を生成します:

  1. 自動登録しないプラグインを登録 (datalabels、chartjs-chart-geo)。
  2. アニメーションを強制 OFF (page.screenshot が安定フレームを捕捉できるように)。
  3. console.warn / console.error をラップして window.__chartMessages に保存。
  4. try/catch で Chart インスタンスを作成。エラーは window.__chartError に格納。
  5. window.__chartRendered = true でレンダリング完了シグナル。

このテンプレートが「任意の Chart.js 設定を描画する」ことを実現しています — 単一のページ初期化が全チャートタイプと全プラグインに対応。

プラグインはページ初期化時に CDN から読み込み(browser エンジン)

browser エンジンでは、Chart.js プラグインを Node 側の JavaScript に バンドルせず、Chromium 内で jsdelivr から毎ページロードで取得します (skia エンジンはプラグインを npm から読み込むため CDN に依存しません)。 この結果:

  • ブラウザコールドスタート後の初回レンダーは遅くなる (ネットワークラウンドトリップ)。
  • オフライン運用にはローカルミラーが必要 (同じパスを配信する nginx、または puppeteer のリクエストインターセプトをキャッシュディレクトリに向ける)。

代わりに得られるメリット: プラグインバージョンアップは template.ts の一行変更だけで済み、再ビルド不要。

フローに含まれないもの

  • データベースなし。 キャッシュはメモリ内で、再起動で失われます。
  • 認証状態なし。 各リクエストはキーを持つか持たないか。
  • WebSocket / SSE なし。 単なる HTTP 1.1 リクエスト/レスポンス。
  • マルチプロセスレンダリングなし。 ロードバランサの裏で複数インスタンスを 走らせて水平スケール。インスタンス間でキャッシュは共有しません (意図的な設計 — CDN が適切なレイヤー)。

同時実行系のチューニングツマミ

環境変数既定値変わること
CONCURRENCY8セマフォスロット上限 — 同時レンダリング数(browser エンジンでは同時ブラウザタブ数)
PAGE_TIMEOUT_SECONDS601 レンダリングの上限時間。超えるとタブを強制終了
CACHE_MAX_ENTRIES1000LRU 容量
CACHE_TTL_SECONDS3600エントリ毎の TTL

ユーザー向け解説は 環境変数 (HTTP サーバー) および 環境変数 (CLI) を参照。

このページを編集Last updated: