Все уроки
ФронтендСредний12 мин чтенияВ моём стеке

PWA и service worker

Устанавливаемые веб-приложения, которые быстро загружаются и работают офлайн: manifest, service worker, кэширование.

  • Manifest
  • Service worker
  • Offline
  • Install
  • Push
Шаг 1/4
Каждый запрос страницы проходит через service worker — швейцара. Онлайн он берёт ответ из сети и кладёт копию в кэш.

За 30 секунд

Progressive Web App (PWA) — это сайт, который можно установить как приложение, который открывается в собственном окне и продолжает работать при плохой сети или без неё. Это возможно благодаря двум частям: web app manifest, описывающему имя приложения, иконки и цвета, и service worker — фоновому скрипту, который стоит между страницей и сетью и решает, что отдавать из кэша. У вас остаётся одна кодовая база и один URL, а поверх добавляется поведение как у приложения.

Почему я это использую

  • Я сделал своё портфолио устанавливаемым PWA, поэтому сам разбирался с деталями manifest, иконок и service worker, а не только читал о них.
  • Мне нравится, что PWA добавляет обычному сайту поведение приложения без магазинов приложений, отдельных кодовых баз и задержек на ревью.
  • Хороший service worker делает повторные визиты быстрыми и показывает пользователям дружелюбную офлайн-страницу вместо экрана ошибки браузера.
  • Это практичный и недорогой способ улучшить воспринимаемое качество на мобильных, где сети медленные или ненадёжные.

Как это работает

01Идея: сайт, который ведёт себя как приложение

PWA — это не новая технология, а набор возможностей браузера, используемых вместе. По HTTPS сайт с валидным manifest можно установить на домашний экран или рабочий стол, запускать в отдельном окне без интерфейса браузера, и он появляется в переключателе приложений. Service worker добавляет поддержку офлайна, более быструю загрузку за счёт кэширования и, при желании, push-уведомления и фоновые задачи. Представьте service worker как умного швейцара: каждый запрос страницы проходит через него, и он решает, взять ли ответ из сети, из своего кэша или и то и другое. «Progressive» означает, что он деградирует аккуратно: браузеры без поддержки какой-то возможности всё равно получают обычный сайт.

  • Требуется HTTPS (для разработки разрешён localhost).
  • Manifest: идентичность и внешний вид после установки.
  • Service worker: сетевой прокси, кэш и фоновая работа.

02Web app manifest

Manifest — это JSON-файл, на который ссылается страница. Ключевые поля: name и short_name (показывается под иконкой), start_url (где открывается приложение), scope (какие URL относятся к приложению), display (standalone для окна приложения), theme_color и background_color (строка заголовка и splash screen) и icons. Предоставьте как минимум PNG-иконки 192x192 и 512x512. У maskable-иконок, помеченных purpose «maskable», важное содержимое находится внутри центральной безопасной зоны (круг диаметром 80% ширины), так что Android может обрезать их до круга, squircle или другой формы, не отрезая логотип. Добавьте id, чтобы браузер мог идентифицировать приложение, даже если start_url изменится, а также screenshots и description для более богатого диалога установки.

