Назад к блогу

DuckDB-Wasm и OPFS: полноценная база данных прямо в браузере

DuckDB-Wasm и OPFS: полноценная база данных прямо в браузере

DuckDB-Wasm позволяет запускать полноценную аналитическую СУБД прямо в браузере, а OPFS даёт ей постоянное хранилище, так что база данных сохраняется между перезагрузками страницы. В статье разбирается, как устроены сборки WebAssembly, конфигурация и преобразование результатов запросов в Arrow-батчи — полезно всем, кто хочет добавить локальную аналитику в веб-приложение без серверной инфраструктуры.

DuckDB-Wasm запускает аналитическую СУБД в браузере как модуль WebAssembly, а OPFS даёт ей настоящую персистентную файловую систему. Разбираем механику: как выбирается вариант сборки, как устроена конфигурация, как результат запроса превращается в Arrow-батчи и как база переживает перезагрузку страницы.

Выбор и инстанцирование бандла

DuckDB-Wasm поставляется не одним модулем, а несколькими, скомпилированными под разные наборы возможностей WebAssembly. Два основных варианта — mvp и eh. mvp соответствует WebAssembly MVP, а eh — сборке с поддержкой exception handling. Разница проявляется в том, как обрабатываются исключения: в MVP-сборке компиляторный тулчейн Emscripten эмулирует их через JavaScript, что порождает множественные вызовы через JavaScript-хук; в eh-сборке используются WebAssembly exceptions без таких вызовов.

Две сборки нужны потому, что разные возможности появляются в браузерах с разной скоростью — складывается fractured space of post-MVP functionality. Exception handling уже включён по умолчанию в Chrome 95, но не везде. Поэтому поставляются несколько WebAssembly-модулей под разные наборы возможностей, и лучший выбирается адаптивно с помощью динамических проверок браузера. В конфигурации mvp использует duckdb-mvp.wasm и duckdb-browser-mvp.worker.js, а eh — duckdb-eh.wasm и duckdb-browser-eh.worker.js.

Объект бандла содержит три поля: mainModule — путь к wasm-модулю, mainWorker — путь к основному воркер-скрипту, pthreadWorker — путь к воркеру для pthread. При инициализации вызывается db.instantiate(bundle.mainModule, bundle.pthreadWorker): instantiate поднимает базу из указанного wasm-модуля, а вторым аргументом получает путь к pthread-воркеру, который нужен для параллельных вычислений.

Есть два способа получить бандлы. duckdb.getJsDelivrBundles() возвращает готовый набор, из которого затем выбирается один через duckdb.selectBundle(JSDELIVR_BUNDLES). Ручной MANUAL_BUNDLES — это объект типа duckdb.DuckDBBundles, где для каждого варианта задаются mainModule и mainWorker с путями к файлам; в варианте asyncNextCOI дополнительно задаётся pthreadWorker, тогда как в mvp и eh он не указан.

При ручном хостинге одинаковое поведение на разных браузерах не гарантируется, потому что SharedArrayBuffer ограничены браузерами, а для их повторного включения сайт должен быть cross-origin-isolated. Без SharedArrayBuffers модуль WebAssembly может работать в отдельном web worker, но не может порождать дополнительные worker'ы для параллельных вычислений внутри одного экземпляра.

Обёртка воркера через Blob-URL

В примерах из README и документации Blob-URL формируется так: создаётся Blob с текстом importScripts("${bundle.mainWorker!}"); и типом text/javascript, затем URL.createObjectURL превращает его в URL, который передаётся в new Worker(worker_url). Обёртка нужна потому, что скрипты воркера должны быть same-origin, и URL воркера с CDN нельзя загрузить напрямую — Blob-URL обходит это ограничение. После await db.instantiate(bundle.mainModule, bundle.pthreadWorker) вызывается URL.revokeObjectURL(worker_url), что освобождает созданный Blob-URL: воркер уже создан и больше не нуждается в этом URL. В альтернативном варианте с MANUAL_BUNDLES обёртка не используется — там сразу const worker = new Worker(bundle.mainWorker!), потому что mainWorker указывает на локальный путь, а не на CDN.

