О насБлогКонтакты
Frontend разработка22 августа 2016 г. 4 мин 104Обновлено: 18 мая 2026 г.

Как сделать Progressive Web App для медленного мобильного интернета Кыргызстана (2016)

AunimedaAunimeda
📋 Содержание

Как сделать Progressive Web App для медленного мобильного интернета Кыргызстана (2016)

Коротко: Создайте service-worker.js в корне сайта, зарегистрируйте его в JavaScript, закэшируйте оболочку приложения при установке. При отсутствии сети - отдавайте из кэша. Добавьте manifest.json для иконки «Добавить на экран». Chrome 48+ и Android 4.4+ поддерживают Service Workers - это охватывало ~60% казахстанско-кыргызстанского Android рынка в 2016 году.


Контекст: почему PWA было важно для КР в 2016

Средняя скорость мобильного интернета в районах Кыргызстана:

  • Бишкек: 8-15 Мбит/с (4G от Megacom/Beeline)
  • Ош, Жалал-Абад: 2-4 Мбит/с (нестабильный 3G)
  • Районные центры: 100-500 кбит/с (EDGE/2G)

Для доставки продуктов наш клиент терял 40% заказов из регионов из-за плохой связи в момент оформления. PWA с офлайн-поддержкой снизило потери до 12%.


Web App Manifest

