Полное руководство по настройке и использованию PWA функциональности в Killerpool.
Последнее обновление: 2026-07-07
- Что реализовано
- Архитектура PWA
- Service Worker
- Офлайн режим
- Офлайн-синхронизация (pending sync retry)
- Иконки и манифест
- Установка на устройства
- Тестирование
- Troubleshooting
- Planned / Not implemented
- ✅ Service Worker (
@ducanh2912/next-pwa+ Workbox) с регистрацией изcomponents/pwa-init.tsx - ✅ Офлайн режим - игра полностью работает без интернета (состояние в localStorage)
- ✅ Кеширование ресурсов - статика, Supabase API, изображения, видео
- ✅ Retry-синхронизация - неудачные синки игр помечаются pending и повторяются при восстановлении связи (
retryPendingSyncs()изlib/sync.ts) - ✅ Установка на устройства - iOS, Android, Desktop
- ✅ Manifest.json с полной конфигурацией
- ✅ Иконки - 16x16 до 512x512, генерируются из
public/icon.svg - ✅ Офлайн fallback страница -
/offline
⚠️ Background Sync API иlib/sync-manager.ts(очередьkillerpool_sync_queue) удалены — вместо них простой retry-механизм на localStorage, см. раздел Офлайн-синхронизация.
⚠️ Production-сборка обязана идти через webpack (next build --webpack, так настроенnpm run build):@ducanh2912/next-pwaгенерируетsw.jsчерез webpack-хук, который Turbopack не выполняет. Turbopack-сборка молча выпускает приложение без service worker — регистрация/sw.jsпадает 404, офлайн-кеширование не работает.
Все стратегии заданы в workboxOptions.runtimeCaching в next.config.js:
| Тип ресурса | Стратегия | Кеш | Детали |
|---|---|---|---|
Google Fonts (fonts.gstatic.com, fonts.googleapis.com) |
CacheFirst | google-fonts |
365 дней, до 4 записей |
Supabase API (*.supabase.co) |
NetworkFirst | supabase-api |
24 часа, до 32 записей, network timeout 10s |
| Изображения (jpg/jpeg/gif/png/svg/ico/webp) | CacheFirst | static-image-assets |
24 часа, до 64 записей |
JavaScript (.js) |
StaleWhileRevalidate | static-js-assets |
24 часа, до 32 записей |
CSS (.css) |
StaleWhileRevalidate | static-css-assets |
24 часа, до 32 записей |
JSON данные (https://*.json) |
NetworkFirst | static-data-assets |
24 часа, до 32 записей |
Next.js Images (/_next/image?url=...) |
CacheFirst | next-image |
24 часа, до 64 записей |
| Видео (mp4/webm) | CacheFirst | static-video-assets |
24 часа, до 32 записей |
killerpool/
├── next.config.js # Конфигурация @ducanh2912/next-pwa + runtimeCaching
├── public/
│ ├── manifest.json # PWA манифест
│ ├── icon.svg # Исходная иконка
│ ├── icon-*.png # Сгенерированные иконки
│ └── sw.js # Service Worker (генерируется при build)
├── app/
│ ├── layout.tsx # PWA мета-теги, manifest, viewport themeColor
│ └── offline/
│ └── page.tsx # Офлайн страница
├── components/
│ └── pwa-init.tsx # Регистрация SW + retry pending-синков
├── lib/
│ ├── sync.ts # autoSyncGame / retryPendingSyncs / syncActiveGameToSupabase
│ └── storage.ts # killerpool_pending_sync (mark/unmark/getPendingSyncIds)
└── scripts/
└── generate-icons.js # Генерация иконок из icon.svg (sharp)
1. User загружает приложение
↓
2. PWAInit (components/pwa-init.tsx) регистрирует /sw.js
и сразу вызывает retryPendingSyncs()
↓
3. Workbox кеширует статику и API-ответы
↓
4. Приложение работает офлайн (игра — в localStorage)
↓
5. При событиях 'online' / 'visibilitychange' → retryPendingSyncs()
досинхронизирует завершённые офлайн игры в Supabase
Service Worker настраивается через next.config.js:
const withPWA = require('@ducanh2912/next-pwa').default({
dest: 'public',
disable: process.env.NODE_ENV === 'development',
register: true,
scope: '/',
sw: 'sw.js',
cacheOnFrontEndNav: true,
aggressiveFrontEndNavCaching: true,
reloadOnOnline: true,
swcMinify: true,
workboxOptions: {
disableDevLogs: true,
runtimeCaching: [ /* см. next.config.js */ ],
},
});Важные опции:
disable: process.env.NODE_ENV === 'development'— в dev-режиме SW не генерируется, тестировать нужно на production-билдеcacheOnFrontEndNav+aggressiveFrontEndNavCaching— кеширование при клиентской навигацииreloadOnOnline— автоматическая перезагрузка страницы при восстановлении связи
Помимо register: true, регистрация продублирована в components/pwa-init.tsx (клиентский компонент, монтируется в app/layout.tsx):
navigator.serviceWorker.register('/sw.js')- слушает
updatefound/statechange— логирует появление новой версии SW - слушает
beforeinstallprompt/appinstalled(сохраняет deferred prompt, кастомная UI-кнопка установки пока не показывается)
- Install - Service Worker устанавливается и прекеширует ресурсы
- Activate - Очистка старых кешей
- Fetch - Обработка запросов согласно runtimeCaching стратегиям
- Состояние игры живёт на клиенте (localStorage:
killerpool_current_game,killerpool_game_history) — играть можно полностью офлайн - Service Worker перехватывает сетевые запросы и отдаёт из кеша по стратегиям выше
- Если страницы нет в кеше → показ
/offlineстраницы. Это работает благодаря явной опцииfallbacks: { document: '/offline' }в конфиге@ducanh2912/next-pwa(next.config.js) — автоопределение fallback-документа у библиотеки срабатывает только дляapp/~offline/page.*, а наша страница лежит вapp/offline/page.tsx
Находится в app/offline/page.tsx (client component):
- Показывает статус подключения (
navigator.onLine+ событияonline/offline) - При восстановлении связи автоматически редиректит на
/через 1 секунду - Кнопка "Повторить попытку" (
router.push('/')если онлайн, иначеwindow.location.reload()) - Блок с информацией об офлайн режиме и автоматической синхронизации
- Откройте приложение в Chrome DevTools
- Application → Service Workers → проверьте регистрацию
- Network → Offline checkbox
- Обновите страницу → приложение должно работать из кеша
Заменяет прежний Background Sync API +
lib/sync-manager.ts— эта подсистема удалена. Никакогоsync-события в SW и очередиkillerpool_sync_queueбольше нет. REST-эндпоинтов/api/games*в проекте тоже нет — вся синхронизация идёт напрямую через Supabase JS-клиент.
- После завершения игры вызывается
autoSyncGame(game)(lib/sync.ts) — upsert игры в таблицуgamesSupabase - Если синк не удался (например, офлайн) → id игры помечается в localStorage-ключе
killerpool_pending_sync(markPendingSync,lib/storage.ts) retryPendingSyncs()проходит по pending-ids, находит игры в истории и повторяетsyncGameToSupabase(game); успешные — снимаются с pending (unmarkPendingSync)- Игры, удалённые из истории, просто снимаются с pending
// lib/sync.ts
export async function autoSyncGame(game: Game): Promise<void>
export async function retryPendingSyncs(): Promise<void>
export async function syncGameToSupabase(game: Game): Promise<boolean>
// lib/storage.ts (ключ killerpool_pending_sync)
export function getPendingSyncIds(): string[]
export function markPendingSync(gameId: string): void
export function unmarkPendingSync(gameId: string): voidcomponents/pwa-init.tsx вызывает retryPendingSyncs():
- при монтировании приложения
- на событии
windowonline - на
documentvisibilitychange(если вкладка стала видимой иnavigator.onLine)
retryPendingSyncs() сам выходит рано, если выполняется на сервере или navigator.onLine === false.
Страница /sync (app/sync/page.tsx) позволяет вручную выгрузить всю локальную историю игр в Supabase через syncAllGamesToSupabase().
Иконки генерируются из public/icon.svg библиотекой sharp:
node scripts/generate-icons.jsСоздаются следующие файлы (см. scripts/generate-icons.js):
icon-192.png- Android (192x192)icon-512.png- Android (512x512)apple-touch-icon.png- iOS (180x180)favicon-32x32.png- Browserfavicon-16x16.png- Browserfavicon.ico- Browser (32x32)
Актуальные поля public/manifest.json:
{
"name": "Killerpool - Modern Killer Pool Game",
"short_name": "Killerpool",
"start_url": "/",
"display": "standalone",
"background_color": "#0a0f0d",
"theme_color": "#10b981",
"orientation": "portrait",
"scope": "/",
"categories": ["games", "entertainment", "sports"],
"lang": "ru"
}Иконки в манифесте: icon-192.png и icon-512.png с purpose: "any maskable", плюс apple-touch-icon.png (180x180, purpose: "any").
app/layout.tsx подключает PWA-метаданные:
metadata.manifest: '/manifest.json'metadata.appleWebApp(capable: true,title: 'Killerpool')viewport.themeColor: '#10b981'<link rel="icon">для favicon.ico / 16x16 / 32x32,<link rel="apple-touch-icon"><meta name="apple-mobile-web-app-capable" content="yes">,apple-mobile-web-app-status-bar-style: black-translucent,mobile-web-app-capable,format-detection: telephone=no
OG/Twitter-картинки — динамические route-файлы app/opengraph-image.tsx и app/twitter-image.tsx (статического og-image.png нет).
- Откройте сайт в Safari
- Нажмите кнопку "Поделиться" (квадрат со стрелкой вверх)
- Прокрутите вниз → "На экран Домой"
- Нажмите "Добавить"
Требования:
- iOS 11.3+
- Safari браузер
- Иконка
apple-touch-icon.png
- Откройте сайт в Chrome
- Нажмите меню (три точки)
- "Установить приложение" / "Add to Home screen"
- Подтвердите установку
Требования:
- Chrome 68+
- manifest.json
- Service Worker
- HTTPS
- Откройте сайт в Chrome/Edge
- Справа в адресной строке появится иконка установки
- Нажмите на иконку
- Подтвердите установку
npm run build
npm run start
# В Chrome DevTools:
# 1. Lighthouse → PWA
# 2. Generate reportПомните: в dev-режиме (
npm run dev) SW отключён (disable: NODE_ENV === 'development').
// В консоли браузера
navigator.serviceWorker.getRegistration()
.then(reg => console.log('SW registered:', reg))
// Проверка кешей
caches.keys().then(keys => console.log('Cache keys:', keys))-
Chrome DevTools:
- Application → Service Workers → проверьте статус
- Network → Offline
- Обновите страницу
-
Pending sync:
// В консоли — посмотреть очередь pending-игр JSON.parse(localStorage.getItem('killerpool_pending_sync') || '[]')
Завершите игру офлайн → id появится в
killerpool_pending_sync→ включите сеть (или вернитесь на вкладку) → массив должен опустеть, игра появится в Supabase. -
Manifest:
- DevTools → Application → Manifest
- Проверьте все поля
- Проверьте иконки
- Установите приложение на домашний экран
- Откройте установленное приложение
- Проверьте:
- Полноэкранный режим (без адресной строки Safari)
- Статус-бар (
black-translucent) - Иконка приложения
Проблема: navigator.serviceWorker is undefined
Решение:
- Убедитесь, что используете HTTPS (или localhost)
- Убедитесь, что это production-билд — в dev SW отключён
- Откройте DevTools → Console для ошибок
Проблема: Старая версия приложения после деплоя
Решение:
// Обновить Service Worker
navigator.serviceWorker.getRegistrations()
.then(regs => regs.forEach(reg => reg.update()))
// Очистить все кеши
caches.keys().then(keys =>
Promise.all(keys.map(key => caches.delete(key)))
)Проблема: Завершённая офлайн игра не появилась в Supabase
Решение:
- Проверьте pending-очередь:
JSON.parse(localStorage.getItem('killerpool_pending_sync') || '[]')
- Убедитесь, что игра осталась в
killerpool_game_history— retry берёт данные оттуда (getGameFromHistory), удалённые из истории игры снимаются с pending без синка - Триггерните retry вручную: уйдите с вкладки и вернитесь (событие
visibilitychange) либо перезагрузите страницу —retryPendingSyncs()вызывается при монтированииPWAInit - Проверьте консоль на ошибки Supabase (RLS, сеть)
Проблема: На iOS нет автоматического prompt
Решение:
- iOS не показывает автоматический prompt
- Пользователь должен вручную добавить через Safari menu
- Можно показать инструкцию в UI
Проблема: DevTools показывает "No manifest detected"
Решение:
- Manifest подключается через
metadata.manifestвapp/layout.tsx - Убедитесь, что
manifest.jsonдоступен по пути/manifest.json - Проверьте Content-Type: должен быть
application/manifest+json
Цель: 100/100
Критерии:
- ✅ Fast and reliable
- ✅ Installable
- ✅ PWA optimized
- LCP (Largest Contentful Paint): < 2.5s
- INP (Interaction to Next Paint): < 200ms
- CLS (Cumulative Layout Shift): < 0.1
Перед деплоем убедитесь:
- Service Worker регистрируется корректно (production-билд)
- Все иконки сгенерированы и доступны (
node scripts/generate-icons.js) - Manifest.json корректен и доступен
- Офлайн страница работает
- Retry-синк работает: игра, завершённая офлайн, попадает в Supabase после восстановления связи
- Lighthouse PWA score = 100
- Тестирование на iOS Safari
- Тестирование на Android Chrome
- HTTPS включен
- Кеш-стратегии настроены корректно
- Share Target: объявление
share_targetубрано изmanifest.json— оно указывало на несуществующий route/share, из-за чего Android показывал приложение в системном share-меню, а шаринг приводил к 404 - UI-кнопка установки:
beforeinstallpromptперехватывается вpwa-init.tsx, но кастомная кнопка установки не показывается (deferred prompt сохраняется и не используется) - Уведомление о новой версии SW: событие
updatefoundлогируется в консоль; toast для пользователя не реализован - Screenshots в манифесте: массив
screenshotsпуст
Последнее обновление: 2026-07-07 Статус: ✅ PWA работает; офлайн-синк переведён с Background Sync API на простой retry-механизм