Конфигурация: DuckDBConfig

Конфигурация читается из JSON через WebDBConfig::ReadFrom. Поле path задаёт путь к базе данных, по умолчанию ":memory:". Поле access_mode хранит необязательный числовой режим доступа. maximum_threads задаёт число потоков: 4 при включённой фиче THREADS, иначе 1.

Поле query — это QueryConfig с настройками преобразования типов и интервалом опроса. query_polling_interval управляет частотой опроса в PollPendingQuery. Флаги cast_bigint_to_double, cast_timestamp_to_date, cast_duration_to_time64, cast_decimal_to_double включают соответствующие приведения типов. Поле filesystem — FileSystemConfig с allow_full_http_reads, force_full_http_reads и reliable_head_requests, влияющими на HTTP-чтение. allow_unsigned_extensions разрешает неподписанные расширения (по умолчанию false), arrow_lossless_conversion включает без потерь преобразование в Arrow (по умолчанию false), custom_user_agent задаёт пользовательский User-Agent (по умолчанию пустая строка). Поле useDirectIO читается в config.use_direct_io, но его назначение не раскрыто.

Режим доступа

accessMode при открытии базы может принимать значение READ_WRITE. Логика выбора режима в WebDB::Open такая: если путь равен :memory: или пустой строке, выбирается AccessMode::AUTOMATIC, иначе AccessMode::READ_ONLY. Если в конфигурации задан access_mode, он используется вместо этого значения. Выбранный режим затем передаётся в db_config.options.access_mode перед созданием duckdb::DuckDB.

Режимы обработки opfs://-путей

DuckDBOPFSConfig.fileHandling задаёт режим обработки opfs://-путей, встречающихся в SQL. При opfs: { fileHandling: 'auto' } DuckDB-Wasm сканирует каждый оператор на наличие литералов 'opfs://...' в одинарных кавычках, регистрирует эти файлы перед выполнением (создавая их и недостающие каталоги при необходимости) и освобождает хэндлы после. Этот режим действует только если сама база была открыта по opfs://-пути; без него каждый файл, кроме самой базы, приходится регистрировать вручную. Ручной режим — это режим по умолчанию: он требует больше кода, но избегает повторного получения OPFS access handle на каждом операторе, что накапливается в приложениях с множеством мелких запросов. Файл может удерживаться только одним хэндлом одновременно, поэтому рекомендуется снимать регистрацию через db.dropFile() перед тем, как другой коннекшн или экземпляр базы откроет эти файлы.

HTTP-настройки файловой системы

Настройки из DuckDBFilesystemConfig попадают в C++ через колбэки, зарегистрированные в webdb.cc. callback_reliable_head_requests записывает значение в webfs->Config()->duckdb_config_options.reliable_head_requests и вызывает webfs->IncrementCacheEpoch(). Поля allow_full_http_reads и force_full_http_reads читаются напрямую из filesystem_.config_->filesystem в WebFileSystem::AddMember.

В AddMember эти поля проверяются только для протоколов HTTP или S3: allowFullHttpReads добавляется в JSON, если filesystem_.config_->filesystem.allow_full_http_reads.value_or(true), forceFullHttpReads — если force_full_http_reads.value_or(true), а reliableHeadRequests — по значению duckdb_config_options.reliable_head_requests.

На стороне JS в runtime_browser.ts эти поля из JSON используются при открытии файла. Если !file.forceFullHttpReads && (file.reliableHeadRequests || !file.allowFullHttpReads), выполняется HEAD-запрос с заголовком Range: bytes=0-; при статусе 206 и наличии Content-Length выбирается стратегия range-запросов. Если HEAD-проверка не дала результата и file.allowFullHttpReads истинно, выполняется GET-запрос с Range: bytes=0-0; при статусе 206 и Content-Length равном 1 выбирается range-стратегия, иначе при статусе 200 файл скачивается целиком. Если file.forceFullHttpReads истинно, HEAD-проверка пропускается, и сразу используется полное чтение.

