Как подружить PWA-функционал и Next.js-приложение с помощью пакета next-pwa-pack

Время прочтения — 14 минут
Содержание
В dev.family мы часто создаём решения, которыми удобно пользоваться, и делимся ими с сообществом. Один из таких инструментов – наш новый пакет next-pwa-pack, с которым вы можете быстро внедрить PWA-функционал в ваше Next.js-приложение.

Мой путь к созданию пакета

Клиенты иногда просят добавить PWA-функционал в наши приложения, и это превращается в настоящую головную боль. Сначала я пытался использовать готовые решения, но они либо были переусложнены и сопровождались кучей настроек, либо не работали корректно с Next.js App Router. Кроме того, часто не хватало поддержки серверных экшенов для управления кэшем с сервера, интеграции с SSR, а также простой возможности подключить PWA-функционал к App Router и реализовать серверную ревалидацию кэша или интеграцию с внешними системами через API-роуты.
Приходилось вручную писать сервис-воркеры, настраивать кэширование, разбираться с обновлениями и синхронизацией между вкладками. Уходило много времени только на то, чтобы заставить PWA просто работать. Особенно раздражало, что после каждого обновления кода приходилось самостоятельно очищать кэш, а пользователи не получали уведомления о новых версиях.
Короче говоря, нам нужно было универсальное решение, которое будет работать из коробки и не потребует глубокого погружения в спецификации сервис-воркеров.

Процесс создания пакета

Я начал с анализа всех проблем, с которыми сталкивался сам. Главной задачей было сделать так, чтобы разработчик мог добавить PWA-функционал быстро и просто, с минимальными манипуляциями.
Сначала я создал базовый сервис-воркер, который умеет кэшировать HTML-страницы с TTL (временем жизни кэша) и статические ресурсы, а также обрабатывать офлайн-режим.
Потом добавил систему сообщений между клиентом и сервис-воркером для управления кэшем. И далее написал пару скриптов, чтобы пакет автоматически копировал необходимые файлы **(sw.js,** **manifest.json**, **offline.html**) в проект при установке.
А также автоматически добавлял серверный экшен `revalidatePWA` для управления кэшем с сервера (через server actions, API routes или server components).
Для интеграции с SSR/Edge middleware и Next.js App Router реализован HOC `withPWA`, который позволяет легко подключить PWA-функционал и серверную ревалидацию кэша даже в сложных сценариях маршрутизации и серверного рендеринга.
Отдельно пришлось повозиться с синхронизацией кэша между вкладками – это важно для SPA-приложений, где пользователь может открыть несколько вкладок. Реализовал ее через localStorage и события storage.
В итоге получился пакет, который работает «из коробки» и не требует сложной настройки.
Ищете партнера для разработки? Расскажите о своем проекте

Преимущества next-pwa-pack

Установка пакета помогает решить следующие проблемы разработки:
  • Автоматизация регистрации сервис-воркера – не нужно вручную писать код для регистрации и управления сервис-воркером;
  • Важные элементы копируются в проект и их можно редактировать под специфические нужды;
  • Управление кэшем – пакет предоставляет удобные функции для очистки, обновления и отключения кэша;
  • Синхронизация между вкладками – автоматическое обновление кэша во всех открытых вкладках;
  • Офлайн-режим – приложение работает без интернета;
  • Удобные инструменты для разработки – встроенная панель разработчика для управления PWA-состоянием;
  • Инструменты для обновления и ревалидации кэша с сервера – поддержка server actions, API routes и интеграция с внешними системами.
Загрузить next-pwa-pack можно по ссылке: https://github.com/dev-family/next-pwa-pack

Что происходит при установке

После установки пакета в папку public автоматически копируются необходимые файлы:
  • sw.js – сервис-воркер с готовой логикой кэширования;
  • offline.html – страница для офлайн-режима;
  • manifest.json – конфигурация PWA (требует настройки под ваш проект).