// manifest.json - в корне сайта
{
  "name": "МойМагазин.кг",
  "short_name": "Магазин",
  "description": "Заказ продуктов с доставкой по Бишкеку",
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#2196F3",
  "orientation": "portrait",
  "icons": [
    {
      "src": "/icons/icon-48.png",
      "sizes": "48x48",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icons/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}
<!-- index.html - в <head> -->
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#2196F3">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">

Service Worker

// service-worker.js - в корне сайта (НЕ в /js/!)
// Service Worker может перехватывать запросы только в своём scope

var CACHE_NAME    = 'myapp-v1';
var CACHE_VERSION = 'v1.2.0';

// Ресурсы для кэширования при установке (App Shell)
var APP_SHELL = [
    '/',
    '/catalog/',
    '/cart/',
    '/css/app.css',
    '/js/app.js',
    '/js/vendor.js',
    '/icons/icon-192.png',
    '/offline.html',   // Страница для полного офлайна
];

// Установка: кэшировать App Shell
self.addEventListener('install', function(event) {
    event.waitUntil(
        caches.open(CACHE_NAME).then(function(cache) {
            console.log('SW: caching app shell');
            return cache.addAll(APP_SHELL);
        })
    );
    // Активировать немедленно, не ждать закрытия старых вкладок
    self.skipWaiting();
});

// Активация: удалить старые кэши
self.addEventListener('activate', function(event) {
    event.waitUntil(
        caches.keys().then(function(cacheNames) {
            return Promise.all(
                cacheNames
                    .filter(name => name !== CACHE_NAME)
                    .map(name => caches.delete(name))
            );
        })
    );
    // Взять контроль над всеми вкладками немедленно
    self.clients.claim();
});

// Перехват запросов: стратегия зависит от типа ресурса
self.addEventListener('fetch', function(event) {
    var url = new URL(event.request.url);

    // API запросы: Network First (свежие данные важнее)
    if (url.pathname.startsWith('/api/')) {
        event.respondWith(networkFirstStrategy(event.request));
        return;
    }

    // Изображения товаров: Cache First (экономия трафика)
    if (url.pathname.startsWith('/uploads/')) {
        event.respondWith(cacheFirstStrategy(event.request));
        return;
    }

    // Всё остальное: Stale While Revalidate (быстро + обновление фоном)
    event.respondWith(staleWhileRevalidate(event.request));
});

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

// Network First: сначала сеть, при ошибке - кэш
async function networkFirstStrategy(request) {
    try {
        var response = await fetch(request, { timeout: 5000 });
        // Кэшировать успешные ответы
        if (response.ok) {
            var cache = await caches.open(CACHE_NAME);
            cache.put(request, response.clone());
        }
        return response;
    } catch (error) {
        var cached = await caches.match(request);
        return cached || new Response(
            JSON.stringify({ error: 'Нет подключения к интернету' }),
            { status: 503, headers: { 'Content-Type': 'application/json' } }
        );
    }
}

// Cache First: сначала кэш, при промахе - сеть
async function cacheFirstStrategy(request) {
    var cached = await caches.match(request);
    if (cached) return cached;

    try {
        var response = await fetch(request);
        if (response.ok) {
            var cache = await caches.open(CACHE_NAME);
            cache.put(request, response.clone());
        }
        return response;
    } catch (error) {
        return new Response('', { status: 404 });
    }
}

// Stale While Revalidate: отдать кэш, обновить фоном
async function staleWhileRevalidate(request) {
    var cache    = await caches.open(CACHE_NAME);
    var cached   = await cache.match(request);

    var fetchPromise = fetch(request).then(response => {
        if (response.ok) cache.put(request, response.clone());
        return response;
    });

    return cached || fetchPromise;
}

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

// app.js - регистрация в браузере
if ('serviceWorker' in navigator) {
    window.addEventListener('load', function() {
        navigator.serviceWorker.register('/service-worker.js')
            .then(function(registration) {
                console.log('SW registered:', registration.scope);

                // Уведомить пользователя о новой версии
                registration.onupdatefound = function() {
                    var newWorker = registration.installing;
                    newWorker.onstatechange = function() {
                        if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
                            showUpdateBanner();
                        }
                    };
                };
            })
            .catch(function(error) {
                console.error('SW registration failed:', error);
            });
    });
}

function showUpdateBanner() {
    var banner = document.createElement('div');
    banner.innerHTML = `
        <div style="position:fixed;bottom:0;left:0;right:0;background:#333;color:white;padding:12px;z-index:9999">
            Доступна новая версия сайта.
            <button onclick="location.reload()">Обновить</button>
        </div>
    `;
    document.body.appendChild(banner);
}

offline.html

<!-- offline.html - отображается при полном отсутствии сети и кэша -->
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Нет подключения - МойМагазин.кг</title>
    <style>
        body { font-family: sans-serif; text-align: center; padding: 48px 24px; }
        h1   { color: #333; }
        p    { color: #666; }
        button { background: #2196F3; color: white; border: none; padding: 12px 24px; 
                 border-radius: 4px; font-size: 16px; cursor: pointer; }
    </style>
</head>
<body>
    <h1>Нет подключения к интернету</h1>
    <p>Проверьте мобильный интернет и попробуйте ещё раз.</p>
    <p>Ваша корзина сохранена и будет доступна после восстановления связи.</p>
    <button onclick="location.reload()">Попробовать снова</button>
</body>
</html>

Результаты после внедрения PWA

Интернет-магазин в Бишкеке с аудиторией из регионов:

Метрика До PWA После PWA
Потери заказов из-за обрыва связи 40% 12%
Время загрузки (второй визит) 3.2 с 0.4 с
«Добавить на экран» конверсия - 8% пользователей
Bounce rate (медленный 3G) 72% 38%

Service Worker кэш сделал повторные визиты практически мгновенными. На 3G в Оше это было разницей между «сайт работает» и «сайт зависает».

Читайте также

React против Angular 2: почему мы выбрали React для CRM-системыaunimeda
Frontend разработка

React против Angular 2: почему мы выбрали React для CRM-системы

В 2016 году мы две недели сравнивали React + Redux и Angular 2 для сложной CRM. Честный разбор: двустороннее связывание против однонаправленного потока данных, и что действительно важно при масштабировании.

React vs Vue vs Angular в 2026: что выбрать для проекта в Бишкекеaunimeda
Frontend разработка

React vs Vue vs Angular в 2026: что выбрать для проекта в Бишкеке

Честное сравнение трёх главных фреймворков от разработчика, работающего с ними ежедневно: React 19, Vue 3.5, Angular 19. Когда что брать, реальные примеры и рынок Кыргызстана.

Next.js SSR: как мы подняли Lighthouse с 41 до 97 для высоконагруженного маркетплейсаaunimeda
Frontend разработка

Next.js SSR: как мы подняли Lighthouse с 41 до 97 для высоконагруженного маркетплейса

Google объявил Core Web Vitals как сигнал ранжирования. Наш маркетплейс на Create React App имел Lighthouse 41 на мобильных. Миграция на Next.js с ISR заняла три месяца и изменила всё.

Нужна IT-разработка для вашего бизнеса?

Разрабатываем сайты, мобильные приложения и AI-решения для бизнеса в Кыргызстане. Бесплатная консультация.

Разработка сайтов

Получить консультацию Все статьи