Интервал опроса запроса

DuckDBQueryConfig содержит необязательное поле queryPollingInterval — интервал опроса для запросов — и флаги преобразования типов castBigIntToDouble, castTimestampToDate, castDurationToTime64, castDecimalToDouble. В FetchQueryResults для потокового результата берётся значение queryPollingInterval (или значение по умолчанию DEFAULT_QUERY_POLLING_INTERVAL), засекается время before, и в цикле вызывается stream_result.ExecuteTask(). Цикл продолжается, пока результат не готов и прошедшее время меньше polling_interval:

} while (!ready && elapsed < polling_interval);

Если по истечении этого интервала результат так и не готов, возвращается статус повторной попытки DUCKDB_WASM_RETRY. Таким образом, queryPollingInterval задаёт максимальное время ожидания готовности результата в миллисекундах, после которого управление возвращается вызывающей стороне для повторного запроса.

Расширения и User-Agent

При открытии базы WebDB::Open читает JSON-конфигурацию через WebDBConfig::ReadFrom, где поле allowUnsignedExtensions (bool) сохраняется в config.allow_unsigned_extensions, а customUserAgent (строка) — в config.custom_user_agent. Затем создаётся duckdb::DBConfig db_config, и в него переносятся оба значения: db_config.SetOptionByName("allow_unsigned_extensions", config_->allow_unsigned_extensions) и db_config.options.custom_user_agent = config_->custom_user_agent.

allow_unsigned_extensions — это опция самого DuckDB, разрешающая загрузку неподписанных расширений, поэтому она передаётся через SetOptionByName. custom_user_agent — это поле options структуры DBConfig, которое относится к HTTP-клиенту: сразу после создания базы код проверяет config.http_util и при необходимости подменяет его на HTTPWasmUtil. То есть пользовательский User-Agent используется сетевым стеком, а не механизмом загрузки расширений.

S3-параметры как extension options

S3-параметры регистрируются как extension options через config.AddExtensionOption: для каждого задаётся имя, описание, тип (LogicalType::VARCHAR для строковых, LogicalType::BOOLEAN для reliable_head_requests), значение по умолчанию и callback. Callback каждого из этих параметров получает WebFileSystem через io::WebFileSystem::Get(), записывает значение в webfs->Config()->duckdb_config_options (соответствующее поле: s3_region, s3_access_key_id, s3_secret_access_key, s3_session_token, s3_endpoint, reliable_head_requests) и затем вызывает webfs->IncrementCacheEpoch().

IncrementCacheEpoch атомарно увеличивает cache_epoch_ (с переходом с максимума uint32_t на 1), что позволяет обнаруживать устаревшие fileInfoCaches из JS. writeS3Config формирует rapidjson-объект с полями region, endpoint, accessKeyId, secretAccessKey, sessionToken и reliable_head_requests.

Выполнение запроса и материализация результата

Цикл for await (const batch of conn.send(...)) получает результаты по одному чанку (порции строк) через FetchQueryResults. Если активного результата нет, это означает «нет активного результата».

Если тип результата — потоковый (STREAM_RESULT), то в цикле опроса выполняется очередной шаг задачи, и в зависимости от полученного StreamExecutionResult принимается решение:

  • при EXECUTION_ERROR запрос завершается ошибкой с текстом из current_query_result_->GetError();
  • при EXECUTION_CANCELLED — ошибкой о том, что выполнение отменено;
  • при CHUNK_READY или EXECUTION_FINISHED флаг ready становится истинным и происходит выход из цикла опроса;
  • при BLOCKED система дожидается задачи и возвращается DUCKDB_WASM_RETRY («повторить попытку»);
  • при NO_TASKS_AVAILABLE также возвращается DUCKDB_WASM_RETRY;
  • при CHUNK_NOT_READY цикл продолжается до истечения polling_interval.

