tech-book-labs
データ基盤(クライアント完結) · 最終検証 2026-09-22 · @duckdb/duckdb-wasm 1.33.1-dev45.0 · 初公開 2026-05-10

DuckDB-Wasm でブラウザ内 SQL を動かす

DuckDB-Wasm をブラウザに埋め込み、SQL をクライアント完結で実行するパターン。jsDelivr から読み込まれるファイルの実測(WASM は転送時 6.8 MB・展開後 34 MB、coi は含まれない)、CDN の worker を直接起動すると SecurityError になる理由と回避、COOP/COEP、4GB の memory 上限、Parquet を URL 直読みする時の CORS まで。

duckdb wasm sql browser-analytics

検証日: 2026-09-22(初版 2026-05-26)

使用バージョン: @duckdb/duckdb-wasm@1.33.1-dev45.0(DuckDB v1.5.1)

対象: ブラウザだけで分析クエリを動かしたい(サーバー不要)、Parquet を直接読みたい、ローカル CSV 解析

DuckDB(列指向の高速分析 SQL DB)を WASM(WebAssembly) ビルドでブラウザに埋め込み、サーバーなしで SQL を実行するパターン。DDL / INSERT / SELECT / Parquet 読み込み、WASM ロードの落とし穴を動く demo で確認します。

触って試す

🦆 DuckDB-Wasm が初回 ~3MB の wasm を fetch します。読み込み完了後に実行できます。

初回は WASM(転送時 約 6.8 MB)を fetch するため、回線によって 1〜数秒待ちます。

セットアップ(npm + 3 種類の WASM bundle)

npm i @duckdb/duckdb-wasm
項目値
公開バージョン1.33.1-dev45.0(SELECT version() は v1.5.1 を返す。2026-09-22 実測)
Node 必要バージョン18+(主用途はブラウザ)
TypeScript 型本体同梱
ブラウザ要件WebAssembly + Web Worker(eh は WebAssembly Exception Handling。SharedArrayBuffer が要るのは coi だけ)
WASM サイズeh = 展開後 34.1 MB / 転送時(brotli)6.8 MB、mvp = 39.2 MB / 7.7 MB(下の実測)。main bundle に同梱厳禁
ライセンスMIT

DuckDB-Wasm は環境別に 3 種類の WASM bundle(mvp / eh / coi)を持っています。ただし getJsDelivrBundles() が返すのは mvp と eh の 2 つだけで、selectBundle() はこの 2 つからブラウザの機能に合わせて選びます(Chromium では eh)。coi を使うには bundle を手動で定義します:

bundle位置づけ必要なブラウザ機能getJsDelivrBundles() に含まれるか
mvpfallbackWebAssembly のみ含まれる
eh標準(現行ブラウザで選ばれる)WebAssembly Exception Handling含まれる
coiマルチスレッドSharedArrayBuffer + Cross-Origin Isolation(COOP/COEP ヘッダ要)含まれない(duckdb-coi.wasm / duckdb-browser-coi.worker.js / duckdb-browser-coi.pthread.worker.js は dist にあるので手動で定義する)

3 つの速度差はこの記事では測っていません(公式の説明では coi がスレッドを使える分だけ速いとされています)。

coi を使うには HTTP レスポンスに 2 つのヘッダ が必要:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Cloudflare Pages / Workers / Vercel いずれもヘッダは _headers ファイル(Pages)や Response.headers で設定可能。iframe 内に埋め込む場合は 親ページも同じヘッダ必須。jsDelivr のファイルは Cross-Origin-Resource-Policy: cross-origin を返すので、COEP を付けたページからでも読み込めます。

なぜブラウザで SQL?

  • サーバー / DB 不要:プライベートデータをアップロードせず、ローカルで分析できる
  • Parquet を直接読める:CSV や JSON より高速、列志向で集約に強い
  • OLAP 系クエリ最適化:GROUP BY / WINDOW / JOIN が Postgres 互換 + 高速
  • AI agent との相性:LLM に SQL を書かせて即実行する UX

代替: SQL.js(SQLite を WASM 化)。OLAP / Parquet なら DuckDB のほうが速い。

最小サンプル(DuckDB-Wasm 1.33、5 ステップ)

AsyncDuckDB を bundle に応じた worker と一緒に初期化、connect() でコネクションを取って query() を実行するだけ:

import * as duckdb from "@duckdb/duckdb-wasm";

