React PWA: Progressive Web App с React

16 октября 2026
Автор

Антон Ларичев

Что такое Progressive Web App

Progressive Web App (PWA) — это веб-приложение, которое использует современные браузерные API для обеспечения возможностей, ранее доступных только нативным приложениям: работа офлайн, push-уведомления, установка на устройство, быстрая загрузка.

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

Ключевые характеристики PWA:

  • Надёжность — работает офлайн или при нестабильном соединении
  • Скорость — быстрый запуск благодаря кэшированию
  • Нативность — выглядит и ведёт себя как нативное приложение

Курс по теме

Пройти курс React и Redux Toolkit

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Технические основы PWA

PWA строится на трёх столпах:

  1. Service Worker — скрипт, работающий в фоновом потоке, отдельно от основного потока браузера. Перехватывает сетевые запросы, управляет кэшем, обрабатывает push-уведомления.

  2. Web App Manifest — JSON-файл, описывающий приложение: название, иконки, цвета, режим отображения.

  3. HTTPS — PWA работают только по защищённому соединению (исключение — localhost для разработки).

Создание React PWA с Vite

Самый простой способ начать — использовать плагин vite-plugin-pwa.

npm create vite@latest my-pwa -- --template react-ts
cd my-pwa
npm install
npm install -D vite-plugin-pwa

Настройка vite.config.ts:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { VitePWA } from 'vite-plugin-pwa'

export default defineConfig({
  plugins: [
    react(),
    VitePWA({
      registerType: 'autoUpdate',
      manifest: {
        name: 'My React PWA',
        short_name: 'ReactPWA',
        description: 'Моё Progressive Web приложение на React',
        theme_color: '#ffffff',
        background_color: '#ffffff',
        display: 'standalone',
        start_url: '/',
        icons: [
          {
            src: '/icons/icon-192.png',
            sizes: '192x192',
            type: 'image/png'
          },
          {
            src: '/icons/icon-512.png',
            sizes: '512x512',
            type: 'image/png'
          }
        ]
      },
      workbox: {
        globPatterns: ['**/*.{js,css,html,ico,png,svg}'],
        runtimeCaching: [
          {
            urlPattern: /^https:\/\/api\.example\.com\/.*/i,
            handler: 'NetworkFirst',
            options: {
              cacheName: 'api-cache',
              expiration: {
                maxEntries: 100,
                maxAgeSeconds: 60 * 60 * 24
              }
            }
          }
        ]
      }
    })
  ]
})

Web App Manifest

Если вы настраиваете манифест вручную, создайте файл public/manifest.json:

{
  "name": "My React PWA",
  "short_name": "ReactPWA",
  "description": "Моё Progressive Web приложение",
  "start_url": "/",
  "display": "standalone",
  "orientation": "portrait",
  "background_color": "#ffffff",
  "theme_color": "#3b82f6",
  "icons": [
    {
      "src": "/icons/icon-72.png",
      "sizes": "72x72",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png",
      "purpose": "maskable any"
    },
    {
      "src": "/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png",
      "purpose": "maskable any"
    }
  ],
  "categories": ["productivity"],
  "lang": "ru"
}

Подключите манифест в index.html:

<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#3b82f6" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<link rel="apple-touch-icon" href="/icons/icon-192.png" />

Параметры display

Значение Описание
standalone Похоже на нативное приложение, без адресной строки
fullscreen Полный экран, без UI браузера
minimal-ui Минимальный UI браузера (кнопки навигации)
browser Обычная вкладка браузера

Service Worker

Service Worker — это прокси между браузером и сетью. Он регистрируется отдельно и работает даже когда страница закрыта.

Жизненный цикл Service Worker

Install → Activate → Fetch/Sync/Push
  • Install — скачивание и кэширование ресурсов
  • Activate — очистка старого кэша, Service Worker готов к работе
  • Fetch — перехват сетевых запросов

Ручное создание Service Worker

Создайте файл public/sw.js:

const CACHE_NAME = 'app-v1'
const STATIC_ASSETS = [
  '/',
  '/index.html',
  '/static/js/main.chunk.js',
  '/static/css/main.chunk.css',
  '/icons/icon-192.png'
]

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(STATIC_ASSETS)
    })
  )
  self.skipWaiting()
})

self.addEventListener('activate', (event) => {
  event.waitUntil(
    caches.keys().then((cacheNames) => {
      return Promise.all(
        cacheNames
          .filter((name) => name !== CACHE_NAME)
          .map((name) => caches.delete(name))
      )
    })
  )
  self.clients.claim()
})

