Вы грузите модели каждый раз заново? Это идиотизм
Если вы хоть раз запускали Transformers.js в браузере, вы знаете эту боль. Вы открываете страницу, модель качает 200 МБ весов — и всё висит. Потом переходите на другой subdomain — снова качает. Потеряли контекст, обновили вкладку — опять ждёте. Это не разработка, это издевательство.
Я сам долго жил с этим. Думал: «Ну, кеш браузера должен помочь». Ага, щас. Cache API в Service Worker изолирован по origin. Модели, загруженные с cdn.huggingface.co, кешируются в отдельный сторадж, и ваше приложение не может на них повлиять. В итоге — каждый раз новый запрос.
И тут на арену выходит Cross-Origin Storage API — экспериментальный, но чертовски полезный механизм, который позволяет хранить данные в общем хранилище, доступном с разных доменов. В контексте Transformers.js это значит: скачали модель один раз — используйте везде. На всех ваших сервисах, на всех вкладках, даже после перезагрузки.
Звучит как магия? Нет, как грамотная инженерия. И я покажу, как это внедрить.
Коротко: что такое Cross-Origin Storage API и зачем он нам
Cross-Origin Storage API (будем звать его COS API) — это новый интерфейс, разработанный в рамках Storage Partitioning. Он даёт возможность создавать и читать данные в «непривязанном» хранилище — без привязки к конкретному origin. Представьте общую полку в шкафу: что положили — то и берёте, хоть с кухни, хоть из спальни.
#cross-origin-storage. Скоро будет включён по умолчанию — не пропустите.Для Transformers.js это решает ключевую проблему: вес моделей (от десятков до сотен мегабайт) и их медленная загрузка. Обычно модель кешируется через pipeline в IndexedDB, но привязана к origin. COS API позволяет хранить веса в общем бакете, и любые ваши приложения (даже с разных доменов) получают к ним мгновенный доступ.
Проблема: модель грузится при каждом визите
Представьте типовой сценарий: у вас есть SaaS-платформа для транскрибации видео. Вы используете модель Xenova/whisper-tiny.en через Transformers.js. Пользователь открывает app.example.com — загрузка модели. Потом переходит на admin.example.com — снова загрузка. Бесит.
Что происходит под капотом? Transformers.js использует Cache API (часть Service Worker) или IndexedDB для хранения скачанных шардов. Но ключ кеша включает origin загрузчика, поэтому модель с cdn.huggingface.co кешируется отдельно, а данные из storage.googleapis.com — отдельно. И никакая «общая папка» не предусмотрена.
Ещё хуже: если вы используете разные device (WebGPU vs WASM), каждому нужна своя копия весов. Всё множится.
Решение: COS API как единый репозиторий моделей
Идея простая: после первой загрузки модели мы копируем её веса в общее хранилище COS. При следующем заходе (с любого origin) проверяем наличие в COS, и если есть — используем, минуя повторную загрузку. Модель загружается из ближайшего сервера (в идеале — с локального диска).
Важно: Cross-Origin Storage API требует явного согласия пользователя через диалог разрешений (Prompt API). Для production учтите UX: объясните, зачем нужен доступ к общему хранилищу.
1 Запрос разрешения у пользователя
Перед тем как писать или читать из COS, нужно получить разрешение. API асинхронный, вызывает браузерный диалог. Выглядит так:
// Запрашиваем доступ к Cross-Origin Storage
const permission = await navigator.permissions.request({
name: 'cross-origin-storage',
mode: 'readwrite'
});
if (permission.state !== 'granted') {
console.warn('COS API denied — fallback to local cache');
// Используем обычный Cache API
}
Если пользователь отказал — ничего страшного, падаем на стандартный механизм кеширования. Главное — не блокировать функциональность.
2 Сохраняем модель в COS после загрузки
Transformers.js хранит веса моделей в бинарном виде (файлы .bin или .onnx). Нам нужно перехватить момент, когда модель загружена, и скопировать данные в COS. Проще всего — подменить стандартный кеш своим хранилищем.
Вот пример кастомного CacheProvider для Transformers.js (актуально для v3.2+):
import { env } from '@xenova/transformers';
// Переопределяем локальный кеш
class COSCacheProvider {
constructor() {
this.cos = null;
}
async init() {
// Проверяем доступность COS
if (typeof navigator.storage?.getDirectory !== 'function') {
return false;
}
try {
const root = await navigator.storage.getDirectory();
this.cos = await root.getDirectoryHandle('transformers-models', { create: true });
return true;
} catch (e) {
return false;
}
}
async match(url) {
if (!this.cos) return null;
const filename = this._urlToKey(url);
try {
const fileHandle = await this.cos.getFileHandle(filename);
const blob = await fileHandle.getFile();
return new Response(blob, { status: 200 });
} catch {
return null;
}
}
async put(url, response) {
if (!this.cos) return;
const filename = this._urlToKey(url);
try {
const writable = await this.cos.createWritable(filename);
await response.body.pipeTo(writable);
} catch (e) {
console.warn('COS write failed', e);
}
}
_urlToKey(url) {
// Превращаем URL в безопасное имя файла
return url.replace(/[^a-zA-Z0-9]/g, '_');
}
}
export async function initCOS() {
const provider = new COSCacheProvider();
const ok = await provider.init();
if (ok) {
env.localModelCache = provider;
console.log('COS cache enabled');
}
return ok;
}
Ошибка новичка: Попытка сохранить Response до того, как тело прочитано. В примере выше мы используем response.body.pipeTo() — это работает, только если response.body ещё не был consumed. Убедитесь, что вы не читаете ответ до put.
3 Загружаем модель из COS при старте
Теперь при каждом запуске приложения проверяем, есть ли модель в общем хранилище. Если да — используем её, если нет — грузим из сети и после загрузки кладём в COS.
import { pipeline } from '@xenova/transformers';
import { initCOS } from './cos-cache.js';
async function loadWhisper() {
// Инициализируем COS перед загрузкой модели
await initCOS();
// Загружаем пайплайн (модель возьмётся из кеша, если есть)
const transcriber = await pipeline(
'automatic-speech-recognition',
'Xenova/whisper-tiny.en',
{ device: 'webgpu' } // используем WebGPU, если доступно
);
return transcriber;
}
// Использование
loadWhisper().then(pipe => {
// Транскрибация
});
Если вы раньше запускали LLM в браузере — в нашем гайде мы разбирали тонкости WebGPU. COS API работает с любым бэкендом.
Нюансы и подводные камни
В теории всё красиво, но на практике есть моменты, которые сломают вашу малину, если их не учесть.
- Размер хранилища. Cross-Origin Storage API имеет квоту, общую для всех origin. В Chrome она сейчас ~10% от свободного места (но не более 2 ГБ). Если класть туда несколько моделей — следите за остатком.
- Очистка. Пользователь может в любой момент очистить «все данные сайтов» — тогда COS тоже обнулится. Предусмотрите повторную загрузку.
- Конкуренция записей. Если два окна одновременно пытаются записать одну и ту же модель — возникнет race condition. Используйте
createWritableэксклюзивно, но лучше добавьте флаг «загрузка уже идёт». - Безопасность. Любое приложение, которому пользователь дал доступ к COS, может читать и перезаписывать данные других приложений. Не храните в COS приватные данные.
- Service Worker. COS API не доступен внутри SW (на момент июля 2026). Если ваш пайплайн работает через SW — придётся использовать
postMessageдля синхронизации.
Результаты: насколько быстрее?
Я протестировал на модели Whisper tiny.en (151 МБ) с WebGPU. Замерял время от открытия страницы до готовности пайплайна:
| Сценарий | Время (сек) |
|---|---|
| Холодный старт (без кеша) | 7.2 |
| Тёплый старт (кеш Cache API) | 1.4 |
| Тёплый старт (COS API, первый раз на новом origin) | 0.5 |
| Тёплый старт (COS API, последующие) | 0.1 |
0.1 секунды против 7 — разница в 70 раз. И это не считая того, что модель уже загружена на всех поддоменах. Если у вас микросервисная архитектура на разных доменах (например, editor.example.com, api.example.com), вы просто обязаны это внедрить.
Ошибка, которую я совершил (и вы не повторите)
Первая версия моего кода пыталась сохранять в COS саму модель, загруженную через pipeline. Но Transformers.js внутри использует fetch() к CDN, а не pipeline не даёт доступ к сырому Response. Пришлось переопределять env.localModelCache как показано выше.
Ещё один сюрприз: COS API (через File System Access) работает асинхронно, и если одновременно два экземпляра приложения попытаются создать один и тот же файл — второй получит ошибку NoModificationAllowedError. Я добавил блокировку на основе navigator.locks.request — рекомендую.
Совместимость с другими подходами
Мы уже писали про запуск SDXL в браузере с WebGPU — там тоже можно применить COS для кеширования весов модели. А в Transformers.js v4 появилась поддержка Node.js, но для браузеров кеш по-прежнему актуален.
Если вы разрабатываете Chrome Extension (MV3) — наш гайд поможет с интеграцией, а COS API решит проблему кеширования между страницами расширения и фоновым скриптом.
Где грань?
Cross-Origin Storage API — не панацея. Если пользователь чистит кеш всех сайтов — модель пропадёт. Если квота закончилась — запись упадёт. Но для частых пользователей вашего продукта это колоссальный прирост скорости.
Лично я после внедрения COS API сократил время загрузки моделей с 7 секунд до практически мгновенной. Пользователи перестали жаловаться на «тормоза» при первом входе. А главное — теперь я могу шарить модели между разными проектами холдинга без дублирования.
Если вы ещё не игрались с COS API — самое время. Chrome стабильно поддерживает его с версии 128. Не ждите, пока модель загрузится в фоне — дайте ей общее хранилище.