Если по истечении интервала опроса флаг ready так и не стал истинным, возвращается DUCKDB_WASM_RETRY. Когда ready истинно, извлекается очередная порция: если при этом возникла ошибка, запрос завершается ошибкой; если порция пуста, результат сбрасывается и это означает конец потока; иначе порция конвертируется в Arrow-батч и сериализуется — это и есть «чанк готов».

Отмена и ожидание запроса

PollPendingQuery сначала проверяет флаг отмены: если запрос был отменён, операция завершается ошибкой «query was canceled»; если ожидающего запроса нет — ошибкой «no active pending query». Пока выполнение не завершено, при состояниях BLOCKED или NO_TASKS_AVAILABLE ожидание приостанавливается, а при RESULT_NOT_READY цикл продолжается до истечения polling_interval. Когда задача завершена (EXECUTION_FINISHED или RESULT_READY), результат извлекается, индекс оператора увеличивается; если это был последний оператор, результат становится потоковым, иначе запускается следующий оператор.

CancelPendingQuery сбрасывает ожидающий запрос только если он ещё не завершён и нет активного текущего результата: запрос помечается отменённым, ожидающий результат обнуляется, а список операторов очищается; иначе отмена не выполняется. FetchQueryResults при отсутствии активного результата ничего не выдаёт; для потокового результата при EXECUTION_CANCELLED запрос завершается ошибкой о том, что выполнение отменено до завершения, вероятно из-за выполнения другого запроса. Когда очередной чанк отсутствует (конец данных), FetchQueryResults сбрасывает текущий результат, схему и патченную схему и больше ничего не выдаёт.

Преобразование в Arrow

Схема Arrow создаётся при материализации результата: типы и имена результата преобразуются в Arrow-схему, затем схема импортируется и патчится с учётом конфигурации запроса. Каждый чанк результата превращается в record batch: чанк преобразуется в Arrow-массив, импортируется как record batch, патчится с учётом конфигурации запроса, после чего batch пишется в файл.

В потоковом режиме (FetchQueryResults) чанк сериализуется иначе: он так же преобразуется в Arrow-массив, импортируется как record batch и патчится с учётом конфигурации запроса, после чего record batch сериализуется с отключёнными потоками. Опции castBigIntToDouble, castTimestampToDate и castDecimalToDouble описаны в DuckDBQueryConfig как флаги приведения типов, и именно они применяются на шаге патчинга схемы и батча, то есть при формировании Arrow-представления результата.

Prepared statements

CreatePreparedStatement принимает текст SQL, подготавливает запрос, проверяет, не возникла ли ошибка, затем берёт очередной идентификатор, при достижении максимума счётчика сбрасывает его в 0, сохраняет подготовленный запрос под этим идентификатором и возвращает его.

RunPreparedStatement и SendPreparedStatement оба выполняют подготовленный запрос с переданными аргументами, но затем по-разному обрабатывают результат: первый материализует его целиком, второй — как потоковый. При выполнении запись ищется по идентификатору, при отсутствии возвращается ошибка KeyError с текстом "No prepared statement found with ID", аргументы парсятся как массив JSON и преобразуются в значения DuckDB (double, string, null, bool), после чего запрос выполняется. ClosePreparedStatement также ищет запись по идентификатору, при отсутствии возвращает KeyError с тем же текстом, а при наличии освобождает её и завершается успешно.

Файловая система и OPFS

assignOPFSRoot лениво получает корневой каталог OPFS: если BROWSER_RUNTIME._opfsRoot ещё не задан, он вызывает navigator.storage.getDirectory() и сохраняет результат. prepareFileHandles для протокола DuckDBDataProtocol.BROWSER_FSACCESS сначала вызывает assignOPFSRoot, затем для каждого пути пытается взять уже известный handle из BROWSER_RUNTIME._files или BROWSER_RUNTIME._preparedHandles и вернуть его с fromCached: true; иначе готовит новый handle.