self.addEventListener('fetch', (event) => {
  if (event.request.method !== 'GET') return

  event.respondWith(
    caches.match(event.request).then((cachedResponse) => {
      if (cachedResponse) {
        return cachedResponse
      }
      return fetch(event.request).then((response) => {
        if (!response || response.status !== 200) {
          return response
        }
        const responseToCache = response.clone()
        caches.open(CACHE_NAME).then((cache) => {
          cache.put(event.request, responseToCache)
        })
        return response
      })
    })
  )
})

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

Cache First — сначала кэш, при промахе — сеть. Подходит для статических ресурсов (иконки, шрифты).

Network First — сначала сеть, при ошибке — кэш. Подходит для API-запросов.

Stale While Revalidate — возвращает кэш немедленно, параллельно обновляет его из сети. Баланс скорости и актуальности.

// Network First с fallback на кэш
self.addEventListener('fetch', (event) => {
  if (event.request.url.includes('/api/')) {
    event.respondWith(
      fetch(event.request)
        .then((response) => {
          const clone = response.clone()
          caches.open('api-cache').then((cache) => {
            cache.put(event.request, clone)
          })
          return response
        })
        .catch(() => caches.match(event.request))
    )
  }
})

Регистрация Service Worker в React

Создайте хук для управления Service Worker:

import { useEffect, useState } from 'react'

interface ServiceWorkerState {
  isInstalled: boolean
  isUpdating: boolean
  updateAvailable: boolean
  update: () => void
}

export function useServiceWorker(): ServiceWorkerState {
  const [isInstalled, setIsInstalled] = useState(false)
  const [isUpdating, setIsUpdating] = useState(false)
  const [updateAvailable, setUpdateAvailable] = useState(false)
  const [waitingWorker, setWaitingWorker] = useState<ServiceWorker | null>(null)

  useEffect(() => {
    if (!('serviceWorker' in navigator)) return

    navigator.serviceWorker
      .register('/sw.js')
      .then((registration) => {
        setIsInstalled(true)

        registration.addEventListener('updatefound', () => {
          const newWorker = registration.installing
          if (!newWorker) return

          newWorker.addEventListener('statechange', () => {
            if (
              newWorker.state === 'installed' &&
              navigator.serviceWorker.controller
            ) {
              setUpdateAvailable(true)
              setWaitingWorker(newWorker)
            }
          })
        })
      })
      .catch(console.error)
  }, [])

  const update = () => {
    if (!waitingWorker) return
    setIsUpdating(true)
    waitingWorker.postMessage({ type: 'SKIP_WAITING' })
    window.location.reload()
  }

  return { isInstalled, isUpdating, updateAvailable, update }
}

Использование хука для показа уведомления об обновлении:

import { useServiceWorker } from './hooks/useServiceWorker'

export function UpdateNotification() {
  const { updateAvailable, update } = useServiceWorker()

  if (!updateAvailable) return null

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

Кнопка установки PWA

Браузер генерирует событие beforeinstallprompt, когда приложение соответствует критериям установки:

import { useEffect, useState } from 'react'

interface BeforeInstallPromptEvent extends Event {
  prompt: () => Promise<void>
  userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>
}

export function useInstallPWA() {
  const [installPrompt, setInstallPrompt] =
    useState<BeforeInstallPromptEvent | null>(null)
  const [isInstalled, setIsInstalled] = useState(false)

  useEffect(() => {
    const handler = (e: Event) => {
      e.preventDefault()
      setInstallPrompt(e as BeforeInstallPromptEvent)
    }

    window.addEventListener('beforeinstallprompt', handler)

    window.addEventListener('appinstalled', () => {
      setIsInstalled(true)
      setInstallPrompt(null)
    })

    // Проверка: уже установлено как standalone
    if (window.matchMedia('(display-mode: standalone)').matches) {
      setIsInstalled(true)
    }

    return () => window.removeEventListener('beforeinstallprompt', handler)
  }, [])

  const install = async () => {
    if (!installPrompt) return
    await installPrompt.prompt()
    const { outcome } = await installPrompt.userChoice
    if (outcome === 'accepted') {
      setInstallPrompt(null)
    }
  }

  return { canInstall: !!installPrompt, isInstalled, install }
}

Компонент кнопки:

import { useInstallPWA } from './hooks/useInstallPWA'

export function InstallButton() {
  const { canInstall, isInstalled, install } = useInstallPWA()

  if (isInstalled) {
    return <span>Приложение установлено</span>
  }

  if (!canInstall) return null

  return (
    <button onClick={install} className="install-btn">
      Установить приложение
    </button>
  )
}

Определение статуса соединения

import { useEffect, useState } from 'react'

export function useNetworkStatus() {
  const [isOnline, setIsOnline] = useState(navigator.onLine)

  useEffect(() => {
    const handleOnline = () => setIsOnline(true)
    const handleOffline = () => setIsOnline(false)

    window.addEventListener('online', handleOnline)
    window.addEventListener('offline', handleOffline)

    return () => {
      window.removeEventListener('online', handleOnline)
      window.removeEventListener('offline', handleOffline)
    }
  }, [])

  return isOnline
}
import { useNetworkStatus } from './hooks/useNetworkStatus'

export function OfflineBanner() {
  const isOnline = useNetworkStatus()

  if (isOnline) return null

  return (
    <div className="offline-banner">
      Нет подключения к интернету. Показаны кэшированные данные.
    </div>
  )
}

Проверка PWA-критериев

Chrome DevTools предоставляет вкладку Lighthouse для аудита PWA. Минимальные требования для установки:

  • Наличие Web App Manifest с обязательными полями
  • Service Worker зарегистрирован и контролирует страницу
  • Иконки не менее 192x192 и 512x512
  • Страница доступна по HTTPS
  • Страница работает офлайн (возвращает хотя бы 200 при отключённой сети)

Проверить статус Service Worker можно в DevTools:

Application > Service Workers

Для отладки кэша:

Application > Cache Storage

Workbox: промышленный подход

Workbox — библиотека от Google для работы с Service Worker. vite-plugin-pwa использует Workbox под капотом.

Ручная настройка стратегий с Workbox:

import { precacheAndRoute } from 'workbox-precaching'
import { registerRoute } from 'workbox-routing'
import { NetworkFirst, CacheFirst, StaleWhileRevalidate } from 'workbox-strategies'
import { ExpirationPlugin } from 'workbox-expiration'

// Precache — кэшировать все статические ресурсы при установке
precacheAndRoute(self.__WB_MANIFEST)

// API запросы: Network First
registerRoute(
  ({ url }) => url.pathname.startsWith('/api/'),
  new NetworkFirst({
    cacheName: 'api-responses',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 50,
        maxAgeSeconds: 5 * 60
      })
    ]
  })
)

