(з кешу)
PWA
можна кешувати заздалегідь
Навіщо офлайн-режим?
Фізичні симуляції — це обчислювально важкі сторінки: Three.js,
власні WASM-модулі та воркери симуляцій часто важать 200–800 КБ на
сторінку. На повільному чи нестабільному з'єднанні кожен візит
обертається болісним початковим завантаженням. Кешування браузером
через заголовки Cache-Control допомагає, але шар
service worker дає нам повний контроль над тим,
які ресурси кешуються, коли і наскільки
застарілими їм дозволено бути.
Життєвий цикл Service Worker
Service worker — це скрипт, який браузер встановлює один раз, а потім запускає як постійний фоновий проксі. Його життєвий цикл має три ключові фази: install (попереднє кешування критичних ресурсів), activate (очищення старих кешів) та fetch (перехоплення мережевих запитів).
// sw.js — install: попереднє кешування оболонки та спільних ресурсів
const PRECACHE = 'shell-v4';
const PRECACHE_URLS = [
'/',
'/index.html',
'/offline.html',
'/shared/theme.css',
'/shared/components.css',
'/shared/components.js',
'/manifest.json',
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(PRECACHE)
.then(cache => cache.addAll(PRECACHE_URLS))
.then(() => self.skipWaiting()) // активувати негайно
);
});
Стратегії кешування за типом ресурсу
Не всі ресурси варто кешувати однаково. Ми використовуємо чотири стратегії:
// sw.js — fetch: маршрутизація до потрібної стратегії
self.addEventListener('fetch', event => {
const { request } = event;
const url = new URL(request.url);
// Пропускаємо не-GET запити та cross-origin
if (request.method !== 'GET' || url.origin !== location.origin) return;
// Версійні ресурси → cache-first
if (/\.(wasm|js|css)$/.test(url.pathname) && /[?&]v=/.test(url.search)) {
event.respondWith(cacheFirst(request));
return;
}
// HTML-сторінки → stale-while-revalidate
if (request.headers.get('Accept')?.includes('text/html')) {
event.respondWith(staleWhileRevalidate(request, 'pages-v4'));
return;
}
});
async function staleWhileRevalidate(request, cacheName) {
const cache = await caches.open(cacheName);
const cached = await cache.match(request);
const fetchPromise = fetch(request).then(response => {
cache.put(request, response.clone());
return response;
});
return cached ?? fetchPromise;
}
async function cacheFirst(request) {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
const cache = await caches.open('assets-v4');
cache.put(request, response.clone());
return response;
}
Кешування сторінок симуляцій під час виконання
Симуляції надто великі, щоб кешувати їх усі одразу під час встановлення — це зайняло б кілька сотень мегабайт кешу і затримало б встановлення service worker. Натомість ми попередньо кешуємо ресурси симуляції під час першого відвідування користувачем, щоб другий візит був миттєвим.
Спеціальний SIM_CACHE має політику максимального
терміну зберігання: записи, старіші за 7 днів, видаляються під час
наступної події activate за допомогою сховища метаданих із
часовими мітками в IndexedDB.
Запрошення до встановлення
Коли виконуються критерії встановлюваності браузера (HTTPS +
service worker + маніфест з іконками + display: standalone),
спрацьовує подія beforeinstallprompt. Ми перехоплюємо
її та показуємо стриманий банер встановлення у нашому футері:
let deferredPrompt;
window.addEventListener('beforeinstallprompt', e => {
e.preventDefault(); // зупиняємо стандартну міні-інформаційну панель браузера
deferredPrompt = e;
showInstallBanner();
});
function showInstallBanner() {
const banner = document.getElementById('install-banner');
if (!banner) return;
banner.hidden = false;
banner.querySelector('button').addEventListener('click', async () => {
banner.hidden = true;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log('Install outcome:', outcome); // 'accepted' або 'dismissed'
deferredPrompt = null;
});
}
Оцінка Lighthouse PWA 100 — чекліст
-
Service worker з офлайн-резервом — наш
offline.htmlвіддається з кешу, коли мережа недоступна і жодної кешованої сторінки не знайдено. -
Web App Manifest —
manifest.jsonз іконками 192px і 512px,theme_color,background_color,display: standaloneіstart_url. -
HTTPS всюди — наш хостинг забезпечує це
автоматично на власному домені
mysimulator.uk. - Без змішаного контенту — усі шрифти, скрипти та зображення розміщені на власному сервері або віддаються через HTTPS CDN з хешами SRI.
- Маскована іконка — запис маскованої іконки в маніфесті з безпечною зоною, щоб іконка коректно відображалася на будь-якій формі лаунчера Android.
- Швидке перше завантаження — критичний CSS вбудовано інлайном; JS відкладено; Three.js завантажується лениво всередині кожного iframe симуляції, щоб не блокувати LCP оболонки сторінки.
Ліміти сховища кешу: браузери зазвичай надають до 60% доступного дискового простору для сховища походження (яке включає IndexedDB, CacheStorage та localStorage). На пристрої з 32 ГБ пам'яті та 15 ГБ вільного місця це ~9 ГБ. Ми залишаємось значно нижче цього ліміту, видаляючи ресурси симуляцій, старіші за 7 днів, і обмежуючи кешовані ресурси кожної симуляції приблизно 5 МБ.