Зачем вам локальный AI в расширении, если есть облака?
Сценарий: вы пишете Chrome-расширение для суммаризации страниц, авто-ответов на почте или офлайн-переводчика. Отправлять каждое нажатие на сервер? Дорого, медленно, а главное — приватность летит к чертям. Chrome Store уже завален расширениями, которые шлют ваш текст на неизвестные API. Пользователи (особенно корпоративные) требуют он-девайс.
С другой стороны — Manifest V3. Гугл убил background pages, ввел service_worker, ограничил eval и кучу всего. Старые расширения на chrome.extension.getBackgroundPage() теперь legacy. Но именно MV3 делает локальный AI жизнеспособным: вы получаете изолированный worker без доступа к DOM, который может грузить модели и считать f32 матрицы без риска для вкладок.
До 2025 года задача «запустить BERT в расширении» требовала костылей с WebAssembly и ручным умножением. Сейчас — просто npm install @xenova/transformers (уже v4 с WebGPU-ускорением). Но если просто воткнуть библиотеку в popup — убьете память и заблокируете UI. Нужна правильная архитектура.
Ключевая идея: Модель живет в service worker'e, общается с вкладками через chrome.runtime.sendMessage. Popup — только тонкий интерфейс. Инференс — фоновая задача с уведомлениями.
Разберем по шагам, как собрать расширение, которое запускает Gemma 4 (или Qwen 2.5) прямо в браузере, не отправляя данные в облако.
Шаг 1: Структура проекта и манифест MV3
Стандартный набор: manifest.json, src/background.js (service worker), src/popup/, src/content/ (если нужен доступ к DOM). Но для AI мы добавляем onnxruntime-web и transformers.js.
{
"manifest_version": 3,
"name": "On-device Summarizer",
"version": "1.0",
"permissions": ["storage", "activeTab", "scripting"],
"host_permissions": [""],
"background": {
"service_worker": "src/background.js",
"type": "module"
},
"action": {
"default_popup": "src/popup/index.html"
},
"content_scripts": [{
"matches": [""],
"js": ["src/content.js"]
}]
} Важно: Service worker не имеет доступа к DOM, но может выполнять chrome.scripting.executeScript. Модель загружается в worker, что идеально — он может «спать» между вызовами, а при активации (по сообщению) просыпается.
Шаг 2: Подключение Transformers.js в Service Worker
Установка:
npm install @xenova/transformers onnxruntime-webВ background.js (ES module):
import { pipeline } from '@xenova/transformers';
let summarizer = null;
// Инициализация модели при старте (лениво)
async function getSummarizer() {
if (!summarizer) {
// Загружаем квантованную модель (q4) для экономии памяти
summarizer = await pipeline('summarization', 'Xenova/distilbart-cnn-6-6', {
quantized: true, // int8 квантование
device: 'wasm', // или 'webgpu' если есть поддержка
});
}
return summarizer;
}
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
if (request.type === 'SUMMARIZE') {
// Асинхронный ответ в MV3: возвращаем true для sendResponse
(async () => {
try {
const model = await getSummarizer();
const result = await model(request.text, { max_length: 100 });
sendResponse({ success: true, summary: result[0].summary_text });
} catch (err) {
sendResponse({ success: false, error: err.message });
}
})();
return true; // keep channel open
}
});Обратите внимание: мы используем quantized: true. Это снижает размер модели с ~1.4 ГБ до ~350 МБ. Для расширения — критично. Подробнее про квантование в нашем гайде по Transformers v5.
Шаг 3: Общение с Popup и Content Script
Popup — браузерное действие. Он не должен грузить модели. Только посылает запрос worker'у и показывает результат.
// popup.js
chrome.tabs.query({ active: true, currentWindow: true }, ([tab]) => {
chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => document.body.innerText
}, async ([result]) => {
const text = result.result.slice(0, 5000); // лимит на вход
const response = await chrome.runtime.sendMessage({ type: 'SUMMARIZE', text });
document.getElementById('output').innerText = response.summary;
});
});Но есть нюанс: время жизни service worker. По спецификации MV3 worker может выгрузиться через 30 секунд бездействия. Если модель грузится 3 секунды — успеет. Но если инференс долгий (большая LLM), Chrome может убить worker. Решение: использовать chrome.storage.local для кеширования результата или chrome.runtime.connect() с портом, который держит worker активным.
ReadableStream. В Transformers.js v4 это поддерживается.Шаг 4: Оптимизация — размер, память, WebGPU
На апрель 2026 года WebGPU доступен в Chrome на 80% устройств (требуется Vulkan/Metal). Если ваша целевая аудитория — энтузиасты с RTX, смело включайте device: 'webgpu'. Это ускорит инференс в 3-5 раз. Но для расширения, которое должно работать на любом ноутбуке, лучше оставить 'wasm' как fallback.
Размер бандла: onnxruntime-web весит ~12 MB. Если добавить модель — еще 300+ MB. Используйте CDN: загружайте модель по URL, а не пакуйте в расширение. В manifest укажите web_accessible_resources для доступа к локальным файлам, но модели лучше тянуть из Hugging Face Hub (кеш через Cache API).
// background.js
const modelId = 'Xenova/distilbart-cnn-6-6';
const quantized = true;
const revision = 'main';
const model = await pipeline('summarization', modelId, { quantized, revision });Библиотека сама скачает модель при первом запуске и закеширует в IndexedDB. Повторный запуск — мгновенный.
Для более продвинутых сценариев (локальный агент, как в On-device браузерный агент на Qwen), понадобится управление сессиями и контекстным окном. Там же описан stepwise planning.
Типичные грабли и как их обойти
Грабли 1: Service worker не просыпается для загрузки модели
Если пользователь открывает popup первый раз, worker еще не загружен. chrome.runtime.sendMessage может упасть. Решение: в popup слушать событие chrome.runtime.onInstalled или chrome.runtime.onStartup? Нет, в MV3 worker стартует при первом сообщении. Лучше сделать так: popup вызывает chrome.runtime.sendMessage с флагом type: 'WAKE', worker в ответ отдает статус готовности. Если не готов — ждать 200ms и повторять.
// popup
function sendWithRetry(msg, retries = 5) {
return new Promise((resolve, reject) => {
const attempt = (n) => {
chrome.runtime.sendMessage(msg, (resp) => {
if (chrome.runtime.lastError) {
if (n > 0) setTimeout(() => attempt(n-1), 200);
else reject(chrome.runtime.lastError);
} else resolve(resp);
});
};
attempt(retries);
});
}Грабли 2: Память — лимит 512 MB на worker
Chrome может убить worker, если он жрет больше 512 МБ. Модели вроде Gemma 4 2B занимают ~1.5 ГБ в fp16, но в int4 — ~400 МБ. Используйте квантованные версии. Если модель всё ещё большая — выгружайте её после использования: model.dispose() и очищайте кэш.
Грабли 3: Content script не имеет доступа к модели
Не пытайтесь импортировать @xenova/transformers в content script — он выполняется в изолированном мире, без доступа к window и с ограниченным WebAssembly. Только communication через chrome.runtime.
Как НЕ надо делать (худшие практики)
Встречал расширения, где модель загружается в popup при каждом клике. Popup закрылся — модель выгрузилась. Следующий клик — снова загрузка 10 секунд. Пользователи в ярости. Никогда так не делайте.
Второй антипаттерн — скачивать модели без квантования. Расширение раздувается до 2 ГБ, Chrome Store отказывается публиковать (лимит 500 MB). Используйте modelId: 'Xenova/distilbart-cnn-6-6' с флагом quantized: true — это даст модель размером ~350 МБ.
FAQ: быстрые ответы
| Вопрос | Ответ |
|---|---|
| Какую модель выбрать для суммаризации? | Xenova/distilbart-cnn-6-6 — легкая, 350 МБ. Для русского языка Xenova/rugpt3-small-summarization. |
| Можно ли запустить Gemma 4 в расширении? | Да, через Transformers.js v4 с device: 'webgpu'. Gemma 4 2B в int4 занимает ~1.2 ГБ, но Chrome может выгрузить worker. Рекомендую использовать chrome.runtime.connect с портом, чтобы держать его живым. |
| Поддерживает ли Transformers.js v5? (2026) | Transformers v5 (Python) — да, но Transformers.js пока на v4. В планах миграция на ONNX Runtime 2.0. |
| Что делать, если модель не загружается из-за CORS? | Используйте web_accessible_resources в manifest или проксируйте через background fetch. |
Финальный код и тестирование
Полный код расширения я выложил в репозиторий (ссылка в профиле). Но главное — понимать архитектуру: model lives in worker, UI — dumb. Если вы освоите этот паттерн, сможете добавить в расширение распознавание изображений (через pipeline('image-classification')), генерацию текста (через pipeline('text-generation', model)) и даже стриминг речи (Kitten TTS — см. гайд).
Напоследок: не гонитесь за самой тяжелой моделью. Для расширения часто хватает DistilBERT или TinyLlama. Локальный AI — это про скорость и приватность, а не про супер-интеллект. Сделайте так, чтобы модель грузилась за 2 секунды и не жрала батарею — и пользователи скажут спасибо.