// Изображения: Cache First
registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 60,
        maxAgeSeconds: 30 * 24 * 60 * 60
      })
    ]
  })
)

// Шрифты Google: Stale While Revalidate
registerRoute(
  ({ url }) => url.origin === 'https://fonts.googleapis.com',
  new StaleWhileRevalidate({ cacheName: 'google-fonts' })
)

Распространённые ошибки

Service Worker не обновляется — браузер использует старый SW, если не вызван skipWaiting(). Убедитесь, что ваш SW при получении сообщения SKIP_WAITING вызывает self.skipWaiting().

self.addEventListener('message', (event) => {
  if (event.data?.type === 'SKIP_WAITING') {
    self.skipWaiting()
  }
})

Кэш не инвалидируется — при изменении файлов меняйте имя кэша (CACHE_NAME = 'app-v2'). Workbox делает это автоматически через хэши файлов.

CORS при кэшировании — нельзя кэшировать opaque-ответы (cross-origin без CORS) стратегией Cache First: при ошибке сервера в кэш попадёт ответ со статусом 0.

iOS Safari — не поддерживает push-уведомления для PWA (до Safari 16.4). Учитывайте это при планировании функциональности.

Итог

PWA на React — это прагматичный способ улучшить пользовательский опыт без разработки нативных приложений. Service Worker даёт офлайн-работу и кэширование, Web App Manifest — установку на устройство, а Workbox упрощает управление стратегиями кэширования.

Для большинства проектов оптимальный старт — vite-plugin-pwa с конфигурацией generateSW: он автоматически создаёт Service Worker на основе Workbox, прекэширует статику и позволяет гибко настраивать стратегии для динамических данных.

Чтобы углубить знания React и научиться строить production-ready приложения, пройдите курс по React на PurpleSchool: https://purpleschool.ru/course/react?utm_source=knowledgebase&utm_medium=text&utm_campaign=react-pwa

Стрелочка влевоReact с WebAssembly: интеграция Wasm-модулейReact DevTools: отладка и профилирование компонентовСтрелочка вправо

Постройте личный план изучения React до уровня Middle — бесплатно!

React — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по React

