Серверный рендеринг (SSR) в JavaScript-экосистеме создаёт фундаментальное расхождение между окружениями выполнения: на сервере отсутствуют браузерные API, тогда как в клиенте они доступны в полном объёме. Библиотеки, ориентированные на хранение данных в браузере, такие как localForage, неизбежно сталкиваются с этой проблемой, поскольку их основной функционал опирается на IndexedDB, WebSQL и localStorage.
Серверная среда выполнения (Node.js или edge runtime без DOM) не
предоставляет объектов window, document,
localStorage, sessionStorage, IndexedDB и
других Web API. Любая попытка обратиться к ним напрямую приводит к
ошибке выполнения.
localForage внутри браузера использует адаптивный слой драйверов:
Однако все эти механизмы завязаны на глобальный объект
window. При SSR этот объект отсутствует, что делает
невозможной даже инициализацию библиотеки без дополнительных мер
защиты.
Полифилы в контексте SSR используются не для добавления недостающих возможностей браузера в полном объёме, а для создания минимально достаточного API, позволяющего кодовой базе выполниться без ошибок.
В случае localForage выделяются три основных подхода:
Каждый подход решает задачу совместимости, но с разным уровнем функциональной полноты.
Самый простой способ устранить падение SSR — предоставить объект, имитирующий интерфейс localForage, но не выполняющий реального хранения данных.
Такой stub реализует те же методы:
getItemsetItemremoveItemclearlengthkeykeysПростейшая реализация выглядит следующим образом:
const noopStorage = {
getItem: async () => null,
setItem: async () => {},
removeItem: async () => {},
clear: async () => {},
length: async () => 0,
key: async () => null,
keys: async () => []
};
Такой объект полностью устраняет падения, но не сохраняет состояние между вызовами. Он используется исключительно как временная заглушка на сервере.
Более функциональный подход заключается в создании временного хранилища в памяти процесса. Оно сохраняет данные в течение жизни запроса или всего SSR-процесса.
const memoryStore = new Map();
const memoryDriver = {
getItem: async (key) => {
return memoryStore.has(key) ? memoryStore.get(key) : null;
},
setItem: async (key, value) => {
memoryStore.set(key, value);
},
removeItem: async (key) => {
memoryStore.delete(key);
},
clear: async () => {
memoryStore.clear();
},
length: async () => memoryStore.size,
key: async (index) => Array.from(memoryStore.keys())[index] || null,
keys: async () => Array.from(memoryStore.keys())
};
Данный подход уже позволяет моделировать поведение localForage более реалистично, однако имеет важное ограничение: данные не переживают перезапуск процесса и не синхронизируются между запросами.
Ключевая стратегия интеграции localForage в SSR-архитектуру заключается в разделении инициализации по окружению выполнения.
Типичная проверка выполняется через определение наличия
window:
import localForage from "localforage";
const isBrowser = typeof window !== "undefined";
export const storage = isBrowser
? localForage.createInstance({
name: "app_storage"
})
: null;
Однако такой подход создаёт проблему: серверный код должен учитывать
возможность null, что усложняет архитектуру.
Более устойчивый вариант — подстановка fallback-драйвера:
import localForage from "localforage";
const isBrowser = typeof window !== "undefined";
const safeStorage = isBrowser
? localForage.createInstance({ name: "app_storage" })
: {
getItem: async () => null,
setItem: async () => {},
removeItem: async () => {},
clear: async () => {},
length: async () => 0,
key: async () => null,
keys: async () => []
};
export default safeStorage;
Такой подход позволяет использовать единый интерфейс без проверки окружения в каждом модуле.
В изоморфных приложениях, использующих React, Vue или Svelte, важной проблемой становится расхождение между серверным HTML и клиентским состоянием после гидратации.
localForage может усугубить проблему, если:
В результате происходит изменение DOM после гидратации, что вызывает warning о несоответствии разметки.
Для предотвращения этого применяется стратегия отложенной загрузки данных:
const isBrowser = typeof window !== "undefined";
export async function getStoredValue(key) {
if (!isBrowser) return null;
return await localForage.getItem(key);
}
Другой подход — загрузка данных только после монтирования UI-слоя, исключая их влияние на серверный рендер.
В экосистеме Next.js часто используется динамический импорт для предотвращения выполнения browser-only кода на сервере:
import dynamic from "next/dynamic";
const StorageComponent = dynamic(
() => import("../components/StorageComponent"),
{ ssr: false }
);
Внутри компонента можно безопасно использовать localForage без проверки окружения.
Для крупных приложений используется абстракция над localForage, скрывающая различия между SSR и браузером:
class UniversalStorage {
constructor(driver) {
this.driver = driver;
}
async get(key) {
return this.driver.getItem(key);
}
async set(key, value) {
return this.driver.setItem(key, value);
}
async remove(key) {
return this.driver.removeItem(key);
}
}
const isBrowser = typeof window !== "undefined";
const driver = isBrowser
? localForage.createInstance({ name: "app" })
: memoryDriver;
export const storage = new UniversalStorage(driver);
Такой слой позволяет полностью скрыть различия между окружениями и не усложнять бизнес-логику.
localForage может выполнять внутреннюю инициализацию драйверов при создании экземпляра. В SSR это становится проблемой, если импорт модуля происходит на сервере.
Решение — ленивое создание экземпляра:
let instance = null;
export function getStorage() {
if (instance) return instance;
if (typeof window === "undefined") {
instance = memoryDriver;
} else {
instance = localForage.createInstance({
name: "lazy_storage"
});
}
return instance;
}
Такой подход предотвращает выполнение browser-specific кода при загрузке модуля.
При использовании in-memory драйвера в серверной среде важно учитывать, что Node.js процесс может обслуживать множество запросов одновременно. Это создаёт риск утечки состояния между пользователями.
Для устранения этой проблемы используется фабрика экземпляров:
export function createSSRStorage() {
const store = new Map();
return {
getItem: async (k) => store.get(k) ?? null,
setItem: async (k, v) => store.set(k, v),
removeItem: async (k) => store.delete(k),
clear: async () => store.clear(),
length: async () => store.size,
key: async (i) => Array.from(store.keys())[i] ?? null,
keys: async () => Array.from(store.keys())
};
}
Каждый запрос получает собственное изолированное хранилище, что предотвращает коллизии данных.
localForage поддерживает концепцию драйверов, что позволяет подменять механизм хранения. Это открывает возможность построения гибридной архитектуры:
Такая модель обеспечивает единый API при разных условиях выполнения.
const config = {
name: "hybrid_app",
driver: isBrowser
? [
localForage.INDEXEDDB,
localForage.WEBSQL,
localForage.LOCALSTORAGE
]
: []
};
На сервере список драйверов намеренно пуст, что исключает попытки обращения к Web API.
В современных сборщиках (Vite, Webpack, esbuild) модули могут выполняться частично при импорте. Поэтому критически важно избегать побочных эффектов на верхнем уровне:
// небезопасно
const storage = localForage.createInstance({ name: "app" });
// безопасно
export function getStorage() {
if (typeof window === "undefined") return memoryDriver;
return localForage.createInstance({ name: "app" });
}
Такой паттерн предотвращает преждевременное обращение к IndexedDB.
В реальных архитектурах SSR редко используется один подход. Чаще всего применяется комбинация:
Так формируется стабильная изоморфная модель хранения данных, в которой localForage выступает исключительно как клиентский слой, а серверный уровень работает через эмуляцию API или полную абстракцию хранилища.