Web Push — единственный способ дотянуться до пользователя, когда он закрыл вашу вкладку. Не email (который читают раз в день), не SMS (который дорогой) — а нативное системное уведомление прямо в трей или на экран блокировки. Звучит мощно, но 80% сайтов внедряют это так, что это бесит и уводит пользователей навсегда.
Разберём, как сделать Web Push, не превратив сайт в спам-машину.
Что такое Web Push под капотом
Это связка из трёх частей:
- Service Worker — фоновый скрипт в браузере, который живёт даже когда сайт закрыт.
- Push API — браузерный API, который через свой push-сервер (FCM у Chrome, Mozilla у Firefox, APNs у Safari) принимает сообщения от вашего бэкенда и будит SW.
- Notifications API — тот же SW рисует на экране нативное уведомление.
Бэкенд отправляет JSON с подписью VAPID на push-endpoint браузера. Браузер сам доставит до клиента, даже если вкладки нет.
Поддержка в 2026
| Браузер | Десктоп | Мобильный |
|---|---|---|
| Chrome / Edge | да | Android — да, iOS Safari — нет |
| Firefox | да | Android — да |
| Safari | да (16.4+) | iOS 16.4+ — только если сайт добавлен на главный экран как PWA |
| Яндекс Браузер | да | Android — да |
iOS — главная боль. Web Push работает, но только для PWA, добавленных на главный экран. Обычная вкладка в Safari ничего не получит.
VAPID-ключи
VAPID — это пара ключей, которой ваш бэкенд подписывает push-сообщения. Браузер по подписи понимает, что это вы.
npx web-push generate-vapid-keys
Выдаст два ключа — public и private. Публичный передаётся в браузер при подписке, приватный хранится на сервере как секрет.
VAPID_PUBLIC_KEY=BNb...
VAPID_PRIVATE_KEY=ZmI...
VAPID_SUBJECT=mailto:admin@example.ru
Service Worker
Минимальный SW, который умеет принимать push:
// public/sw.js
self.addEventListener("push", (event) => {
const data = event.data?.json() ?? {};
const title = data.title ?? "Новое уведомление";
const options = {
body: data.body ?? "",
icon: "/icons/icon-192.png",
badge: "/icons/badge-72.png",
data: { url: data.url ?? "/" },
tag: data.tag,
requireInteraction: false,
};
event.waitUntil(self.registration.showNotification(title, options));
});
self.addEventListener("notificationclick", (event) => {
event.notification.close();
const url = event.notification.data?.url ?? "/";
event.waitUntil(clients.openWindow(url));
});
Регистрация в приложении:
if ("serviceWorker" in navigator) {
await navigator.serviceWorker.register("/sw.js");
}
Подписка пользователя
Самая важная часть — момент запроса разрешения. Сделать это сразу при первом заходе — гарантия, что 90% пользователей нажмёт «Block» навсегда. Браузер запоминает выбор и не покажет prompt повторно — больше уведомлений этому пользователю не отправить.
Правильная схема — двухступенчатый запрос: сначала показываем свой кастомный баннер «хотите получать уведомления о...», и только после клика «Да» вызываем нативный prompt.
async function subscribeUser() {
const reg = await navigator.serviceWorker.ready;
const sub = await reg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(VAPID_PUBLIC_KEY),
});
await fetch("/api/push/subscribe", {
method: "POST",
body: JSON.stringify(sub),
});
}
userVisibleOnly: true обязательно — без него в Chrome не разрешат подписку.
Бэкенд для отправки
Самый простой вариант — Node.js с библиотекой web-push:
import webpush from "web-push";
webpush.setVapidDetails(
process.env.VAPID_SUBJECT!,
process.env.VAPID_PUBLIC_KEY!,
process.env.VAPID_PRIVATE_KEY!,
);
await webpush.sendNotification(subscription, JSON.stringify({
title: "Заказ #1242",
body: "Курьер забрал ваш заказ, доставка через 30 минут",
url: "/orders/1242",
tag: "order-1242",
}));
Если возвращается 410 Gone — подписка протухла, удаляйте из БД.
Хранение подписок
CREATE TABLE push_subscriptions (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
endpoint TEXT NOT NULL UNIQUE,
p256dh TEXT NOT NULL,
auth TEXT NOT NULL,
user_agent TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
last_used_at TIMESTAMPTZ
);
CREATE INDEX ON push_subscriptions(user_id);
Один пользователь может иметь несколько подписок (разные устройства, браузеры). При рассылке отправляйте на все, но дедуплицируйте по tag, чтобы на телефоне и ноутбуке не приходило одно и то же два раза подряд.
Сегментация и тематика
Жёсткое правило: подписка должна быть тематической, а не «на всё». Если пользователь подписался на «уведомления о доставке заказа», не шлите ему «скидка 20% на новую коллекцию». Это убивает доверие, а отписка от Web Push — это «Block» навсегда, без шанса вернуть.
Хорошая практика — категории подписок:
| Тема | Когда отправлять |
|---|---|
| Статус заказа | изменение состояния доставки |
| Сообщения от поддержки | новый ответ в чате |
| Напоминания | пользователь сам поставил «напомни через X» |
| Маркетинговые акции | только если явно подписан отдельно |
Метрики
Минимум, что нужно мерить:
- Conversion rate баннера → нативного prompt → подписки.
- Доля «Block» (потерянных навсегда).
- Доля доставленных push (410 → отписан).
- CTR — процент кликов от доставленных.
- Доля unsubscribe (отозвал разрешение).
Хорошие цифры: CTR 5-15%, отписка менее 1% в месяц. Если CTR менее 2% — вы шлёте мусор, остановитесь.
Когда не стоит вообще
- Контент-сайт без аккаунтов и без транзакционных событий — не о чём уведомлять.
- B2B-сервис с email как основным каналом — пользователь и так читает почту.
- Лендинг — нечего и зачем пушить, кроме «купи».
- Если нет ресурса на сегментацию — вы скатитесь в спам и испортите репутацию.
Web Push хорош там, где есть события в реальном времени: маркетплейсы (статус заказа), мессенджеры, такси, доставка, биржи, тикет-системы поддержки.
iOS-специфика
Для iOS-пользователей:
- Сайт должен быть PWA с
manifest.jsonи установлен «На экран Домой». - После установки запросить разрешение можно только в ответ на пользовательское действие (клик).
- Уведомления приходят в нативный Notification Center.
Если iOS — большая доля аудитории и PWA нет, инвестируйте сначала в PWA, потом в Push.
Альтернативы
| Канал | Плюсы | Минусы |
|---|---|---|
| Web Push | бесплатно, мгновенно | не работает при offline-устройстве, iOS только PWA |
| универсально, доступно офлайн | задержка, попадание в спам | |
| SMS | гарантированная доставка | дорого (1.5-3 ₽ в РФ за штуку) |
| Telegram-бот | бесплатно, быстро | нужно подписать на бота отдельно |
| In-app уведомления | работают на любом устройстве | только когда пользователь на сайте |
Часто Web Push не вместо, а вместе с email и Telegram-ботом — для разных типов событий.
Итого
Web Push — мощный канал, но требует уважения к пользователю. Правильно: тематические подписки, явное согласие на каждый тип, метрики, разумная частота. Неправильно — prompt при первом заходе, рассылка скидок «всем подписанным» и удивление, почему отписка 30%.
Частые вопросы
Можно ли отправлять Web Push без согласия пользователя?
Нет, ни технически, ни юридически. Браузер не разрешит подписку без явного Notification.permission === "granted". И с точки зрения 152-ФЗ согласие на обработку контактных данных для рассылок должно быть явным и отзывным. Хорошая практика — отдельный чекбокс «согласен получать уведомления о...» с понятным описанием, что именно вы будете слать.
Сколько стоит инфраструктура для Web Push?
Сами push-серверы Chrome/Firefox/Safari бесплатны. Платите вы только за свой бэкенд: VPS для очереди отправки и БД для хранения подписок. Для 10-50 тысяч активных подписок хватает $10-20/мес сервера. Если рассылок миллионы в день — стоит вынести в отдельный воркер с очередью на RabbitMQ или Redis.
Что делать с Safari на iOS?
Только через PWA. Нужен валидный manifest.json, иконки правильных размеров, обслуживание по HTTPS, и сайт должен быть «Добавлен на экран Домой». Тогда Push работает. Если у вас 80% iOS-аудитории и нет PWA — Web Push для вас не вариант, рассматривайте email и Telegram.
Какая частота отправки безопасна?
Зависит от типа. Транзакционные (изменился статус заказа) — по событию, хоть 5 в день. Маркетинговые — не чаще 1-2 в неделю. Если шлёте каждый день «акция-акция-акция», получите массовую отписку и попадание под антиспам-эвристики браузеров (некоторые версии Chrome тихо снижают приоритет частых отправителей).
Как тестировать Push в разработке?
На localhost Chrome разрешает Push без HTTPS. На staging-окружении нужен валидный сертификат (Let's Encrypt). Тестировать удобно через DevTools → Application → Service Workers → Push, можно эмулировать получение сообщения. Для нагрузочного тестирования — отдельный скрипт, который шлёт через web-push на тестовые endpoints.
Если пользователь нажал Block, можно ли как-то вернуться?
Технически — нет, разрешение «забетонировано» в настройках браузера. Пользователь должен зайти в настройки сайта и вручную сменить на Allow. На практике этого почти никто не делает. Поэтому первый запрос — самый важный, и его нельзя транжирить на «может быть». Лучше потерять 50% потенциальных подписчиков на этапе своего баннера, чем сжечь их навсегда нативным prompt.
Можно ли через Web Push отправлять только текст или ещё картинки?
Картинки можно: параметр image в showNotification() поддерживается в Chrome и Edge. В Safari и Firefox — игнорируется, показывается только иконка. Поэтому проектируйте уведомление так, чтобы оно работало без картинки, а картинка была усилением. И помните про лимит payload — 4 КБ на push-сообщение, картинку лучше отдавать как ссылку, а не base64.