json
{
  "id": "/",
  "name": "Example Portfolio",
  "short_name": "Portfolio",
  "start_url": "/?source=pwa",
  "scope": "/",
  "display": "standalone",
  "theme_color": "#0f172a",
  "background_color": "#0f172a",
  "icons": [
    { "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
    { "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png" },
    { "src": "/icons/maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
  ]
}

03Жизненный цикл service worker

Service worker — это JavaScript-файл, который работает в собственном потоке, без доступа к DOM, и может жить дольше страницы. Страница регистрирует его; браузер скачивает его и вызывает install, где обычно предкэшируют офлайн-страницу и основные ресурсы. Затем он ждёт, пока ни одна открытая вкладка не будет использовать старый worker, и вызывает activate, где удаляют старые кэши. С этого момента он получает события fetch для каждого запроса в своём scope. Когда вы деплоите изменённый файл worker-а, браузер замечает разницу в байтах, устанавливает новую версию и держит её в ожидании, пока не закроются старые вкладки. skipWaiting() и clients.claim() заставляют новую версию взять управление сразу — это удобно, но может смешать старые страницы с новыми закэшированными ресурсами, поэтому используйте их осознанно.

js
// In the page (only in production builds)
if ("serviceWorker" in navigator) {
  window.addEventListener("load", () => {
    navigator.serviceWorker.register("/sw.js", { scope: "/" })
      .catch((err) => console.error("SW registration failed", err));
  });
}

04Стратегии кэширования

Внутри обработчика fetch вы выбираете стратегию для каждого вида запросов. Cache-first отдаёт из кэша и идёт в сеть только при промахе; идеально для статических ресурсов с отпечатком в имени (app.3f9a.js) и шрифтов, которые никогда не меняются. Network-first пробует сеть и при офлайне откатывается к кэшу; идеально для HTML-страниц и данных API, которые должны быть свежими. Stale-while-revalidate мгновенно отвечает из кэша и обновляет кэш в фоне; хорошо для аватаров и некритичных данных. Network-only — для того, что никогда нельзя кэшировать, например аутентифицированных мутаций и платежей. Распространённый безопасный вариант по умолчанию для контентных сайтов — network-first для навигаций с офлайн-страницей в качестве fallback и cache-first для хэшированных статических файлов.

  • Cache-first: хэшированные JS/CSS, шрифты, иконки.
  • Network-first: HTML-навигации, свежие чтения из API.
  • Stale-while-revalidate: изображения, некритичный JSON.
  • Network-only: POST-запросы, auth, оформление заказа.

05Network-first service worker с офлайн-страницей

Этот написанный вручную worker при install предкэширует офлайн-страницу, при activate удаляет старые кэши и обрабатывает навигации по стратегии network-first: пробует сеть, сохраняет копию успешных страниц и при сбое сети откатывается к закэшированной странице или офлайн-странице. Не-GET запросы и другие origin он не трогает. Повышайте версию кэша, когда меняются офлайн-страница или основные файлы.

js
const CACHE = "site-v3";
const OFFLINE_URL = "/offline";
 
self.addEventListener("install", (e) => {
  e.waitUntil(caches.open(CACHE).then((c) => c.addAll([OFFLINE_URL, "/icons/icon-192.png"])));
});
self.addEventListener("activate", (e) => {
  e.waitUntil(caches.keys().then((keys) =>
    Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))));
});
self.addEventListener("fetch", (e) => {
  const req = e.request;
  if (req.method !== "GET" || new URL(req.url).origin !== location.origin) return;
  if (req.mode !== "navigate") return;
  e.respondWith(fetch(req).then((res) => {
    const copy = res.clone();
    if (res.ok) caches.open(CACHE).then((c) => c.put(req, copy));
    return res;
  }).catch(async () => (await caches.match(req)) || caches.match(OFFLINE_URL)));
});

06PWA в Next.js

Next.js App Router поддерживает manifest «из коробки»: файл app/manifest.ts, возвращающий объект MetadataRoute.Manifest, отдаётся как /manifest.webmanifest и подключается автоматически. Цвет темы задаётся в экспорте viewport. Service worker — статический файл public/sw.js, который отдаётся из корня сайта, чтобы его scope покрывал всё приложение, и регистрируется из небольшого клиентского компонента. Добавьте офлайн-роут (app/offline/page.tsx), который не зависит от данных, чтобы его можно было предкэшировать. Отдавайте sw.js с Cache-Control: no-cache, чтобы браузеры быстро проверяли обновления; документация Next.js показывает это через headers() в next.config. Для больших приложений Serwist (поддерживаемый преемник next-pwa, построенный на идеях Workbox) генерирует precache-манифесты из сборки.

ts
// app/manifest.ts
import type { MetadataRoute } from "next";
 