❗ Файлы не будут скопированы, если одноименные уже присутствуют в проекте.
Для копирования вручную можно применить скрипт:
node node_modules/next-pwa-pack/scripts/copy-pwa-files.mjs
# или
npx next-pwa-pack/scripts/copy-pwa-files.mjs
**Автоматически добавляется (или обновляется) файл с серверным экшеном** `revalidatePWA` – app/actions.ts или src/app/actions.ts (если используется структура с папкой src).
"use server";
export async function revalidatePWA(urls: string[]) {
 const baseUrl = process.env.NEXT_PUBLIC_HOST || "http://localhost:3000";
 const res = await fetch(`${baseUrl}/api/pwa/revalidate`, {
   method: "POST",
   headers: { "Content-Type": "application/json" },
   body: JSON.stringify({
     urls,
     secret: process.env.REVALIDATION_SECRET,
   }),
 });
 return res.json();
}
Если файл не появился, выполните:
node node_modules/next-pwa-pack/scripts/copy-pwa-server-actions.mjs

Настройка manifest.json

После установки настройте public/manifest.json под ваш проект:
{
 "name": "Моё приложение",
 "short_name": "Моё приложение",
 "description": "Описание моего приложения",
 "start_url": "/",
 "display": "standalone",
 "background_color": "#ffffff",
 "theme_color": "#000000",
 "icons": [
   {
     "src": "/icons/icon-192x192.png",
     "sizes": "192x192",
     "type": "image/png"
   },
   {
     "src": "/icons/icon-512x512.png",
     "sizes": "512x512",
     "type": "image/png"
   }
 ]
}
Добавьте свои иконки в папку public/icons/ или измените пути в manifest.json.

Быстрый старт

Далее достаточно обернуть ваше приложение в компонент PWAProvider, и все необходимые механики будут подключены автоматически:
import { PWAProvider } from "next-pwa-pack";


export default function layout({ children }) {
 return <PWAProvider>{children}</PWAProvider>;
}
Для работы сервер экшена по ревалидации кеша дополнительно оберните приложение в HOC в файле /middleware.ts.
// /middleware.ts
import { withPWA } from "next-pwa-pack/hoc/withPWA";


function originalMiddleware(request) {
 // ...ваша логика
 return response;
}


export default withPWA(originalMiddleware, {
 revalidationSecret: process.env.REVALIDATION_SECRET!,
 sseEndpoint: "/api/pwa/cache-events",
 webhookPath: "/api/pwa/revalidate",
});