const bundles = duckdb.getJsDelivrBundles();
const bundle = await duckdb.selectBundle(bundles);

// CDN の URL を new Worker() に直接渡すと SecurityError になる(Worker は同一オリジンのスクリプトしか起動できない)。
// 同一オリジンの Blob から importScripts で読み込ませる
const workerUrl = URL.createObjectURL(
  new Blob([`importScripts("${bundle.mainWorker!}");`], { type: "text/javascript" }),
);
const worker = new Worker(workerUrl);
const db = new duckdb.AsyncDuckDB(new duckdb.ConsoleLogger(), worker);
await db.instantiate(bundle.mainModule, bundle.pthreadWorker);
URL.revokeObjectURL(workerUrl);

const conn = await db.connect();
await conn.query(`
  CREATE TABLE sales (region TEXT, revenue DOUBLE);
  INSERT INTO sales VALUES ('east', 1500), ('west', 1100);
`);

const result = await conn.query("SELECT region, SUM(revenue) FROM sales GROUP BY region");
console.log(result.toArray());

Parquet / CSV を直接読む

registerFileURL で URL を登録 → SQL から read_csv / read_parquet で直接読める:

// HTTP 経由で Parquet を直接 SQL 対象にできる
const r = await conn.query(`
  SELECT category, COUNT(*)
  FROM 'https://example.com/data.parquet'
  GROUP BY category
`);

// ローカル CSV(ユーザがアップロードした File を Blob URL 化)
const file = inputElement.files![0];
await db.registerFileHandle("upload.csv", file, duckdb.DuckDBDataProtocol.BROWSER_FILEREADER, true);
await conn.query("SELECT * FROM 'upload.csv' LIMIT 10");

バンドル戦略

WASM は転送時でも約 6.8 MB あるので、main bundle に同梱しない。動的 import で初回利用時にだけロード:

const init = async () => {
  const duckdb = await import("@duckdb/duckdb-wasm");
  // ...
};

jsDelivr から読み込むときに起きること(実測)

getJsDelivrBundles() は、インストールした版に対応する jsDelivr の URL を組み立てて返すだけの関数です。何が読み込まれるかを、Node から jsDelivr に問い合わせて確かめました(1.33.1-dev45.0)。

ブラウザ側(Chromium、crossOriginIsolated = false)で確かめた挙動:

やったこと結果
selectBundle(getJsDelivrBundles())duckdb-eh.wasm が選ばれる(pthreadWorker は無し)
new Worker(bundle.mainWorker)(CDN の URL を直接)SecurityError: Failed to construct 'Worker'(別オリジンのスクリプトは Worker にできない)
Blob + importScripts で包んで instantiate1,594 ms(WASM のダウンロード込み・キャッシュなし)。その後の最初の SELECT は 60 ms

初期化の 1.6 秒は回線とキャッシュで大きく変わります(2 回目以降はブラウザのキャッシュから読まれる)。自前でホストする場合は bundles を手動で定義し、.wasm を application/wasm で、brotli を効かせて配信します。圧縮が効いていないと 34 MB をそのまま送ることになります。再現: scripts/measure/duckdb-jsdelivr-worker.mjs

主な SQL 機能

機能サポート
GROUP BY / 集約✅
JOIN(全種類)✅
WINDOW 関数✅(LEAD / LAG / RANK / ROW_NUMBER)
CTE / 再帰 CTE✅
Date / Timestamp 関数✅(date_trunc / interval)
JSON 関数✅(json_extract)
Geospatial△(spatial extension で対応)

PostgreSQL 互換が高く、Postgres で書いた analytic クエリはほぼそのまま動く。

Parquet を URL から直読みする 3 つの前提

SELECT * FROM 'https://example.com/data.parquet' の 1 行で外部 Parquet を読めるのは強力だが、現実は CORS と Range request が前提:

  1. Access-Control-Allow-Origin: 別ドメインの Parquet を読むなら必須。S3 / GCS / R2 は bucket の CORS 設定で許可
  2. Accept-Ranges: bytes: DuckDB は Parquet の footer から先に読むため、サーバが HTTP Range request に応える必要がある。Cloudflare R2 / S3 はデフォルトで対応、自前 nginx は chunked_transfer_encoding off 等の調整が必要
  3. HTTPS: crossOriginIsolated 環境では mixed content 不可

ローカル CSV を読ませる場合は registerFileHandle で File を渡すだけで OK(CORS の話は出てこない):