export default function manifest(): MetadataRoute.Manifest {
  return {
    name: "Example Portfolio",
    short_name: "Portfolio",
    start_url: "/",
    display: "standalone",
    background_color: "#0f172a",
    theme_color: "#0f172a",
    icons: [
      { src: "/icons/icon-192.png", sizes: "192x192", type: "image/png" },
      { src: "/icons/icon-512.png", sizes: "512x512", type: "image/png" },
      { src: "/icons/maskable-512.png", sizes: "512x512", type: "image/png", purpose: "maskable" },
    ],
  };
}

07Установка, push и различия платформ

Браузеры на Chromium (Chrome, Edge, Samsung Internet) показывают предложение установки или пункт меню, когда сайт работает по HTTPS и у него есть manifest с name, icons, start_url и подходящим режимом display; можно перехватить событие beforeinstallprompt, чтобы показать собственную кнопку установки. Safari на iOS и iPadOS устанавливает через «Поделиться», затем «На экран „Домой“», и начиная с iOS 16.4 установленные веб-приложения могут получать Web Push. Safari на macOS поддерживает Add to Dock. Для push нужны service worker, разрешение пользователя, запрошенное после его действия, VAPID-ключи и сервер, отправляющий сообщения. Background Sync и Periodic Background Sync есть только в Chromium, так что считайте их улучшениями. Firefox на десктопе не устанавливает PWA нативно. Тестируйте на каждой целевой платформе.

Когда применять

  • Контентные сайты, портфолио и блоги, которым полезны мгновенные повторные визиты и дружелюбная офлайн-страница.
  • Внутренние инструменты и полевые приложения в ненадёжных сетях, где важны чтение офлайн или отправка через очередь.
  • Продукты, которым нужен устанавливаемый опыт как у приложения без затрат на нативные приложения и ревью в магазинах.
  • E-commerce и медиасайты, где более быстрая загрузка на мобильных напрямую влияет на конверсию.

Когда не стоит

  • Приложения, которым нужна глубокая интеграция с устройством (Bluetooth на iOS, фоновая геолокация, продвинутое управление камерой), недоступная из браузера.
  • Когда находимость через магазины приложений — ключевое бизнес-требование, а сборка-обёртка для магазина неприемлема.
  • Сильно персонализированные, всегда свежие данные, где агрессивное кэширование покажет устаревший или чужой контент.

Альтернативы

  • Нативные приложения (Swift, Kotlin)

    Полный доступ к устройству и присутствие в магазинах; отдельные кодовые базы и циклы релизов.

  • React Native / Expo

    Общая кодовая база на JavaScript для iOS и Android с нативным UI и распространением через магазины.

  • Capacitor

    Оборачивает существующее веб-приложение в нативную оболочку для магазинов, с плагинами для API устройства.

  • Обычное HTTP-кэширование + CDN

    Часто достаточно ради одной скорости; без поддержки офлайна и установки.

Лучшие практики

  1. Отдавайте service worker из корня сайта с Cache-Control: no-cache, чтобы обновления подхватывались быстро.
  2. Используйте network-first для HTML и cache-first только для ресурсов с отпечатком, чтобы пользователи никогда не застревали на старых страницах.
  3. Версионируйте кэши и удаляйте старые в activate; никогда не позволяйте кэшам расти без ограничений.
  4. Предоставляйте и обычные, и maskable-иконки и проверяйте их в инструменте предпросмотра maskable-иконок.
  5. Никогда не кэшируйте аутентифицированные или персонализированные ответы под общим ключом кэша; не-GET запросы пропускайте полностью.
  6. Регистрируйте service worker только в продакшен-сборках, чтобы избежать запутанного кэширования во время разработки.
  7. Проводите аудит через Chrome DevTools (панель Application) и Lighthouse и тестируйте офлайн-режим на реальных устройствах.