React тестирование с Vitest
React с TanStack Router
Zustand: продвинутые паттерны, persist middleware и slicesUncontrolled Components: когда DOM управляет даннымиБезопасность в React: защита от XSS, CSRF и утечек данныхRender Props: гибкое управление рендерингом в ReactРефакторинг React-кода: техники и лучшие практикиReact с WebSocket — реалтайм в компонентахReact Native приложение с нуля до публикации в сторахПрофилирование React: как найти и устранить узкие местаЧастичное применение: как создавать компоненты без лишнего кодаИменование компонентов в React: соглашения и лучшие практикиЛенивая загрузка: как ускорить React-приложение в разыОшибки гидратации в React и способы их устраненияHOC в React: мастерство композиции компонентовАрхитектура больших React приложений: Feature-Sliced DesignuseMemo: как спасти производительность от тяжелых вычисленийError Boundaries: создаем надежные React-приложенияКонтролируемые компоненты в React: полный контроль над формамиCompound Components в React: создаем гибкие компоненты с мощным APIДокументирование компонентов в React: Storybook, JSDoc и READMEКомпозиция компонентов в React: строим гибкие интерфейсыКомментирование кода в React: когда и как писать комментарииCode Splitting в React: как уменьшить бандл и ускорить загрузку приложенияАсинхронные компоненты в React: новый стандарт работы с даннымиДоступность (a11y) в React: ARIA, семантика и клавиатурная навигация
Zustand — управление состоянием в ReactZod - валидация с TypeScriptXState - конечные автоматыYup - валидация схемТемизация в ReactТестирование хуковTailwind CSS с ReactSWR - библиотека для запросовStyled Components — стилизация через JSStorybook - документация компонентовSnapshots тестированиеRTK Query - работа с APIRedux Toolkit - современный ReduxRecoil — библиотека управления состоянием от FacebookВиртуализация списков с react-window: как отображать тысячи элементов без лаговReact Toastify - уведомления в ReactReact Testing LibraryСоздание таблиц в React гайд по react-tableReact Spring - анимацииРабота с формами и селектами в ReactReact Query (TanStack Query) - работа с серверомReact PDF - работа с PDF файламиПлагины в React что это и как их использоватьОбзор популярных библиотек для ReactReact Icons - библиотека иконок для ReactReact Hook Form — валидация форм в ReactReact Dropzone — загрузка файловПодключение Bootstrap к React-приложениюReact Beautiful DnD - перетаскивание элементовАнимация при монтировании компонентов в ReactМокирование APIMobX — реактивное управление состоянием в ReactМикрофронтенды с React (micro-frontends)Загрузка и индикаторыАнимация списков в ReactJotai - атомарное состояниеБесконечная прокруткаFramer Motion - библиотека анимацийEmotion — библиотека CSS-in-JSДинамические стили в ReactE2E тестирование с CypressCSSTransition - переходыCSS-in-JS — плюсы и минусыКонтекст vs Redux — когда что использоватьИспользование Chart.js в ReactAxios с ReactТестирование асинхронных компонентовОбработка ошибок API
Valtio: атомарный state management в ReactuseState в React что это и как использоватьuseTransition - плавные переходы между состояниямиuseSyncExternalStore — работа с внешними сторамиuseRef в React — создание ссылок на DOM и значенияuseOptimistic — оптимистичные обновления UIuseInsertionEffect — внедрение стилей до мутаций DOMuseImperativeHandle в React — настройка ref дочернего компонентаuseLayoutEffect в React — эффект до отрисовкиuseId — генерация уникальных идентификаторовuseFormStatus - отслеживание статуса отправки формыuseDeferredValue — отложенное обновление состоянияuseDebugValue — отладка кастомных хуковuseCallback в React — мемоизация функцийuseReducer — альтернатива useState для сложной логикиХук use() в React 19useMemo в React: как и когда оптимизировать тяжелые вычисленияuseEffect в React что это и как использоватьuseContext — работа с контекстом в ReactuseCallback в React — мемоизация функций и оптимизация ре-рендеровuseActionState в React 19Оптимизация рендеринга в React: от теории к глубокой практикеЧто такое useRef и как его применять в ReactКак и зачем использовать React HooksУправление состоянием в React через ContextНовые возможности React 19Как предотвратить лишние ре-рендеры в React: полное руководствоuseMemo vs useCallback: подробное руководство по мемоизации в ReactПравила хуков — правила использованияflushSync — синхронные обновления состояния в ReactuseEffect vs useLayoutEffect: в чём разница и какой хук выбрать?Кастомные хуки в React — создание собственных хуковuseState продвинутое использование в React
Transition API — плавные обновления интерфейса в ReactReact Suspense — приостановка рендераСерверные компоненты React (RSC) — подробный разбор и практикаStrictMode в React — как находить ошибки на этапе разработкиКак работает рендеринг в ReactЧто такое props в React и как их правильно использоватьКак работает JSX связка React и HTMLЧто такое React.js и как его использоватьКак использовать элементы в ReactКак использовать React DOM в проектеЧто такое компоненты в React и как их применятьРабота с children в ReactПорталы в React: рендер компонентов вне иерархии DOMCSS Modules в ReactConcurrent Mode — конкурентный режим в ReactFragment в React: группировка элементов без лишних узлов DOM
Открыть базу знаний

Лучшие курсы по теме

изображение курса

React и Redux Toolkit

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий