Binary data в браузере представляют собой один из ключевых сценариев
работы с современными веб-приложениями, особенно когда требуется хранить
изображения, аудио, шифрованные блоки данных, результаты вычислений
WebGL, файлы или любые структуры, выходящие за пределы строк и JSON. В
экосистеме браузера такие данные чаще всего представлены через
ArrayBuffer и типизированные массивы
(TypedArray), которые обеспечивают эффективную работу с
двоичными буферами фиксированного размера.
localForage, как абстракция над IndexedDB, WebSQL и localStorage, предоставляет единый интерфейс хранения, но при работе с бинарными данными возникает ряд особенностей, связанных с сериализацией, клонированием и выбором драйвера.
В JavaScript бинарные данные не являются «первоклассным» типом в привычном смысле строки или числа. Вместо этого используется набор структур:
ArrayBuffer — сырой блок памяти фиксированного
размераTypedArray — представления над буфером
(Uint8Array, Int16Array,
Float32Array и др.)DataView — низкоуровневый доступ с управлением порядком
байтовTypedArray не хранит данные сам по себе, а лишь
предоставляет «окно» в ArrayBuffer. Это критично для
понимания того, как такие структуры сериализуются при сохранении через
localForage.
localForage использует разные драйверы в зависимости от окружения, но
наиболее важный для бинарных данных — IndexedDB. Именно он поддерживает
хранение Blob, ArrayBuffer и частично
TypedArray без преобразования в строки.
Базовый сценарий:
import localforage from "localforage";
const buffer = new ArrayBuffer(8);
const view = new Uint8Array(buffer);
view[0] = 255;
view[1] = 128;
await localforage.setItem("bin", buffer);
const result = await localforage.getItem("bin");
В большинстве современных браузеров ArrayBuffer
сохраняется напрямую, без преобразования в JSON или Base64, если
используется IndexedDB. Это обеспечивает высокую производительность и
минимальные накладные расходы.
localForage принимает не только ArrayBuffer, но и любые
TypedArray. Однако важно понимать, что сохраняется не сам
объект представления, а его underlying buffer.
const data = new Uint16Array([1000, 2000, 3000]);
await localforage.setItem("typed", data);
При сохранении происходит извлечение data.buffer. При
восстановлении возвращается именно ArrayBuffer, а не
Uint16Array. Это означает, что типизация теряется и должна
быть восстановлена вручную.
После получения данных из storage необходимо восстановить конкретный тип представления:
const buffer = await localforage.getItem("typed");
const restored = new Uint16Array(buffer);
console.log(restored[0]); // 1000
Это ключевой момент архитектуры: localForage не хранит метаданные о типе массива, поэтому восстановление всегда является явной операцией.
TypedArray включает множество типов, и выбор зависит от задачи:
Uint8Array — универсальный формат байтов (изображения,
файлы)Int16Array — аудио с 16-bit PCMFloat32Array — WebGL и аудиосигналыUint32Array — индексы, hash-структурыПример хранения Float32 данных:
const signal = new Float32Array([0.1, 0.5, -0.3, 0.8]);
await localforage.setItem("signal", signal.buffer);
const loaded = await localforage.getItem("signal");
const restoredSignal = new Float32Array(loaded);
localForage может переключаться между IndexedDB, WebSQL и localStorage. Это критически влияет на работу с бинарными данными.
ArrayBuffer и BlobСледствие: одинаковый код может вести себя по-разному в зависимости от драйвера.
IndexedDB использует алгоритм структурного клонирования (structured clone algorithm). Он поддерживает:
Но важно, что TypedArray при сохранении может
восстанавливаться как buffer без сохранения типа view.
Это объясняет поведение:
const original = new Uint8Array([1, 2, 3]);
await localforage.setItem("x", original);
const loaded = await localforage.getItem("x");
console.log(loaded instanceof Uint8Array); // false
console.log(loaded instanceof ArrayBuffer); // true
TypedArray может ссылаться на часть буфера:
const buffer = new ArrayBuffer(10);
const full = new Uint8Array(buffer);
const slice = new Uint8Array(buffer, 2, 4);
slice.set([10, 20, 30, 40]);
При сохранении через localForage сохраняется весь
ArrayBuffer, а не подмассив. Это означает, что логика
«срезов» должна быть восстановлена отдельно.
Поскольку ArrayBuffer передается по ссылке, возможны
побочные эффекты при изменении исходного объекта после сохранения:
const buffer = new ArrayBuffer(4);
const view = new Uint8Array(buffer);
await localforage.setItem("safe", buffer);
view[0] = 99;
В IndexedDB данные уже сериализованы, поэтому изменения после
setItem не влияют на сохраненное значение, но в некоторых
драйверах или при буферизации это может отличаться. Поэтому безопасной
практикой является явное копирование:
const copy = buffer.slice(0);
await localforage.setItem("safe", copy);
TypedArray используется в сценариях, где критична скорость:
localForage добавляет минимальный overhead поверх IndexedDB, но важно учитывать:
ArrayBuffer блокирующая для
транзакцииПример пакетной записи:
const chunks = [];
for (let i = 0; i < 10; i++) {
const buf = new Uint8Array(1024);
crypto.getRandomValues(buf);
chunks.push(buf.buffer);
}
await Promise.all(
chunks.map((chunk, i) => localforage.setItem(`chunk_${i}`, chunk))
);
При работе с бинарными данными часто требуется хранить не только сам буфер, но и метаданные: длину, тип, смещение.
Один из подходов — комбинированное хранение:
const record = {
type: "float32",
length: data.length,
buffer: data.buffer
};
await localforage.setItem("record", record);
Но здесь возникает ограничение: localForage сериализует объект
целиком, и ArrayBuffer останется валидным только в
IndexedDB-драйвере.
Более универсальный подход — разделение:
await localforage.setItem("meta", {
type: "float32",
length: data.length
});
await localforage.setItem("data", data.buffer);
TypedArray часто является мостом между localForage и другими API:
getImageData().data → Uint8ClampedArrayПример сохранения изображения:
const imageData = ctx.getImageData(0, 0, 100, 100);
await localforage.setItem("img", imageData.data.buffer);
И восстановление:
const buffer = await localforage.getItem("img");
const pixels = new Uint8ClampedArray(buffer);
const imageData = new ImageData(pixels, 100, 100);
ctx.putImageData(imageData, 0, 0);
Несмотря на универсальность, существуют ограничения:
SharedArrayBuffer) в некоторых режимахТакже важно учитывать различие между:
Uint8Array)ArrayBuffer)localForage оперирует именно buffer-уровнем, игнорируя view-метаданные.
Для устойчивой архитектуры хранения обычно применяются следующие принципы:
ArrayBuffer, а не TypedArrayUint8Array как универсальный транспортный
слойПример универсального паттерна:
function saveTyped(key, typedArray) {
return localforage.setItem(key, {
buffer: typedArray.buffer,
type: typedArray.constructor.name
});
}
function loadTyped(record) {
const map = {
Uint8Array,
Uint16Array,
Float32Array
};
return new map[record.type](record.buffer);
}
Такая схема делает работу с binary data предсказуемой независимо от драйвера и окружения.