Работа с большими бинарными данными (Blob, ArrayBuffer)

При работе с локальными базами данных браузера часто возникает необходимость хранить не только текстовую информацию и числовые значения, но и крупные бинарные объекты. К таким данным относятся:

  • изображения;
  • аудиофайлы;
  • видеозаписи;
  • PDF-документы;
  • архивы;
  • модели машинного обучения;
  • результаты экспорта данных;
  • пользовательские вложения.

Поскольку Dexie.js представляет собой удобную надстройку над IndexedDB, библиотека позволяет работать с бинарными объектами практически так же, как с обычными JavaScript-структурами.

Наиболее распространёнными форматами бинарных данных являются:

  • Blob
  • ArrayBuffer
  • TypedArray
  • Uint8Array
  • DataView

Все эти структуры могут сохраняться в IndexedDB без дополнительного преобразования.


Объект Blob

Blob (Binary Large Object) представляет собой контейнер для хранения произвольного бинарного содержимого.

Создание Blob:

const blob = new Blob(
    ["Пример содержимого файла"],
    { type: "text/plain" }
);

Размер объекта:

console.log(blob.size);

MIME-тип:

console.log(blob.type);

Blob особенно удобен при работе с файлами, загружаемыми пользователем через элемент <input type="file">.

Пример:

const input = document.querySelector("#file");

input.addEventListener("change", event => {
    const file = event.target.files[0];

    console.log(file.name);
    console.log(file.size);
});

Объект File является наследником Blob, поэтому может сохраняться в Dexie без преобразований.


Сохранение Blob в Dexie.js

Структура базы данных:

import Dexie fr om "dexie";

const db = new Dexie("StorageDB");

db.version(1).stores({
    files: "++id, name"
});

Сохранение файла:

await db.files.add({
    name: file.name,
    data: file
});

Структура записи:

{
    id: 1,
    name: "image.jpg",
    data: Blob
}

Dexie автоматически передаст объект в IndexedDB через механизм структурированного клонирования.

Дополнительная сериализация не требуется.


Получение Blob из базы данных

Чтение объекта:

const fileRecord = await db.files.get(1);

Доступ к бинарным данным:

const blob = fileRecord.data;

Проверка:

console.log(blob instanceof Blob);

После извлечения данные остаются полноценным объектом Blob.


Отображение изображений из Blob

Одна из самых частых задач — хранение изображений.

Получение объекта:

const imageRecord = await db.files.get(1);

const url = URL.createObjectURL(
    imageRecord.data
);

Отображение:

img.src = url;

Полный пример:

const image = document.createElement("img");

image.src = URL.createObjectURL(
    imageRecord.data
);

document.body.appendChild(image);

После завершения использования рекомендуется освобождать память:

URL.revokeObjectURL(url);

Особенно это важно при работе с большими изображениями.


Сохранение ArrayBuffer

В ряде случаев бинарные данные удобнее хранить как массив байтов.

Получение ArrayBuffer:

const buffer = await file.arrayBuffer();

Сохранение:

await db.files.add({
    name: file.name,
    data: buffer
});

Чтение:

const record = await db.files.get(1);

const buffer = record.data;

Проверка:

console.log(
    buffer instanceof ArrayBuffer
);

Преобразование Blob в ArrayBuffer

Современные браузеры поддерживают прямой метод:

const buffer = await blob.arrayBuffer();

Ранее использовался FileReader:

function blobToBuffer(blob) {
    return new Promise((resolve, reject) => {

        const reader = new FileReader();

        reader.onl oad = () => {
            resolve(reader.result);
        };

        reader.oner ror = reject;

        reader.readAsArrayBuffer(blob);
    });
}

Сегодня предпочтителен первый вариант.


Преобразование ArrayBuffer в Blob

Обратная операция:

const blob = new Blob(
    [buffer],
    { type: "application/octet-stream" }
);

Для изображений:

const imageBlob = new Blob(
    [buffer],
    { type: "image/png" }
);

Для PDF:

const pdfBlob = new Blob(
    [buffer],
    { type: "application/pdf" }
);

TypedArray и Dexie

Нередко бинарные данные обрабатываются через типизированные массивы.

Пример:

const bytes = new Uint8Array([
    10,
    20,
    30,
    40
]);

Сохранение:

await db.files.add({
    data: bytes
});

Извлечение:

const record = await db.files.get(1);

console.log(record.data);

Также поддерживаются:

Int8Array
Uint8Array
Uint16Array
Uint32Array
Float32Array
Float64Array
BigInt64Array
BigUint64Array

IndexedDB сохраняет их без потери структуры.


Работа с большими файлами

Хотя IndexedDB поддерживает крупные объекты, существуют ограничения.

На практике размер зависит от:

  • браузера;
  • операционной системы;
  • свободного места на диске;
  • политики квотирования.

Например, изображения размером в несколько мегабайт обычно не вызывают проблем.

Однако хранение тысяч файлов по сотни мегабайт требует особого подхода.


Проблема полного чтения файла в память

Опасная ситуация:

const buffer =
    await hugeFile.arrayBuffer();

Если размер файла составляет:

500 МБ

то браузеру потребуется выделить как минимум 500 МБ оперативной памяти.

При нескольких одновременных операциях расход памяти возрастает многократно.

Для мобильных устройств это может приводить к:

  • зависаниям;
  • завершению вкладки;
  • принудительной очистке памяти браузером.

Использование Blob вместо ArrayBuffer

Blob работает ленивее.

Сохранение:

await db.files.add({
    file: blob
});

Получение:

const record =
    await db.files.get(id);

Фактическое чтение содержимого происходит только при необходимости.

Поэтому для крупных файлов Blob обычно предпочтительнее.


Chunking — разбиение файлов на части

При работе с очень большими объектами применяется стратегия chunking.

Файл разбивается на множество небольших блоков.

Пример структуры:

db.version(1).stores({
    files: "id",
    chunks: "[fileId+index]"
});

Метаданные файла:

{
    id: "file1",
    name: "video.mp4",
    chunkCount: 50
}

Фрагмент:

{
    fileId: "file1",
    index: 0,
    data: Blob
}

Разбиение файла на блоки

Размер блока:

const CHUNK_SIZE =
    1024 * 1024;

Один мегабайт.

Создание частей:

const chunks = [];

for (
    let offset = 0;
    offset < file.size;
    offset += CHUNK_SIZE
) {
    chunks.push(
        file.slice(
            offset,
            offset + CHUNK_SIZE
        )
    );
}

Каждый элемент массива является Blob.


Сохранение частей

for (
    let i = 0;
    i < chunks.length;
    i++
) {
    await db.chunks.add({
        fileId: "file1",
        index: i,
        data: chunks[i]
    });
}

Сохранение метаданных:

await db.files.put({
    id: "file1",
    name: file.name,
    chunkCount: chunks.length
});

Сборка файла обратно

Получение частей:

const chunks =
    await db.chunks
        .wh ere("fileId")
        .equals("file1")
        .sortBy("index");

Создание Blob:

const restoredFile =
    new Blob(
        chunks.map(
            chunk => chunk.data
        )
    );

Полученный объект идентичен исходному.


Потоковая обработка данных

Современные браузеры поддерживают Streams API.

Получение потока:

const stream = blob.stream();

Чтение:

const reader =
    stream.getReader();

Получение данных частями:

while (true) {

    const {
        done,
        value
    } = await reader.read();

    if (done) {
        break;
    }

    console.log(value);
}

Такой подход значительно снижает потребление памяти.


Хранение медиаконтента

Для фотографий обычно сохраняются:

{
    id,
    filename,
    mimeType,
    width,
    height,
    imageBlob
}

Для аудио:

{
    id,
    title,
    duration,
    audioBlob
}

Для видео:

{
    id,
    title,
    codec,
    videoBlob
}

Дополнительные метаданные позволяют быстро выполнять поиск без чтения самого файла.


Индексирование бинарных данных

Создавать индекс непосредственно по Blob невозможно.

Неправильно:

files:
"++id, data"

Поиск по содержимому бинарного объекта IndexedDB не поддерживает.

Поэтому индексируются только метаданные:

files:
"++id, name, type, createdAt"

Например:

await db.files
    .where("type")
    .equals("image/png")
    .toArray();

Контроль занимаемого пространства

Для мониторинга размера хранилища используется Storage API.

Получение статистики:

const estimate =
    await navigator.storage.estimate();

Результат:

console.log(
    estimate.usage
);

Лимит:

console.log(
    estimate.quota
);

Возможный вывод:

Usage: 120 MB
Quota: 2 GB

Это позволяет контролировать заполнение локального хранилища.


Удаление крупных объектов

Удаление файла:

await db.files.delete(id);

Если используется chunking:

await db.transaction(
    "rw",
    db.files,
    db.chunks,
    async () => {

        await db.files.delete(id);

        await db.chunks
            .where("fileId")
            .equals(id)
            .delete();
    }
);

Транзакция гарантирует согласованность данных.


Практические рекомендации по хранению бинарных данных

Blob предпочтительнее для крупных файлов, поскольку позволяет избежать лишнего копирования данных в память.

ArrayBuffer удобен для обработки содержимого, когда требуется анализировать байты, изменять структуру файла или выполнять криптографические операции.

TypedArray подходит для низкоуровневой работы с бинарными форматами, сетевыми протоколами и мультимедийными данными.

Метаданные должны храниться отдельно от содержимого файла, чтобы поиск и фильтрация выполнялись по индексируемым полям.

Для объектов размером в десятки и сотни мегабайт рекомендуется chunking, позволяющий уменьшить нагрузку на память и повысить устойчивость операций чтения и записи.

Контроль объёма хранилища через Storage API помогает предотвращать переполнение квоты браузера, особенно в офлайн-приложениях, где количество локально сохраняемых файлов постоянно растёт.

Транзакционное удаление файлов и их фрагментов обеспечивает целостность базы данных, предотвращая накопление неиспользуемых бинарных блоков после удаления записей.