Частые ошибки

  • Кэширование HTML по стратегии cache-first, из-за чего после деплоя пользователи продолжают видеть старую версию сайта.
  • Отдача sw.js с долгими заголовками кэширования или из подпапки, что задерживает обновления или ограничивает его scope.
  • Слепой вызов skipWaiting(), из-за которого старая страница внезапно работает с новыми закэшированными ресурсами и ломается.
  • Использование не-maskable логотипа в качестве maskable-иконки, которая на Android плохо обрезается.
  • Запрос разрешения на push-уведомления при первой загрузке страницы, который пользователи рефлекторно отклоняют.

Глубже

08Scope, navigation preload и производительность

Service worker управляет страницами в пределах своего scope, который по умолчанию — директория, откуда он отдаётся; поэтому sw.js живёт в корне. Страница попадает под управление только после активации worker-а, так что самый первый визит никогда не перехватывается, если не вызвать clients.claim(). Одна скрытая цена network-first — запуск: когда worker простаивает, браузер должен загрузить его, прежде чем запрос уйдёт в сеть, что добавляет десятки миллисекунд. Navigation preload это решает: браузер начинает навигационный запрос параллельно с запуском worker-а, а обработчик fetch использует event.preloadResponse. Держите обработчик fetch быстрым и избегайте пустого слушателя fetch, который добавляет накладные расходы без пользы.

js
self.addEventListener("activate", (e) => {
  e.waitUntil(self.registration.navigationPreload?.enable());
});
 
self.addEventListener("fetch", (e) => {
  if (e.request.mode !== "navigate") return;
  e.respondWith((async () => {
    try {
      return (await e.preloadResponse) || (await fetch(e.request));
    } catch {
      return (await caches.match(e.request)) || caches.match("/offline");
    }
  })());
});

09Безопасные сценарии обновления

Самый безопасный паттерн обновления такой: новый worker устанавливается и ждёт; страница слушает ожидающий worker и показывает баннер «доступна новая версия»; когда пользователь соглашается, страница отправляет сообщение, worker вызывает skipWaiting(), а страница перезагружается по controllerchange. Так работающую страницу не начнёт внезапно обслуживать worker, в кэше которого лежат другие версии ресурсов. Для контентного сайта с network-first HTML и хэшированными ресурсами автоматический skipWaiting обычно нормален, потому что старые ресурсы не удаляются посреди сессии, если какое-то время хранить предыдущий кэш. Всегда предусматривайте kill switch: деплой worker-а, который снимает свою регистрацию и очищает кэши, — это аварийное восстановление, если выкатился плохой worker.

js
// Emergency "kill switch" sw.js
self.addEventListener("install", () => self.skipWaiting());
self.addEventListener("activate", (e) => {
  e.waitUntil((async () => {
    for (const key of await caches.keys()) await caches.delete(key);
    await self.registration.unregister();
    for (const c of await self.clients.matchAll()) c.navigate(c.url);
  })());
});

10Лимиты хранилища, вытеснение и офлайн-данные

Cache API, IndexedDB и другие хранилища origin делят квоту, которой управляет браузер; Chromium позволяет origin занять значительную долю свободного диска, Safari строже и может вытеснять хранилище сайтов, которыми какое-то время не пользовались. По умолчанию хранилище работает в режиме «best effort» и может быть очищено при нехватке места; navigator.storage.persist() запрашивает постоянное хранилище, которое браузеры предоставляют с учётом вовлечённости или установки. Проверяйте использование через navigator.storage.estimate(). Для офлайн-данных помимо закэшированных ответов, например черновиков или отложенных отправок форм, используйте IndexedDB (через небольшую обёртку вроде idb) и повторяйте отложенные запросы при восстановлении сети — через Background Sync в Chromium и с fallback на событие online в остальных браузерах. Проектируйте с учётом конфликтов: к моменту попадания на сервер отложенные записи могут устареть.

11Подводные камни безопасности и приватности