export const config = {
 matcher: ["/", "/(ru|en)/:path*", "/api/pwa/:path*"],
};
```


**Аргументы хока:**


- `originalMiddleware` — ваша функция middleware (например, для i18n, авторизации и т.д.)
- `revalidationSecret` — секрет для авторизации запросов на ревалидацию, нужен, чтобы никто кроме вас не мог в него стучаться
- `sseEndpoint` — endpoint для SSE-событий (по умолчанию `/api/pwa/cache-events`), измените его, если данный роут уже занят вашим приложением
- `webhookPath` — endpoint для webhook-ревалидации (по умолчанию `/api/pwa/revalidate`), по нему методом POST можно осуществлять ревалидацию кеша на сервере или извне, так же на нем завязана функция revalidatePWA


**Важно:**
В `config.matcher` обязательно укажите пути, которые должны обрабатываться этим middleware (например, корень сайта, локализованные маршруты и PWA endpoints).
Теперь вы можете вызывать revalidatePWA из server actions, server components или API routes для триггера PWA-ревалидации по URL.
На этом все! Остальное будет выполнять пакет.
Хотите разработать PWA-приложение? Приходите на консультацию!
Максим Бонцевич
CEO dev.family

Что внутри PWAProvider

PWAProvider объединяет несколько ключевых компонентов, которые обеспечивают полный PWA-функционал, автоматическое кэширование, синхронизацию и поддержку серверной ревалидации кэша, а так же devTools.

RegisterSW

Компонент RegisterSW автоматически регистрирует сервис-воркер. Он проверяет поддержку сервис-воркеров в браузере и регистрирует файл сервис-воркера (по умолчанию /sw.js). Если регистрация не удалась, в консоль выводится ошибка.

CacheCurrentPage

Компонент CacheCurrentPage отвечает за кэширование текущей страницы. Он перехватывает навигацию (включая SPA-переходы) и отправляет HTML-содержимое страницы в сервис-воркер для кэширования. Это позволяет приложению работать в офлайн-режиме и ускоряет повторные загрузки страниц.

SWRevalidateListener

Компонент SWRevalidateListener следит за изменениями в localStorage и обновляет кэш для указанных URL. Это обеспечивает синхронизацию кэша между разными вкладками.

SSERevalidateListener

SSERevalidateListener — слушает серверные события (Server-Sent Events, SSE) с endpoint'а (по умолчанию `/api/pwa/cache-events`). Он позволяет инициировать обновление кэша на клиенте по сигналу с сервера: например, после серверной мутации или внешнего webhook-а. Это ключевой элемент для поддержки серверной ревалидации кэша и интеграции с server actions, API routes и внешними системами.
SSERevalidateListener автоматически обновит кэш нужных страниц, если сервер отправит событие с типом `revalidate` и списком URL.

DevPWAStatus

Компонент DevPWAStatus предоставляет панель разработчика (включается через prop devMode в PWAProvider). Она отображает статус онлайн/оффлайн, наличие обновлений и позволяет выполнять различные действия:
import { PWAProvider } from "next-pwa-pack";


export default function layout({ children }) {
 return <PWAProvider devMode>{children}</PWAProvider>;
}
Он отображает статус онлайн/оффлайн, наличие обновлений и позволяет выполнять различные действия:
  • Очистка кэша;
  • Перезагрузка сервис-воркера;
  • Обновление кэша страницы;
  • Удаление сервис-воркера и кэша;
  • Включение/отключение кэша PWA.

Что делает сервис-воркер?

Сервис-воркер – это скрипт, который работает в фоновом режиме и управляет кэшем, сетью и обновлениями приложения. В next-pwa-pack сервис-воркер отвечает за следующие задачи.
Кэширование HTML-страниц
  • Кэширует HTML с TTL (время жизни) по умолчанию 10 минут. Можно изменить в файле /sw.js;
Если вам необходимо расположение файла сервис-воркера, новый путь можно указать в проп PWAProvider:
import { PWAProvider } from "next-pwa-pack";


export default function layout({ children }) {
 return <PWAProvider swPath="/some-path/sw.js">{children}</PWAProvider>
;
}
  • При истечении TTL автоматически обновляет кэш;
  • Показывает кэшированную версию при отсутствии интернета
Кэширование статических ресурсов
  • CSS, JavaScript, изображения кэшируются навсегда;
  • Улучшает скорость загрузки при повторных посещениях;
  • Работает только с GET-запросами (безопасность).
Обработка сообщений
Сервис-воркер обрабатывает 6 типов сообщений от клиента:
  • CACHE_CURRENT_HTML — кэширование текущей страницы;
  • REVALIDATE_URL – принудительное обновление кэша;
  • DISABLE_CACHE / ENABLE_CACHE – включение/отключение кэша;
  • SKIP_WAITING – активация новой версии;
  • CLEAR_STATIC_CACHE — сброс кэша статических данных и API-ответов (например, после серверной ревалидации или обновления данных через SSE);
Офлайн-режим
  • Автоматически показывает offline.html при отсутствии интернета и кеша текущей страницы;
  • Пытается загрузить свежую версию при восстановлении соединения.

HOC withPWA

Интеграция с SSR/Edge middleware. Поддерживает серверные экшены и SSE для обновления кэша. Сервер может отправлять события ревалидации, которые будут обработаны клиентом для обновления кэша.
export default withPWA(originalMiddleware, {
 revalidationSecret: process.env.REVALIDATION_SECRET!,
 sseEndpoint: "/api/pwa/cache-events",
 webhookPath: "/api/pwa/revalidate",
});


- `originalMiddleware` — ваша функция middleware (например, для i18n, авторизации и т.д.)
- `revalidationSecret` — секрет для авторизации запросов на ревалидацию, нужен, чтобы никто кроме вас не мог в него стучаться
- `sseEndpoint` — endpoint для SSE-событий (по умолчанию `/api/pwa/cache-events`), измените его, если данный роут уже занят вашим приложением
- `webhookPath` — endpoint для webhook-ревалидации (по умолчанию `/api/pwa/revalidate`), по нему методом POST можно осуществлять ревалидацию кеша на сервере или извне, так же на нем завязана функция revalidatePWA

Примеры использования

Также покажу несколько реальных сценариев работы пакета.

Обновление кэша после изменения данных

import { updateSWCache } from "next-pwa-pack";


// После успешного создания поста
const handleCreatePost = async (data) => {
 await createPost(data);
 // Обновляем кэш вкладок с блогом и дашбордом
 updateSWCache(["/blog", "/dashboard"]);
};
Если требуется обновление данных на сервере:
import { revalidatePWA } from "../actions";


await createPost(data);
await revalidatePWA(["/my-page"]);

Очистка кэша при выходе пользователя

import { clearAllCache } from "next-pwa-pack";


const handleLogout = async () => {
 await logout();
 await clearAllCache(); // Очищаем все кэши
 router.push("/login");
};

Уведомление о новой версии

import { usePWAStatus } from "next-pwa-pack";


function UpdateNotification() {
 const { hasUpdate, update } = usePWAStatus();
  if (hasUpdate) {
   return (
     <div className="update-banner">
       <p>Доступна новая версия приложения</p>
       <button onClick={update}>Обновить</button>
     </div>
   );
 }
  return null;
}

Краткое описание всех экспортируемых экшенов

import {
 clearAllCache,
 reloadServiceWorker,
 updatePageCache,
 unregisterServiceWorkerAndClearCache,
 updateSWCache,
 disablePWACache,
 enablePWACache,
clearStaticCache,
 usePWAStatus,
} from "next-pwa-pack";


// Очищает все кэши, связанные с сервис-воркером.
await clearAllCache();


// Перезагружает сервис-воркер и обновляет страницу.
await reloadServiceWorker();


// Обновляет кэш для указанной страницы (или текущей, если не указано).
await updatePageCache("/about");


// Отключает сервис-воркер и очищает кэш.
await unregisterServiceWorkerAndClearCache();


// Запускает сигнал для всех вкладок и обновляет кэш в текущей вкладке.
// Может быть вызван после revalidateTag на клиенте.


// Сбрасывает кэш статических данных и API-ответов.
await clearStaticCache();


updateSWCache(["/page1", "/page2"]);


// Глобально отключает кэш PWA (до перезагрузки или вызова enablePWACache).
disablePWACache();


// Глобально включает кэш PWA (после вызова disablePWACache).
enablePWACache();


const { online, hasUpdate, swInstalled, update } = usePWAStatus();
// - `online` — онлайн/оффлайн статус
// - `hasUpdate` — доступно ли обновление
// - `swInstalled` — установлен ли сервис-воркер
// - `update()` — активировать новую версию приложения

Пример: API Route для внешней ревалидации

Иногда требуется обновить кэш сразу извне, при изменении респонсов, не дожидаясь «просрочки» времени кеша. Например, после изменения данных в админ-панели. Для этого можно создать API Route  наподобии этого:
// app/api/webhook/revalidate/route.ts


import { NextRequest, NextResponse } from "next/server";
import { revalidatePWA } from "@/app/actions";
import { revalidateTag } from "next/cache";


import { FetchTags } from "@/app/api/endpoints/backend";


interface RevalidateRequest {
 tags?: string[];
 secret: string;
 urls?: string[];
}


interface RevalidateResponse {
 success: boolean;
 message: string;
 tagsRevalidated: boolean;
 urlsRevalidated: boolean;
 tags: string[];
 urls: string[];
 successful: number;
 failed: number;
 timestamp: string;
}


export async function POST(request: NextRequest) {
 try {
   const { tags, secret, urls }: RevalidateRequest = await request.json();


   if (secret !== process.env.REVALIDATION_SECRET) {
     return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
   }


   let successful = 0;
   let failed = 0;
   let tagsRevalidated = false;
   let urlsRevalidated = false;
   const validTags = Object.values(FetchTags);
   const invalidTags =
     tags?.filter((tag) => !validTags.includes(tag as any)) || [];


   if (invalidTags.length > 0) {
     return NextResponse.json(
       { error: `Invalid tags: ${invalidTags.join(", ")}` },
       { status: 400 }
     );
   }


   if (tags && tags.length > 0) {
     const tagResults = await Promise.allSettled(
       tags.map((tag) => revalidateTag(tag as FetchTags))
     );
     successful = tagResults.filter((r) => r.status === "fulfilled").length;
     failed = tagResults.filter((r) => r.status === "rejected").length;
     tagsRevalidated = true;
   }


   if (urls && urls.length > 0) {
     await revalidatePWA(urls);
     urlsRevalidated = true;
   }


   const response: RevalidateResponse = {
     success: true,
     message: "Cache revalidation completed",
     tagsRevalidated,
     urlsRevalidated,
     tags: tags || [],
     urls: urls || [],
     successful,
     failed,
     timestamp: new Date().toISOString(),
   };


   return NextResponse.json(response);
 } catch (error) {
   console.error("Webhook revalidation error:", error);
   return NextResponse.json(
     { error: "Internal server error" },
     { status: 500 }
   );
 }
}
Теперь в приложение можно «постучаться» снаружи. Например, так:
POST:  https://my-app(или localhost:3000)/api/webhook/revalidate


body: {
 "tags": ["faq"],
 "secret": "1234567890",
 "urls": ["/ru/question-answer"]
}


response: {
 "success": true,
 "message": "Cache revalidation completed",
 "tagsRevalidated": true,
 "urlsRevalidated": true,
 "tags": [
   "faq"
 ],
 "urls": [
   "/ru/question-answer"
 ],
 "successful": 1,
 "failed": 0,
 "timestamp": "2025-07-21T12:43:47.819Z"
}

Отладка и мониторинг

Вот, что происходит на этом этапе.
Проверка работы кэша:
  1. Откройте DevTools → Application → Service Workers.
  2. Убедитесь, что сервис-воркер зарегистрирован.
  3. Перейдите в Cache Storage → html-cache-v2.
  4. Проверьте, что страницы кэшируются.
Тестирование офлайн-режима:
  1. Включите devMode в PWAProvider.
  2. Откройте панель разработчика (зеленая точка в левом нижнем углу).
  3. Отключите интернет в DevTools → Network → Offline.
  4. Обновите страницу — должна показаться offline.html.
Логи в консоли
Сервис-воркер выводит подробные логи:
  • [PWA] Service Worker registered — успешная регистрация;
  • [SW] Cached: /about — страница закэширована;
  • [SW] Revalidated and updated cache for: /blog — кэш обновлен.

Ограничения и особенности пакета

И еще несколько нюансов, о которых важно знать перед установкой.
Безопасность
  • HTTPS обязателен для PWA в продакшене;
  • Кэшируются только GET-запросы (API-вызовы не кэшируются);
  • Чувствительные данные не сохраняются в кэше.
Производительность
  • Пакет не влияет на производительность в обычном режиме;
  • Улучшает скорость загрузки при повторных посещениях.
Настройка
  • TTL кэша (10 минут) можно изменить только в sw.js;
  • Исключения из кэширования настраиваются в CACHE_EXCLUDE;
  • Manifest.json требует ручной настройки под проект.
  • Серверный экшен revalidatePWA копируется в проект и его можно редактировать;
  • HOC withPWA имеет свои пропсы для более гибкой настройки под проект;
  • PWAProvider имеет свои пропсы для управления:
export default function PWAProvider({
 children,
 swPath,
 devMode = false,
 serverRevalidation = { enabled: true, sseEndpoint: "/api/pwa/cache-events" },
}: PWAProviderProps) {
next-pwa-pack — это быстрый и удобный способ превратить ваше Next.js-приложение в полноценное PWA. Пакет решает все основные проблемы, связанные с внедрением PWA, и предоставляет простой API для управления кэшем и сервис-воркером.
В следующих версиях мы планируем добавить следующие возможности:
  • Настройка TTL через конфиг — без редактирования sw.js;
  • Поддержка push-уведомлений — для уведомлений пользователей;
  • Более гибкая настройка кэширования — по паттернам URL;
  • Метрики производительности — мониторинг эффективности кэширования.
Пакет был написан под Next.js 15, и у нас не было времени тестировать на предыдущих версиях. Но он должен корректно работать с Next.js 13 App Router.
Спасибо за внимание!
Остались вопросы по установке или использованию пакета? Отправьте их нам!
Читайте также
Инструкции
1 февраля 2022
2 минуты

Как добавить карту в Apple Wallet на React Native?

Мы – фанаты кросс-платформенных приложений. В частности, написанных на React Native. Проекты, которые мы делали с его помощью, всегда были совершенно разными. Поэтому нам редко удавалось переиспользовать фрагменты кода. Иногда на помощь приходили библиотеки с открытым исходным кодом, а иногда нам приходилось писать их самим. И эта библиотека – одна из них.

Маша, mobile-разработчик