prepareDBFileHandle для того же протокола формирует список из четырёх путей — самого dbPath и файлов ${dbPath}.wal, ${dbPath}.wal.checkpoint, ${dbPath}.wal.recovery — и передаёт их в prepareFileHandles. Вложенные директории создаются по принципу mkdir -p: путь обрезается по OPFS_PREFIX_LEN, разбивается по PATH_SEP_REGEX, последний элемент становится именем файла, а для каждой папки вызывается dirHandle.getDirectoryHandle(folder, { create: true }). Сам файл открывается через dirHandle.getFileHandle(fileName, { create: false }), а при NotFoundError — повторно с { create: true }, после чего fileHandle.createSyncAccessHandle() даёт синхронный handle, который кэшируется в BROWSER_RUNTIME._preparedHandles[path].

В opfs_util.ts isOPFSProtocol проверяет наличие подстроки, соответствующей REGEX_OPFS_PROTOCOL (/(opfs:\/\/\S*?)/g), через path.search(...) > -1, а searchOPFSFiles извлекает из текста все совпадения REGEX_OPFS_FILE (/'(opfs:\/\/\S*?)'/g) и возвращает первую группу каждого совпадения — то есть пути opfs://..., заключённые в одинарные кавычки.

Эпохи кэша

IncrementCacheEpoch увеличивает счётчик эпох кэша, чтобы JS мог обнаруживать устаревшие fileInfoCaches: он читает текущее значение cache_epoch_, вычисляет следующее (при достижении std::numeric_limits<uint32_t>::max() сбрасывает на 1, иначе curr_epoch + 1) и пытается записать его через compare_exchange_strong, повторяя цикл при неудаче. LoadCacheEpoch в C++ просто возвращает текущее значение cache_epoch_.load(std::memory_order_relaxed).

В JS getFileInfo берёт из _fileInfoCache запись по fileId и передаёт её cacheEpoch (или 0, если записи нет) в вызов duckdb_web_fs_get_file_info_by_id; если возвращённый n === 0, это означает «Epoch is up to date» и возвращается закэшированный объект, иначе строка JSON парсится и кэш обновляется. Аналогично getGlobalFileInfo передаёт BROWSER_RUNTIME._globalFileInfo?.cacheEpoch || 0 в duckdb_web_get_global_file_info и при n === 0 возвращает старый _globalFileInfo.

Изменение S3-конфигурации инвалидирует кэш, потому что writeS3Config формирует JSON с полями region, endpoint, accessKeyId, secretAccessKey, sessionToken из extension_options, а OpenFile для протокола S3 при открытии на запись сохраняет file->buffered_http_config_options_ = config_->duckdb_config_options, то есть конфигурация входит в состояние файла.

Стратегия чтения

Стратегия чтения выбирается в WebFileSystem::Read по полю file.data_protocol_. Для DataProtocol::BUFFER данные копируются напрямую из памяти через ::memcpy, для NODE_FS/BROWSER_FILEREADER/BROWSER_FSACCESS — через duckdb_web_fs_file_read, а для HTTP/S3 сначала вызывается file_hdl.ResolveReadAheadBuffer(file_guard): если он вернул буфер, чтение идёт через ra->Read с лямбдой reader, иначе — напрямую duckdb_web_fs_file_read.

ResolveReadAheadBuffer сначала возвращает уже установленный readahead_, затем ищет буфер по идентификатору потока в fs.readahead_buffers_, а если не нашёл — создаёт новый ReadAheadBuffer, вставляет его в карту и сохраняет указатель в readahead_. InvalidateReadAheads вызывается при Write и Truncate и проходит по всем readahead_buffers_, вызывая ra.second->Invalidate(file_id), чтобы сбросить устаревшие буферы для этого файла.