Service worker — мощный инструмент, поэтому браузеры требуют HTTPS и скрипты того же origin. Основные риски — те, что мы создаём себе сами. Кэширование ответов с ключом только по URL может сохранить персонализированную страницу одного пользователя и показать её следующему на общем устройстве; никогда не кэшируйте аутентифицированный HTML или ответы API в общем кэше и очищайте кэши при выходе. Кросс-доменные opaque-ответы (no-cors) сильно расходуют квоту и не позволяют проверить их на ошибки, поэтому избегайте их кэширования. XSS-баг, через который можно зарегистрировать worker, закрепит код атакующего, поэтому помогают строгая Content Security Policy и директива worker-src. Наконец, сломанный worker может отрезать пользователей от обновлений — вот почему важны короткое HTTP-кэширование для sw.js и kill switch.

Реальные ситуации

01

После редизайна пользователи всё ещё видят старый сайт

Проблема

После крупного редизайна некоторые пользователи днями видят старый макет. Год назад был добавлен service worker со стратегией cache-first для всех запросов.

Как решить

Выкатите новый sw.js, который переключает навигации на network-first, использует новое имя кэша, удаляет все старые кэши в activate и вызывает skipWaiting плюс clients.claim, чтобы сразу взять управление. Убедитесь, что веб-сервер отдаёт для sw.js заголовок Cache-Control: no-cache, иначе браузеры дольше держат старый worker. Если старый worker сломан всерьёз, сначала задеплойте kill-switch worker, который очищает кэши и снимает регистрацию, а правильный worker верните в следующем релизе. Проверьте в DevTools в разделе Application, Service workers с включённым update on reload.

02

Иконка на Android выглядит крошечной и в рамке

Проблема

После установки PWA на Android иконка на домашнем экране выглядит как маленький логотип внутри белого круга, в отличие от нативных приложений вокруг.

Как решить

В manifest есть только обычные иконки, поэтому Android уменьшает их в безопасную зону на белом фоне. Создайте отдельную maskable-иконку: квадрат 512x512 на всю площадь в фирменном цвете фона с логотипом внутри центрального круга 80%. Добавьте её в manifest с purpose «maskable», сохранив обычные иконки для других случаев. Проверьте её в инструменте предпросмотра maskable-иконок и на панели manifest в Chrome DevTools, затем переустановите приложение на устройстве, поскольку лаунчеры кэшируют иконки.

03

Выездные сотрудники теряют данные форм при плохой связи

Проблема

Инспекторы заполняют длинные формы в подвалах без сигнала. Когда они отправляют форму офлайн, запрос падает и данные теряются.

Как решить

Сохраняйте черновик формы в IndexedDB при каждом изменении, чтобы ничего не терялось, даже если вкладка закроется. При отправке пробуйте сеть; если не удалось, добавляйте отправку в outbox в IndexedDB с UUID, сгенерированным на клиенте, и показывайте её как ожидающую. Повторяйте outbox при восстановлении связи: используйте Background Sync в Chromium и fallback на событие online и запуск приложения в остальных браузерах. Сервер использует UUID как ключ идемпотентности, так что дважды повторённая отправка сохраняется один раз. Показывайте пользователю понятный статус каждого ожидающего элемента.

Шпаргалка

Подключить manifest (обычный HTML)
<link rel="manifest" href="/manifest.webmanifest">
Файл manifest в Next.js
app/manifest.ts, экспортирующий MetadataRoute.Manifest
Зарегистрировать service worker
navigator.serviceWorker.register("/sw.js", { scope: "/" })
Запись maskable-иконки
{ "src": "/icons/maskable-512.png", "sizes": "512x512", "purpose": "maskable" }
Заголовок для sw.js
Cache-Control: no-cache, no-store, must-revalidate
Активировать новый worker сейчас
self.skipWaiting(); self.clients.claim();
Проверить использование хранилища
await navigator.storage.estimate()
Запросить постоянное хранилище
await navigator.storage.persist()
Отладка в Chrome
DevTools > Application > Manifest / Service workers / Cache storage

