Синхронная природа localStorage и асинхронная обёртка

Браузерное хранилище localStorage реализовано как синхронный механизм ключ–значение, выполняющий операции непосредственно в основном потоке JavaScript. Каждое обращение к чтению или записи блокирует выполнение кода до завершения операции, что делает поведение предсказуемым, но создаёт потенциальные проблемы производительности.

Синхронная модель localStorage

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

Ключевые особенности:

  • Синхронные операции чтения и записи
  • Блокировка main thread на время выполнения операции
  • Хранение только строк (все данные приводятся к string)
  • Отсутствие транзакций
  • Простая модель ошибок (чаще всего исключения при переполнении квоты)

Пример синхронной записи и чтения:

localStorage.setItem('user', JSON.stringify({ name: 'Alex' }));
const user = JSON.parse(localStorage.getItem('user'));

Каждый вызов выполняется немедленно. Даже при небольших объёмах данных это может создавать заметные задержки в интерфейсе при частых операциях.

Последствия синхронности

Синхронная природа приводит к нескольким архитектурным ограничениям:

Блокировка основного потока

Любая операция записи или чтения останавливает выполнение JavaScript до завершения работы хранилища. При увеличении объёма данных или частых вызовах это может вызывать:

  • просадки FPS в интерфейсе
  • задержки реакции на пользовательские действия
  • джанк (jank) в анимациях

Непригодность для тяжёлых данных

При хранении больших структур данных:

for (let i = 0; i < 10000; i++) {
  localStorage.setItem(`key${i}`, JSON.stringify({ index: i }));
}

весь цикл выполняется в основном потоке, создавая заметную блокировку.

Отсутствие конкурентного доступа

Синхронная модель не поддерживает параллельные операции. Любая попытка “одновременных” записей фактически превращается в последовательное выполнение.


Асинхронная модель современных браузерных хранилищ

В отличие от localStorage, современные механизмы хранения, такие как IndexedDB, используют асинхронную модель. Операции выполняются вне основного потока и возвращают результат через callback или Promise.

Пример асинхронного подхода:

const request = indexedDB.open('app', 1);

request.onsucc ess = (event) => {
  const db = event.target.result;
  // операции выполняются через транзакции
};

Асинхронность позволяет:

  • не блокировать UI
  • выполнять тяжелые операции в фоне
  • масштабировать объём данных

Однако IndexedDB имеет сложный API, требующий управления транзакциями, курсорами и версиями схемы.


Появление localForage как абстракции

localForage решает ключевую проблему: необходимость унифицированного API поверх разных механизмов хранения с разной природой (синхронной и асинхронной).

Главная идея заключается в предоставлении единого асинхронного интерфейса, независимо от того, какой драйвер используется внутри:

  • IndexedDB (асинхронный)
  • WebSQL (устаревший, асинхронный)
  • localStorage (синхронный, но обёрнутый)

Асинхронная обёртка над синхронным localStorage

Наиболее интересный технический момент заключается в том, что синхронный localStorage превращается в асинхронный API.

Проблема унификации

Если один драйвер работает синхронно, а другой асинхронно, возникает несоответствие моделей:

  • localStorage: const v = getItem(key)
  • IndexedDB: getItem(key).then(...)

localForage решает это путём принудительной асинхронности интерфейса.


Как достигается асинхронность

Даже если операция выполняется синхронно внутри localStorage, результат всегда возвращается асинхронно через Promise или callback.

Упрощённая модель:

function getItem(key) {
  return new Promise((resolve) => {
    const value = localStorage.getItem(key);
    resolve(value);
  });
}

На практике используется более сложная логика с очередями задач, но принцип остаётся:

  • синхронная операция выполняется немедленно
  • результат откладывается в микротаску или макротаску
  • вызывающий код всегда получает Promise

Причины выбора асинхронного API

Единая асинхронная модель даёт несколько архитектурных преимуществ.

1. Предсказуемость API

Код, работающий с хранилищем, не зависит от выбранного драйвера:

await storage.setItem('key', value);
const result = await storage.getItem('key');

localForage гарантирует одинаковое поведение независимо от backend-а.


2. Защита от блокировки UI

Даже если используется localStorage, асинхронная обёртка предотвращает:

  • синхронную блокировку call stack
  • непредсказуемые лаги при массовых операциях

3. Масштабируемость к IndexedDB

Если приложение позже переключается на IndexedDB, код не требует переписывания. Меняется только драйвер, но не API.


Внутренняя модель драйверов

Архитектура localForage строится вокруг концепции драйверов:

  • localStorageWrapper
  • indexedDBWrapper
  • webSQLWrapper

Каждый драйвер реализует одинаковый интерфейс:

  • getItem
  • setItem
  • removeItem
  • clear
  • length

Но способы выполнения операций различаются.


Сравнение поведения синхронного и асинхронного пути

Операция localStorage IndexedDB через localForage
setItem мгновенно, блокирует поток неблокирующая Promise
getItem мгновенно Promise
removeItem мгновенно Promise
clear мгновенно Promise

Эмуляция очередей и event loop

При использовании синхронного backend-а важным элементом становится интеграция с event loop.

Даже если результат уже известен, он возвращается через асинхронный механизм:

  • Promise создаётся сразу
  • результат резолвится после текущего стека выполнения
  • вызывающий код не получает синхронного доступа

Это создаёт единое поведение независимо от драйвера.


Побочные эффекты унификации

Ложное ощущение асинхронности

При работе с localStorage через localForage может возникать иллюзия «настоящей» асинхронной работы, хотя внутри операция выполняется синхронно.

Это важно учитывать:

  • CPU-нагрузка не исчезает
  • блокировка просто смещается в момент вызова, но скрывается за Promise

Микрозадержка как компромисс

Асинхронная обёртка добавляет минимальную задержку:

  • Promise resolution
  • постановка в очередь event loop

Однако этот оверхед компенсируется предсказуемостью и единообразием API.


Практическая модель поведения

При последовательных операциях:

await storage.setItem('a', 1);
await storage.setItem('b', 2);
await storage.setItem('c', 3);

localForage гарантирует:

  • последовательное выполнение
  • отсутствие гонок между драйверами
  • единый порядок операций

Даже если backend синхронный, интерфейс остаётся асинхронным.


Итоговая архитектурная идея

Различие между localStorage и IndexedDB не только в объёме или скорости, но и в фундаментальной модели выполнения:

  • localStorage — синхронный, блокирующий, строковый
  • IndexedDB — асинхронный, транзакционный, структурированный

localForage объединяет эти модели, вводя единый асинхронный контракт, скрывающий различие между блокирующими и неблокирующими операциями за одинаковым API.