Три причины возврата буфера в комментарии к Read: источник данных не поддерживает HTTP range-запросы и allowFullHTTPRequests истинно; файл имеет протокол HTTP или S3 и был открыт с флагом записи; файл является нативным файлом, который не был найден, и происходит откат к пустому буферу.

Регистрация файлов и фактическая загрузка

RegisterFileURL в WebDB сначала проверяет наличие WebFileSystem, затем пытается удалить уже зарегистрированный файл через buffered_filesystem_->TryDropFile(file_name), чтобы освободить буфер; если файл ещё кем-то удерживается, возвращается ошибка. После этого удаляется старая запись из pinned_web_files_, а новый URL регистрируется в WebFileSystem через web_fs->RegisterFileURL(file_name, file_url, protocol), возвращённый handle сохраняется в pinned_web_files_.

В WebFileSystem::RegisterFileURL создаётся объект WebFile с protocol, ему присваиваются data_url_ = file_url, file_size_ = std::nullopt, last_modification_time_ = std::nullopt, и файл добавляется в files_by_id_, files_by_name_ и files_by_url_. RegisterFileBuffer в WebDB аналогично удаляет старый файл и старую запись из pinned_web_files_, затем вызывает web_fs->RegisterFileBuffer(file_name, std::move(data)) и регистрирует файл в buffered_filesystem_ с force_direct_io = true. В WebFileSystem::RegisterFileBuffer для уже существующего файла с протоколом NODE_FS, BROWSER_FSACCESS или BROWSER_FILEREADER файл закрывается через duckdb_web_fs_file_close и заменяется буфером; для HTTP, S3 или BUFFER протокол меняется на BUFFER и сохраняется data_buffer_.

Фактическая загрузка HTTP выполняется не в этих функциях, а в runtime_browser.ts: при openFile для протоколов HTTP/S3 отправляются XMLHttpRequest (HEAD с Range, затем GET с Range bytes=0-0) для определения размера и получения данных.

Обработка HTTP-источников

При открытии HTTP/S3-файла на чтение runtime_browser.ts сначала пытается определить поддержку range-запросов: если не установлен forceFullHttpReads и (reliableHeadRequests или не разрешены полные чтения), отправляется HEAD-запрос с заголовком Range: bytes=0-. Range-запрос считается поддержанным, если получен Content-Length и статус 206; тогда возвращается размер файла, смещение 0 и время модификации.

Если HEAD-запрос с range-заголовком выбрасывает исключение, ошибка сохраняется в переменной error, выводится предупреждение, и далее может быть предпринята попытка полного чтения. При разрешённых полных чтениях сначала выполняется GET с Range: bytes=0-0: если статус 206, Content-Length равен 1 и удалось определить предполагаемую длину, возвращается range-результат; если статус 200 и Content-Length совпадает с длиной из HEAD, файл целиком загружается в память и возвращается буфер. Если ни один из этих вариантов не сработал, выполняется обычный GET без range; при статусе 200 данные также буферизуются и возвращаются. Если после всех попыток сохранённая ошибка не равна null, выбрасывается ошибка чтения файла; иначе возвращается 0.

Для BROWSER_FILEREADER при отсутствии зарегистрированного handle и без флага FILE_FLAGS_NULL_IF_NOT_EXISTS выполняется fallback к пустому буферизованному файлу: выделяется буфер размером 1 байт, возвращается размер 1, указатель на буфер и время модификации 0.

Персистентность и жизненный цикл

Вызов db.open с path: 'opfs://analytics.duckdb' и accessMode: READ_WRITE открывает базу: префикс opfs:// заставляет файловую систему DuckDB-Wasm разрешать путь в приватной файловой системе origin, а не в in-memory Emscripten FS. Открытие создаёт сам файл базы и его .wal в OPFS; сборки начиная с 1.33.1-dev64.0 дополнительно создают два пустых вспомогательных файла .wal.checkpoint и .wal.recovery, используемых при чекпоинтинге. Результат — обычный .duckdb-файл с журналом предзаписи (write-ahead log) и чекпоинтами, который переживает перезагрузки страницы и перезапуски браузера.