В моих реальных проектах

  • Мой собственный сайт-портфолио на Next.js — устанавливаемый PWA с web app manifest.
  • В нём есть maskable-иконки, чтобы иконка приложения правильно выглядела в разных формах иконок на Android.
  • Его service worker использует стратегию network-first и при отсутствии соединения откатывается на отдельную офлайн-страницу.

Вопросы и ответы

Q1Что делает сайт PWA?

Технически — три вещи: он отдаётся по HTTPS, у него есть web app manifest с именем, иконками, стартовым URL и режимом display, и обычно есть service worker, который отвечает за кэширование и поведение офлайн. С этим браузеры могут предложить установку, открыть его в отдельном окне и сохранить пригодным к использованию в плохих сетях. Помимо чеклиста, хороший PWA адаптивен, быстр при повторных визитах и аккуратно обрабатывает офлайн, показывая понятное сообщение вместо ошибки браузера. При этом он остаётся обычным сайтом с URL, поэтому индексируется, им можно делиться, и он мгновенно обновляется при деплое.

Q2Как обновляется service worker и почему пользователи иногда видят старый контент?

При навигации браузер проверяет скрипт service worker на изменения в байтах, не реже чем раз в 24 часа. Если он изменился, новый worker устанавливается, но ждёт, пока закроются все вкладки под управлением старого, и только потом активируется. Пользователи видят старый контент, когда HTML отдаётся по cache-first, когда сам sw.js закэширован HTTP-заголовками или когда ожидающий worker так и не активируется, потому что вкладка остаётся открытой. Решения — network-first для страниц, заголовки no-cache на sw.js и либо подсказка «доступна новая версия, перезагрузить», которая вызывает skipWaiting, либо аккуратное использование skipWaiting вместе с clients.claim.

Q3В чём разница между Cache API и HTTP-кэшированием?

HTTP-кэширование управляется заголовками ответа вроде Cache-Control и ETag, и браузер сам решает, когда переиспользовать ответ. Cache API — это программируемое хранилище, которым service worker и страницы управляют из кода: вы решаете, что туда положить, когда читать и когда удалять, независимо от заголовков. Они работают слоями: fetch() из service worker всё равно проходит через HTTP-кэш. На практике HTTP-кэширование обеспечивает большую часть скорости для статических ресурсов, а Cache API добавляет поддержку офлайна и собственные стратегии вроде network-first с офлайн-fallback.

Q4Что такое maskable-иконки и почему они важны?

Лаунчеры Android показывают иконки в разных формах — кругах, squircle или скруглённых квадратах — в зависимости от устройства. Обычная иконка уменьшается на белом фоне или неудачно обрезается. Maskable-иконка спроектирована так, что её важное содержимое находится внутри центральной безопасной зоны — круга диаметром 80% иконки, — а весь квадрат залит цветом фона, так что лаунчер может аккуратно обрезать её до любой формы. Её объявляют в manifest с purpose «maskable», обычно рядом с обычной иконкой «any», которая используется в других контекстах.

Q5Могут ли PWA отправлять push-уведомления на iPhone?

Да, начиная с iOS и iPadOS 16.4, но только для веб-приложений, которые пользователь добавил на домашний экран. Поток использует стандартные Push API и Notifications API с service worker, а разрешение нужно запрашивать в ответ на действие пользователя, например нажатие кнопки. На сервере нужны VAPID-ключи и библиотека вроде web-push, чтобы отправлять сообщения на эндпоинты подписок. На Android и в десктопном Chromium push работает в браузере без установки. Уважайте пользователей: спрашивайте только тогда, когда ценность очевидна, и давайте управлять настройками.

Q6Должен ли каждый сайт быть PWA?

Не каждому сайту нужна установка, но почти каждому полезна небольшая часть PWA. Manifest с хорошими иконками почти ничего не стоит и улучшает вид сайта, когда им делятся или закрепляют его. Простой service worker с офлайн-страницей и кэшированием статических ресурсов помогает пользователям с плохим соединением. Полноценное offline-first поведение, background sync и push оправданы только тогда, когда пользователи часто возвращаются или работают в ненадёжных условиях. Риск — ошибки кэширования, которые отдают устаревший или приватный контент, поэтому минимальный, хорошо протестированный worker лучше амбициозного и глючного.