await db.registerFileHandle("upload.csv", file, duckdb.DuckDBDataProtocol.BROWSER_FILEREADER, true);
await conn.query("CREATE TABLE t AS SELECT * FROM 'upload.csv'");

8 つの落とし穴と直し方

症状原因直し方
new Worker(bundle.mainWorker) で SecurityErrorjsDelivr の URL は別オリジン。Worker は同一オリジンのスクリプトしか起動できないBlob に importScripts("<CDN の URL>") を書いて、その Blob URL から Worker を作る(上の最小サンプル)
coi bundle が使われないgetJsDelivrBundles() に coi は含まれない。さらに COOP/COEP ヘッダ未設定 だと SharedArrayBuffer が使えないcoi の bundle(duckdb-coi.wasm と pthread worker)を手動で定義し、host の HTTP ヘッダに COOP/COEP 設定。iframe で読むなら親も同じヘッダ
Parquet を読むと CORS エラーCORS / Range request 未設定S3 / R2 の CORS allow origin と Accept-Ranges: bytes を確認
Out of Memory で死ぬWASM の 4GB 上限(32-bit)Parquet を read_parquet('url', file_row_group_size=100000) で chunk 読み、または server 側で集約
UI がガクッと止まるsync DB を使っている / worker を使っていないAsyncDuckDB + worker を必ず使う(new duckdb.AsyncDuckDB(logger, worker))
列が全部 VARCHAR で推論されるCSV 型推論が ambiguousread_csv_auto('upload.csv', columns={'age': 'INT'}) で明示、もしくは CAST
初回 fetch が 3 秒以上かかるWASM が main bundle で同梱されない / preload なし<link rel="preload" as="fetch" type="application/wasm" crossorigin> で予読、または dynamic import で route 単位
クエリ結果が JS 側で BigInt で来るDuckDB の BIGINT は JS 上 BigIntresult.toArray() で受けた後 Number(row.x) で変換、もしくは SQL で CAST AS INT

評価

観点評価コメント
クエリ性能◎OLAP に最適化、数百万行も実用速度
Parquet サポート◎直接 URL から読める
バンドル△WASM は転送時 6.8 MB(展開後 34 MB)、動的 import 必須
学習コスト○SQL 既知者なら API は薄いラッパー
エコシステム○Observable Notebook / MotherDuck で連携進行中

向く / 向かないケース

  • 向く: ブラウザ内 BI ダッシュボード、ユーザがアップロードした CSV/Parquet の分析、エディタ内 query playground
  • 向かない: 数 GB のデータをクライアントで処理(memory limit)、リアルタイム書き込みの DB 用途
  • 向かない: 単純な「SQLite で十分」(SQL.js のほうがバンドル軽い)

関連 Topic / 関連書籍

この記事と関係する tech-book.net の Topic と、それぞれの Topic に紐づく書籍:

tech-book.net /books/9784297144944

JavaScriptによるはじめてのアルゴリズム入門

河西 朝雄 · 技術評論社 · 2024年

JavaScriptでアルゴリズムの基礎から応用まで体系的に実装する

詳細を tech-book.net で見る
tech-book.net /books/9784873118086

Python と JavaScriptではじめるデータビジュアライゼーション

Kyran Dale / 嶋田 健志 / 木下 哲也 · オライリー・ジャパン · 2017年

PythonとJavaScriptで実装するデータ収集から可視化までのフルパイプライン

詳細を tech-book.net で見る
tech-book.net /books/9784839966645

React Native+Expoではじめるスマホアプリ開発 : JavaScriptによるアプリ構築の実際

松澤 太郎 · マイナビ出版 · 2018年

ExpoでiOS/Android両対応アプリをJavaScriptで構築する

詳細を tech-book.net で見る
tech-book.net /books/9784627857216

はじめてのWebデザイン&プログラミング : HTML、CSS、JavaScript、PHPの基本

村上 祐治 · 森北出版 · 2023年

HTML・CSS・JavaScript・PHP を横断してWebの全体像を最短で把握する

詳細を tech-book.net で見る
tech-book.net /books/9784297138714

フロントエンドの知識地図ーー 一冊でHTML/CSS/JavaScriptの開発技術が…

株式会社ICS 池田 泰延/西原 翼/松本 ゆき

フロントエンド技術の全体像を俯瞰し、学習の優先順位を自分で判断できるようにする

詳細を tech-book.net で見る