Чекпоинт можно вызвать вручную через conn.query('CHECKPOINT'), либо один раз после подключения установить порог чекпоинта в ноль, тогда DuckDB делает чекпоинт после каждого оператора, что снижает пропускную способность записи, но снимает вопрос отслеживания батчей. Файл .duckdb является обычным файлом базы DuckDB: если извлечь его из OPFS и открыть через CLI или Python-клиент, он работает.

CHECKPOINT и корректное завершение

CHECKPOINT — это команда, которая запускается через conn.query('CHECKPOINT') и выполняет контрольную точку; в примере она вызывается после вставки строки. Если не нужно отслеживать батчи, можно один раз после подключения установить порог контрольных точек в ноль:

await conn.query(`SET checkpoint_threshold = '0KB'`);

Тогда DuckDB делает контрольную точку после каждого оператора, что снижает пропускную способность записи, но полностью снимает вопрос отслеживания батчей. Корректное завершение состоит из трёх шагов: await conn.query('CHECKPOINT'); await conn.close(); await db.terminate();.

Сброс состояния базы

WebDB::Reset не выполняет собственную очистку, а просто вызывает Open(). Внутри Open() после создания новой базы DuckDB выполняется блок сброса состояния старой базы: connections_.clear() удаляет все сохранённые соединения-сессии (записи, добавленные в Connect через connections_.insert), database_.reset() освобождает прежний объект базы DuckDB, а buffered_filesystem_ = nullptr сбрасывает указатель на буферизованную файловую систему. После этого сохраняется новое состояние: buffered_filesystem_ = buffered_fs_ptr; database_ = std::move(db);.

Функции FlushFiles и FlushFile не освобождают ресурсы, а лишь делегируют работу буферу страниц файлов: file_page_buffer_->FlushFiles() и file_page_buffer_->FlushFile(path). Метод Finish у ProgressBarCustom не освобождает ресурсы, а отправляет финальное сообщение о прогрессе и сбрасывает счётчики.

Что из этого следует на практике

  • Выбор бандла не ручной. Адаптивный подбор через selectBundle избавляет от необходимости угадывать, поддерживает ли браузер exception handling; eh-сборка быстрее за счёт отсутствия вызовов через JavaScript-хук, но mvp нужна там, где этой возможности ещё нет.
  • Обёртка воркера в Blob-URL обязательна при загрузке с CDN — скрипты воркера должны быть same-origin. При локальном хостинге (MANUAL_BUNDLES) обёртка не нужна, а URL.revokeObjectURL освобождает Blob сразу после db.instantiate.
  • Параллелизм зависит от SharedArrayBuffers. Без cross-origin-isolated сайта с нужными HTTP-заголовками модуль работает в одном воркере и не может порождать дополнительные для параллельных вычислений.
  • Режим доступа выбирается неявно. Для файловой базы по умолчанию получается READ_ONLY, для in-memory — AUTOMATIC; чтобы писать в opfs://-базу, нужно явно указать READ_WRITE.
  • fileHandling: 'auto' удобен, но дорог. Автоматическая регистрация opfs://-литералов из SQL избавляет от ручного кода, но повторно берёт OPFS access handle на каждом операторе — при множестве мелких запросов это накапливается. Ручной режим по умолчанию требует больше кода, но экономнее.
  • Файл удерживается одним хэндлом. Перед открытием зарегистрированного файла другим коннекшном или экземпляром базы нужно снять регистрацию через db.dropFile().
  • HTTP-чтение адаптируется к серверу. Сначала проверяется поддержка range-запросов через HEAD

Где смотреть в коде

Источники

Похожее