DuckDB-WASM: аналитика в браузере
Обычная архитектура аналитического приложения выглядит так: браузер отправляет SQL на сервер, сервер исполняет запрос в базе, гонит результат обратно по сети. Каждый клик в дашборде — это сетевой round-trip, нагрузка на бэкенд и задержка на десятки или сотни миллисекунд. А ещё это сервер, который надо поднять, масштабировать и оплачивать.
DuckDB-WASM убирает сервер из этой схемы целиком. Это DuckDB, скомпилированный в WebAssembly — бинарный формат, который исполняется виртуальной машиной внутри браузера почти на скорости нативного кода. Движок, оптимизатор, storage-формат, чтение Parquet — всё это работает на вкладке пользователя. SQL-запрос не покидает машину клиента. В этом уроке разберём, как именно DuckDB попадает в браузер, почему он там быстрый, и как OPFS даёт ему настоящую персистентность — базу, которая переживает перезагрузку вкладки.
Что такое WebAssembly и зачем он DuckDB
JavaScript — единственный язык, который браузер исполняет напрямую, и для тяжёлых вычислений он не годится: динамическая типизация, сборка мусора, отсутствие контроля над памятью. DuckDB написан на C++ — переписать его на JavaScript значило бы потерять всю производительность.
WebAssembly решает эту проблему. Это низкоуровневый байткод со статической типизацией и линейной моделью памяти (один непрерывный массив байт). Компилятор C++ (в случае DuckDB — Emscripten поверх LLVM) транслирует исходники DuckDB в .wasm-модуль, а браузер исполняет этот байткод своей WASM-машиной — той же, что лежит в основе V8 или SpiderMonkey.
Производительность WASM — порядка 80-95% от нативного C++ на вычислительных задачах. Для DuckDB это критично: векторизованный движок, который мы разбирали в модуле о движке исполнения, остаётся векторизованным и в браузере. Vector size 2048, push-based исполнение, zonemaps — всё работает. WASM теряет немного на границах вызовов и на отсутствии некоторых SIMD-инструкций, но проигрыш умеренный.
При этом WASM наследует ограничения песочницы браузера. Самое заметное — нет прямого доступа к файловой системе диска: WASM-модуль не может открыть /home/user/data.parquet так, как это делает нативный DuckDB. Параллелизм возможен только через Web Workers, а разделяемая память между ними требует заголовков Cross-Origin-Opener-Policy и Cross-Origin-Embedder-Policy на странице. Эти ограничения формируют то, как DuckDB-WASM устроен.
Как данные попадают в браузерный DuckDB
Раз файловой системы нет, возникает вопрос: откуда DuckDB-WASM берёт данные? Есть несколько путей, и они закрывают большинство сценариев.
Первый — HTTP-доступ к удалённым файлам. DuckDB-WASM умеет читать Parquet и CSV прямо по URL, причём не целиком: для Parquet он использует HTTP range requests — запрашивает сначала footer файла с метаданными, по нему понимает, какие row groups нужны под текущий фильтр, и подтягивает только их байтовые диапазоны. Файл на 2 ГБ в публичном бакете S3 можно осмысленно запрашивать, скачав десятки мегабайт.
-- DuckDB-WASM читает Parquet по URL, подтягивая только нужные row groups
SELECT carrier, count(*) AS flights
FROM 'https://example-bucket.s3.amazonaws.com/flights-2026.parquet'
WHERE origin = 'JFK'
GROUP BY carrier
ORDER BY flights DESC;
-- Результат (запрос исполнен целиком в браузере):
-- carrier | flights
-- B6 | 41207
-- DL | 38114
-- AA | 22980
Второй путь — загрузка локального файла пользователем. Когда пользователь выбирает файл через <input type="file"> или перетаскивает его на страницу, JavaScript получает объект File. DuckDB-WASM регистрирует этот файл в своей виртуальной файловой системе, и дальше FROM 'имя_файла' работает так же, как с локальным файлом в нативном DuckDB. Данные при этом не уходят никуда за пределы вкладки.
Третий — прямая передача данных из JavaScript: можно вставить Arrow-таблицу или результат fetch() напрямую в DuckDB через его JS API.
DuckDB-WASM поставляется в нескольких сборках (bundles): облегчённая стартует быстрее, полная поддерживает разделяемую память и многопоточность через Web Workers. Загрузчик @duckdb/duckdb-wasm сам выбирает подходящий bundle по возможностям браузера. Многопоточный bundle требует COOP/COEP-заголовков — без них DuckDB-WASM откатывается на однопоточное исполнение.
OPFS: настоящая персистентность в браузере
Чтение по HTTP и загруженные файлы хороши, но это эфемерные данные: закрыл вкладку — состояние потеряно. Долгое время браузерный DuckDB работал только in-memory. Это изменил OPFS — Origin Private File System.
OPFS — это часть Storage API браузера: приватная файловая система, выданная конкретному origin (паре «протокол + домен»). Она не видна пользователю как папки, не пересекается с данными других сайтов, и — ключевой момент — даёт WASM-коду быстрый синхронный доступ к файлам через специальные synchronous access handles в Web Worker. Именно синхронность важна: storage-движку DuckDB нужно читать и писать блоки предсказуемо, а не через цепочку промисов.
DuckDB-WASM использует OPFS как место хранения своего single-file storage-формата. Указываете путь со схемой opfs:// — и база живёт в OPFS:
-- Открыть (или создать) персистентную базу в OPFS
ATTACH 'opfs://analytics.db' AS local_db;
-- Включить запись каждого изменения сразу в OPFS, без откладывания
SET checkpoint_threshold = '0KB';
-- Создаём таблицу — она физически ложится в OPFS
CREATE TABLE local_db.events AS
SELECT * FROM 'https://example-bucket.s3.amazonaws.com/events.parquet';
-- Результат:
-- 1_240_551 строк записано в opfs://analytics.db
После перезагрузки вкладки тот же ATTACH 'opfs://analytics.db' откроет базу с уже существующими данными. Это полноценная локальная аналитическая БД внутри браузера: с persistent-хранилищем, ACID-транзакциями, колоночным сжатием и storage-форматом, который мы разбирали в модуле о storage.
Параметр checkpoint_threshold = '0KB' стоит понимать точно. По умолчанию DuckDB накапливает изменения в WAL и сбрасывает их в основной файл (делает checkpoint), когда WAL вырастает до порога. Значение 0KB означает «делать checkpoint после каждого изменения» — состояние всегда полностью на диске OPFS. Это безопасно при внезапном закрытии вкладки, но создаёт I/O-нагрузку на каждый statement. Для приложения, где пользователь много читает и редко пишет, это разумный компромисс; для пакетной загрузки тысяч строк порог лучше оставить выше и сделать CHECKPOINT вручную в конце.
OPFS не безграничен. Браузер выделяет origin квоту, зависящую от свободного места на диске и эвристик, а при нехватке места может вычистить хранилище неактивного сайта. Для рабочих наборов в единицы и десятки гигабайт это рабочий инструмент; рассчитывать на него как на бесконечное хранилище нельзя.
Где DuckDB-WASM на месте, а где нет
DuckDB-WASM не замена серверной аналитике, у него своя ниша. Сильные стороны:
| Сценарий | Почему DuckDB-WASM выигрывает |
|---|---|
| Интерактивный дашборд над фиксированным датасетом | Каждый фильтр — локальный запрос, ноль сетевых round-trip, отклик в миллисекундах |
| Демо и playground SQL | Никакого бэкенда, страница работает как статика на CDN |
| Приватные данные пользователя | Файл не покидает браузер — важно для медицинских, финансовых данных |
| Edge/offline-приложения | После загрузки .wasm и данных работает без сети |
| Снижение нагрузки на сервер | Аналитика уезжает на тысячи клиентских машин вместо одного бэкенда |
Слабые стороны тоже определяются архитектурой. Объём ограничен памятью вкладки и квотой OPFS — терабайтный датасет в браузер не положить. Холодный старт включает скачивание .wasm-модуля (единицы мегабайт) и данных. Параллелизм упирается в число Web Workers и наличие COOP/COEP-заголовков. И каждый клиент исполняет запрос на своём железе — у кого-то это мощный ноутбук, у кого-то слабый телефон.
Хорошая ментальная модель: DuckDB-WASM — это «последняя миля» аналитики. Тяжёлую подготовку данных делает серверный пайплайн (в капстоуне это будет DuckLake), он публикует компактные Parquet-витрины, а DuckDB-WASM раздаёт по ним интерактивные запросы прямо в браузере. Связка «серверная обработка + браузерная подача» — то, ради чего этот движок и нужен.
Попробуй сам
DuckDB-WASM удобнее всего пощупать без единой строчки кода — через официальную браузерную оболочку DuckDB по адресу shell.duckdb.org. Она целиком построена на DuckDB-WASM: SQL исполняется в вашем браузере.
Задания:
- Откройте
shell.duckdb.orgи выполнитеSELECT version();. ЗатемFROM duckdb_extensions();— посмотрите, какие расширения доступны в WASM-сборке и чем список отличается от нативного DuckDB. - Найдите любой публичный Parquet-файл по HTTPS-URL и выполните над ним
SELECT count(*)и агрегирующий запрос сGROUP BY. Откройте вкладку Network в DevTools и убедитесь, что браузер скачал не весь файл, а только диапазоны байт (HTTP range requests). - Выполните
ATTACH 'opfs://test.db' AS db;, создайте в ней таблицу черезCREATE TABLE db.t AS SELECT range AS id FROM range(1000);, затем перезагрузите вкладку, снова сделайтеATTACH 'opfs://test.db'и проверьте, что таблица на месте. - Сравните
SET checkpoint_thresholdсо значениями по умолчанию и'0KB': вставьте в обоих режимах партию строк и подумайте, в каком случае состояние гарантированно на диске сразу после каждого statement.