Реалізація PWA (Progressive Web App) для сайту

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.
Розробка та обслуговування будь-яких видів сайтів:
Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація PWA (Progressive Web App) для сайту
Складна
від 1 тижня до 3 місяців
Часті питання
Наші компетенції:
Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Розробка Progressive Web App (PWA)

PWA — веб-приложення з можливістю установки на пристрій, офлайн-роботою та нативним UX. Користувачі можуть додати сайт на домашній екран — він відкривається без адресної рядки, з кастомним сплєшем та іконкою.

Критерії встановлюваності

Для появи баннера «Додати на головний екран» браузер вимагає:

  • HTTPS (обов'язково)
  • Зареєстрований Service Worker з обробником fetch
  • Коректний Web App Manifest з icons, name, start_url, display
  • Користувач провів на сайті достатньо часу

Web App Manifest

{
    "name": "ТехноМагазин — купити електроніку",
    "short_name": "ТехноМагазин",
    "description": "Смартфони, ноутбуки, аксесуари з доставкою",
    "start_url": "/?source=pwa",
    "scope": "/",
    "display": "standalone",
    "orientation": "portrait-primary",
    "theme_color": "#1a73e8",
    "background_color": "#ffffff",
    "lang": "uk",
    "dir": "ltr",
    "icons": [
        { "src": "/icons/icon-72.png",   "sizes": "72x72",   "type": "image/png" },
        { "src": "/icons/icon-96.png",   "sizes": "96x96",   "type": "image/png" },
        { "src": "/icons/icon-128.png",  "sizes": "128x128", "type": "image/png" },
        { "src": "/icons/icon-144.png",  "sizes": "144x144", "type": "image/png" },
        { "src": "/icons/icon-152.png",  "sizes": "152x152", "type": "image/png" },
        { "src": "/icons/icon-192.png",  "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
        { "src": "/icons/icon-384.png",  "sizes": "384x384", "type": "image/png" },
        { "src": "/icons/icon-512.png",  "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
    ],
    "screenshots": [
        {
            "src": "/screenshots/mobile-catalog.webp",
            "sizes": "390x844",
            "type": "image/webp",
            "form_factor": "narrow",
            "label": "Каталог товарів"
        }
    ],
    "shortcuts": [
        {
            "name": "Кошик",
            "url": "/cart",
            "icons": [{ "src": "/icons/cart-96.png", "sizes": "96x96" }]
        },
        {
            "name": "Вибране",
            "url": "/wishlist",
            "icons": [{ "src": "/icons/heart-96.png", "sizes": "96x96" }]
        }
    ],
    "share_target": {
        "action": "/share",
        "method": "POST",
        "enctype": "multipart/form-data",
        "params": {
            "title": "title",
            "text": "text",
            "url": "url"
        }
    }
}
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#1a73e8">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="ТехноМагазин">
<link rel="apple-touch-icon" href="/icons/icon-192.png">

Service Worker для PWA

// sw.js — базова стратегія для PWA
const CACHE_VERSION = 'v3';
const APP_SHELL = [
    '/',
    '/manifest.json',
    '/offline.html',
    '/css/app.css',
    '/js/app.js',
    '/fonts/inter-regular.woff2',
    '/icons/icon-192.png',
];

// Установка: кешируємо App Shell
self.addEventListener('install', event => {
    event.waitUntil(
        caches.open(`shell-${CACHE_VERSION}`)
            .then(cache => cache.addAll(APP_SHELL))
            .then(() => self.skipWaiting())
    );
});

// Активація: видаляємо старі кеші
self.addEventListener('activate', event => {
    event.waitUntil(
        caches.keys()
            .then(keys => Promise.all(
                keys.filter(k => !k.endsWith(CACHE_VERSION))
                    .map(k => caches.delete(k))
            ))
            .then(() => self.clients.claim())
    );
});

// Fetch: різні стратегії для різних ресурсів
self.addEventListener('fetch', event => {
    const { request } = event;
    const url = new URL(request.url);

    // App Shell — cache first
    if (APP_SHELL.includes(url.pathname)) {
        event.respondWith(
            caches.match(request).then(r => r || fetch(request))
        );
        return;
    }

    // HTML-сторінки — network first, fallback offline
    if (request.headers.get('Accept')?.includes('text/html')) {
        event.respondWith(
            fetch(request)
                .then(response => {
                    const clone = response.clone();
                    caches.open(`pages-${CACHE_VERSION}`)
                        .then(cache => cache.put(request, clone));
                    return response;
                })
                .catch(() => caches.match(request)
                    .then(cached => cached || caches.match('/offline.html'))
                )
        );
        return;
    }

    // Зображення — stale while revalidate
    if (request.destination === 'image') {
        event.respondWith(
            caches.open(`images-${CACHE_VERSION}`).then(async cache => {
                const cached = await cache.match(request);
                const fetchPromise = fetch(request).then(response => {
                    cache.put(request, response.clone());
                    return response;
                });
                return cached ?? fetchPromise;
            })
        );
    }
});

Кнопка установки (Install Prompt)

// useInstallPrompt.ts
export function useInstallPrompt() {
    const [installPrompt, setInstallPrompt] = useState<BeforeInstallPromptEvent | null>(null);
    const [isInstalled, setIsInstalled] = useState(false);

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

        window.addEventListener('beforeinstallprompt', handler as EventListener);
        window.addEventListener('appinstalled', () => setIsInstalled(true));

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

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

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

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

// Використання в компоненті
function InstallBanner() {
    const { canInstall, install } = useInstallPrompt();
    if (!canInstall) return null;

    return (
        <div className="install-banner">
            <p>Встановіть приложення для швидкого доступу</p>
            <button onClick={install}>Встановити</button>
        </div>
    );
}

Офлайн сторінка

<!-- /offline.html -->
<!DOCTYPE html>
<html lang="uk">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Немає з'єднання — ТехноМагазин</title>
    <style>
        body { font-family: system-ui; display: flex; align-items: center;
               justify-content: center; height: 100vh; margin: 0; }
        .offline { text-align: center; }
    </style>
</head>
<body>
    <div class="offline">
        <svg><!-- іконка wifi off --></svg>
        <h1>Немає з'єднання</h1>
        <p>Перевірте інтернет та оновіть сторінку</p>
        <button onclick="location.reload()">Спробувати знову</button>
    </div>
</body>
</html>

Аналітика PWA-установок

window.addEventListener('appinstalled', () => {
    gtag('event', 'pwa_installed', { event_category: 'PWA' });
});

// Джерело трафіку з PWA
const isPWA = window.matchMedia('(display-mode: standalone)').matches;
if (isPWA) {
    gtag('set', { 'content_group': 'PWA' });
}

Lighthouse PWA аудит

Lighthouse автоматично перевіряє встановлюваність. Мета: усі перевірки PWA розділу зелені:

  • Installable: манівфест + SW + HTTPS
  • PWA Optimized: meta viewport, тема, іконки, offline

Час розробки: 2–4 дні для повної PWA з Service Worker, манівфестом та offline-сторінкою.