Web開発
月額3ドルの予算でWeb 3Dの機能レポートを作成する
Reporting Web 3D Capabilities on a Budget of $3 a Month (ben3d.ca)
要約
Web3DSurvey.comは、Web 3D開発者向けにWebGLやWebGPUの機能サポートデータを収集するコミュニティサービスです。著者は、BigQueryの無料枠活用、効率的なデータ収集方法、JavaScriptでの集計、キャッシュ機構などを駆使し、月額約3ドルという低コストでこのサービスを運用している方法を解説しています。このアーキテクチャは、データ収集とレポート作成を分離し、コストを最小限に抑えています。
全文翻訳
私はWeb3DSurvey.comをコミュニティサービスとして運営しています。これは、3Dウェブ開発者が推測ではなく測定されたサポート率から機能を選択できるように、実際のWebGL、WebGL 2、およびWebGPUの機能データを収集するものです。なぜ私がこれを構築したのか、そしてv2のリライトで何が追加されたのかについてはすでに書いています。この記事は、人々が私に繰り返し尋ねる別の質問についてです。それは、運営コストはいくらか、そしてパイプラインはどのように機能するかということです。2026年4月、サイトとデータを保存するサービスは、約3カナダドルでした。2026年6月、コレクターは約50万件のリクエストを受け取りました。各リクエストは最大596個のデータポイントを持つ1つのサンプルを運び、月に約3億データポイントになります。私はReactとBigQueryを学ぶために2023年初頭にこのプロジェクトを開始しましたが、どちらも本番環境で使用したことはありませんでした。学習は定着し、低コストも維持されました。アーキテクチャは、収集、レポート作成、および両方の実行コストを分離しています。
コレクター
コレクターが測定するもの
コレクターは、3Dサイトがiframeを通じて埋め込む小さなスクリプトです。ブラウザでWebGL、WebGL 2、WebGPU、WebXR、およびデバイスの機能を調査した後、結果のstatsオブジェクトをAPIサーバーに送信します。専用のAPIサーバーは、ボットトラフィックをドロップし、正規化されたプラットフォーム、ブラウザ、およびエンジンフィールドを追加してから、バッチでBigQueryにサンプルを挿入します(これによりBigQueryの請求が削減されます)。当初はレガシーなストリーミング挿入APIを使用していましたが、BigQuery Storage Write APIに置き換えました。現在のボリュームは月間の無料取り込み割り当て内に収まるため、これによりBigQueryのコストが大幅に削減されました。
クライアントへの影響を軽減する
コレクターは、ホストページに配慮するように設計されています。参加サイトは、コレクタースクリプトを独自のページで直接実行するのではなく、小さなiframeとして埋め込みます。これにより、2つの別々のことが達成されます。セキュリティ—コレクタースクリプトがホストページ内のものにアクセスするのを防ぎます。パフォーマンス—コレクタースクリプトがホストページを遅くするのを回避します。
<iframe title="Web3D Survey" src="https://web3dsurvey.com/collector-iframe.html" style="width: 1px; height: 1px;" ></iframe>
コレクターが最初にGETでレポートする理由
コレクターは、POSTにフォールバックする前に、GETリクエストでサンプルをレポートしようとします。分析データとしては奇妙に聞こえますが、サーバーへのクロスオリジンリクエストの数を半分にします。埋め込みコレクターからのJSON POSTは、CORS OPTIONSプリフライトリクエストと実際のPOSTをトリガーします。圧縮されたペイロードをクエリ文字列に含めたプレーンなGETは、このプリフライトを必要としないため、成功したレポートは2回ではなく1回の要求でサーバーに到達します。ペイロードは依然として同じstatsオブジェクトです。コレクターはそれを文字列化し、gzip圧縮し、圧縮されたバイトをbase64エンコードし、結果をstatsクエリパラメータに格納します。
const statsAsBytes = new TextEncoder().encode(JSON.stringify(stats));
const compressedStats = gzipSync(statsAsBytes, { level: 9 });
const encodedStats = btoa(String.fromCharCode(...compressedStats));
const searchParams = new URLSearchParams();
searchParams.append('stats', encodedStats);
await fetch(`${collectionUrl}?${searchParams.toString()}`);
これは分析ブロッカーを回避するためではありません。それらは通常、ドメインまたはパスでブロックされ、リクエストがGETかPOSTかではブロックされません。
BigQueryでの50万件のサンプル
Web3DSurveyは毎月約50万件のコレクターリクエストを受け取ります。各リクエストは、最大596個のデータポイントを持つ1つのサンプルを運びます。これは、月に約3億の可能なデータポイントになります。ブラウザがAPIをサポートしていない場合、一部のフィールドは空になりますが、形状は十分に広いため、分析スキャン用に構築されたデータベースが必要になります。以前はPostgreSQLに分析データを保存しようとしました。PostgreSQLはアプリケーショントランザクションに優れていますが、成長するイベントテーブル全体でのオンデマンド分析は高価で、応答性を維持するのが困難になります。結局、その周りに要約テーブル、バックグラウンド集計ジョブ、または2番目の分析システムを構築することになります。BigQueryは、その分析システムを直接提供します。サンプルは、ブラウザ、WebGL、WebGL 2、WebGPU、およびWebXRデータの5日間のパーティション化されたテーブルに入ります。各パーティションは120日後に自動的に期限切れになります。
bq mk --table \
--time_partitioning_field createdAt \
--time_partitioning_type DAY \
--time_partitioning_expiration 10368000 \
--schema ./webgl2.json \
web3dsurvey:stats_v4.webgl2
保持されているテーブルは現在、Googleのus-central1リージョン(アイオワ州カウンシルブラフス)で約6.2 GiBを占めています。BigQueryには、毎月最初の10 GiBの論理ストレージが含まれているため、このデータセットの保存には現在0カナダドルかかります。その割り当てがない場合でも、リスト価格は約0.14米ドルになります。有料作業は、データの挿入と分析のためのデータの取得です。このプロジェクトはレガシーのストリーミング挿入を使用しており、正常に挿入された行に対して課金されます。オンデマンドクエリは、大きな月間無料割り当て後のスキャンバイト数に対して課金されます。レポートは最後の7日間のみをクエリするため、保持されているデータの狭いスライスをスキャンします。
レポート作成
コレクターとレポート作成パスは独立して実行されます。APIサーバーはサンプルを書き込み、Web3DSurveyサービスは公開サイトのアグリゲートを読み取ります。片側でのトラフィックの急増は、もう一方を枯渇させることはできません。
小さなクエリとJavaScriptのロールアップ
レポートサービスは、1つの巨大な集計クエリを実行しません。多数の小さなクエリを実行し、JavaScriptで要約を組み立てます。単一の機能クエリは、プラットフォームとブラウザごとにグループ化された生のtrue/falseカウントを取得し、7日間のウィンドウにフィルタリングします。
SELECT platform.name AS platformName, browser.name AS browserName, COUNTIF(extensions.`EXT_color_buffer_float` = TRUE) AS `true`, COUNTIF(extensions.`EXT_color_buffer_float` = FALSE) AS `false`,
FROM `web3dsurvey.stats_v4.webgl`
WHERE platform.name IS NOT NULL AND version >= 7 AND DATE(createdAt) >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY)
GROUP BY platformName, browserName
次に、JavaScriptはこれらのカウントを、ページが表示するサポート率、プラットフォームごとの内訳、およびブラウザファミリーごとのロールアップに変換します。同じパターンが、累積制限分布、アダプターとベンダーの階層、およびトップNトリミングを処理します。集計をJavaScriptに保持することで、SQLはシンプルになり、ロジックはテスト可能な1つの言語になります。コストはファンアウトです。完全なWebGLレポートは、拡張機能とパラメータごとにこれらのクエリの1つを実行し、アダプターとサポートクエリも並列で実行します。
const [featuresArray, limitsArray, rendererInfo /* ... */] = await Promise.all([
Promise.all(extensionNames.map((name) => getFeatureStats(tableName, 'extensions', name))),
Promise.all(parameterNames.map((name) => getLimitStats(tableName, 'parameters', name))),
getGraphicsAdapterStats(tableName, 'rendererInfo', 'renderer'),
// ...
]);
各クエリはBigQueryに対して約500ミリ秒かかります。単一のレポートには、それらの数十が必要です。ページビューごとにそれらを再実行すると、ページの構築に数秒かかるでしょう。
透明なJSONキャッシュ
BigQueryの組み込み結果キャッシュに依存することはできませんでした。参照されているテーブルに最近のストリーミング挿入がある場合、クエリが変更されていないパーティションの古いデータにフィルタリングしても、BigQueryはキャッシュをスキップします。1日を通してサンプルが到着するため、有効期間を制御できるBigQueryの外部にキャッシュが必要でした。BigQueryの前に配置される汎用キャッシュレイヤーを記述しました。すべての集計呼び出しは、名前、クエリパラメータ、TTL、および実際の作業を実行するクリエーター関数を受け取るcache()関数を通過します。
const rows = await cache<GetFeatureStatsQueryResult>(
'getFeatureStats',
{ tableName, categoryName, featureName, minVersion },
cacheExpiration,
async () => bigQuery<GetFeatureStatsQueryResult>(query, getFeatureStatsQueryResult),
getFeatureStatsQueryResult,
);
キャッシュは最初にメモリをチェックし、次にCloud StorageのJSONファイルをチェックし、ミスの場合にのみクリエーターを呼び出します。キーはパラメータのハッシュであるため、一意のクエリごとに独自のキャッシュファイルがあります。TTLは2日間です。キャッシュファイルが存在しても期限切れの場合、レイヤーはすぐに古いコピーを提供し、バックグラウンドで再検証するため、v