Вопросы уровня senior

S1Как бы вы спроектировали offline-first архитектуру для PWA для ввода данных?

Я бы сделал локальную базу основным источником для UI: IndexedDB хранит записи и outbox ожидающих мутаций, у каждой — UUID, сгенерированный на клиенте, и временная метка. UI читает и пишет локально и показывает статус синхронизации. Движок синхронизации при появлении сети отправляет outbox, используя UUID как ключи идемпотентности, и забирает изменения с сервера начиная с курсора. Для конфликтов нужна явная политика по каждой сущности: last-write-wins для простых полей, слияние на уровне полей или пометка для ручного разрешения там, где данные важны. Service worker предкэширует app shell и обрабатывает навигации; слой данных живёт в приложении, а не в worker-е, поэтому его можно тестировать. Запрашивается постоянное хранилище, а сервер валидирует всё, потому что клиенты могут быть устаревшими или подделанными.

S2Когда стоит использовать Workbox или Serwist вместо написанного вручную service worker?

Ручной worker хорош для небольшой понятной политики: предкэшировать офлайн-страницу, network-first для навигаций, cache-first для хэшированных ресурсов. Это несколько десятков строк, и их легко проверить. Как только нужен генерируемый при сборке precache-манифест с хэшами ревизий, несколько runtime-маршрутов с истечением срока и лимитами размера, broadcast-обновления, очереди background sync и navigation preload, библиотека окупается, потому что в этих деталях легко тонко ошибиться. Workbox даёт всё это в виде модулей; Serwist — поддерживаемый форк с хорошей интеграцией с Next.js. Компромисс — ещё одна зависимость и абстракция, в которой нужно разбираться при отладке. Я начинаю с простого и переключаюсь, когда таблица маршрутов разрастается больше чем на несколько правил.

S3Как PWA влияют на SEO и Core Web Vitals?

PWA — это всё ещё сайт, поэтому индексация зависит от HTML, который возвращает сервер, а не от service worker; краулеры обычно ваш worker не используют. Серверно-рендеримые или статические страницы остаются обязательными для SEO. Для полевых Core Web Vitals service worker помогает повторным визитам: закэшированные ресурсы улучшают LCP, а app shell может сделать навигации почти мгновенными. Но плохо написанный worker может навредить: запуск worker-а добавляет задержку к каждой навигации, если не включён navigation preload, а network-first с медленными таймаутами может задерживать страницы в нестабильных сетях. Я измеряю реальные пользовательские данные с разбивкой на первые и повторные визиты, включаю navigation preload и держу логику fetch в worker-е минимальной.

Глоссарий

PWA
Progressive Web App: сайт с manifest и service worker, который можно установить и который работает офлайн.
Web app manifest
JSON-файл, описывающий имя приложения, иконки, цвета, стартовый URL и режим display для установки.
Service worker
Фоновый скрипт, который перехватывает сетевые запросы страницы и может кэшировать, изменять их или отвечать на них.
Scope
Путь URL, которым управляет service worker; по умолчанию — папка, из которой отдаётся файл worker-а.
Maskable icon
Иконка с содержимым внутри центральной безопасной зоны, чтобы платформы могли обрезать её до любой формы.
Cache API
Браузерный API хранилища для сохранения и получения пар запрос-ответ из кода.
Network-first
Стратегия кэширования, которая пробует сеть и при офлайне откатывается к закэшированному контенту.
Standalone display
Режим manifest, в котором установленное приложение открывается в собственном окне без адресной строки браузера.
VAPID
Ключи, которые идентифицируют ваш сервер перед push-сервисами браузеров при отправке сообщений Web Push.

Есть идея продукта?Давайте создадим его.

Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.