Kas tie Service Workers ir kodėl turėtumėte susipažinti
Jei dar nesate susidūrę su Service Workers, tai greičiausiai dirbate su legacy projektais arba tiesiog nesekate naujausių web development tendencijų. Bet nesijaudinkite – niekada nevėlu pradėti. Service Workers iš esmės yra JavaScript failai, kurie veikia atskirame thread’e nuo pagrindinės svetainės ir gali perimti bei modifikuoti network užklausas. Skamba bauginančiai? Gal šiek tiek, bet realybėje tai vienas galingiausių įrankių moderniam web’ui.
Pagrindinis dalykas, kurį reikia suprasti – Service Worker veikia kaip proxy tarp jūsų aplikacijos ir tinklo. Jis gali interceptinti fetch užklausas, cache’inti resursus ir net leisti jūsų aplikacijai veikti offline. Tai nėra magiška bullet, kuri išspręs visas performance problemas, bet tinkamai implementavus gali drastiškai pagerinti user experience.
Vienas svarbiausių aspektų – Service Workers veikia tik per HTTPS (išskyrus localhost development). Tai saugumo reikalavimas, nes turite galimybę perimti ir modifikuoti network traffic’ą. Taip pat verta žinoti, kad jie neturi prieigos prie DOM – visą komunikaciją su page reikia daryti per postMessage API.
Cache strategijos: ne viena dydis tinka visiems
Kai pradėjau dirbti su Service Workers, didžiausia klaida buvo manyti, kad cache’insiu viską ir viskas bus super greita. Realybė – skirtingi resursai reikalauja skirtingų strategijų. Yra keletas pagrindinių pattern’ų, kuriuos verta žinoti:
Cache First strategija puikiai tinka statiniams resursams – CSS, JavaScript bundle’ams, šriftams, paveikslėliams. Čia logika paprasta: pirma žiūrime cache’e, jei nerandame – einame į network. Kartą užkrovę resursą, jis lieka cache’e ir užsikrauna akimirksniu. Problema – jei resursas pasikeičia serveryje, vartotojas gali nematyti atnaujinimų, kol neišvalys cache arba nepersirašysite Service Worker’io.
Network First geriau tinka dinaminiams turiniams – API atsakymams, naujienom, bet kokiam content’ui, kuris dažnai keičiasi. Pirma bandome gauti fresh data iš serverio, jei nepavyksta (offline arba lėtas internetas) – grąžiname iš cache. Taip užtikriname, kad vartotojas mato naujausią informaciją, bet vis tiek turi fallback’ą.
Stale While Revalidate – mano asmeniškai mėgstamiausia strategija daugeliui use case’ų. Grąžini iš cache (jei yra), bet tuo pačiu background’e darai network request ir atnaujini cache. Vartotojas gauna instant response, o sekantis apsilankymas jau turės fresh data. Idealus balansas tarp greičio ir aktualumo.
Praktinė implementacija: nuo nulio iki veikiančio sprendimo
Pradėkime nuo paprasto Service Worker registracijos. Jūsų main JavaScript faile (pvz., app.js) reikia tokio kodo:
„`javascript
if (‘serviceWorker’ in navigator) {
window.addEventListener(‘load’, () => {
navigator.serviceWorker.register(‘/service-worker.js’)
.then(registration => {
console.log(‘SW registered:’, registration);
})
.catch(error => {
console.log(‘SW registration failed:’, error);
});
});
}
„`
Čia tikriname ar naršyklė palaiko Service Workers (ne visos senos versijos palaiko), ir registruojame mūsų worker’į. Svarbu daryti tai po `load` event’o, kad neblokuotume pradinės page load.
Dabar pats Service Worker failas. Sukuriame `service-worker.js` projekto root’e:
„`javascript
const CACHE_NAME = ‘my-app-cache-v1’;
const urlsToCache = [
‘/’,
‘/styles/main.css’,
‘/scripts/app.js’,
‘/images/logo.png’
];
self.addEventListener(‘install’, event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => {
console.log(‘Cache opened’);
return cache.addAll(urlsToCache);
})
);
});
„`
Install event’as įvyksta kai Service Worker pirmą kartą užregistruojamas. Čia pre-cache’iname kritinius resursus – tuos, kurie būtini aplikacijai veikti. Neperkraukite šito sąrašo – kuo daugiau failų, tuo ilgiau užtruks installation.
Fetch interceptavimas ir cache logika
Dabar pats įdomiausias dalykas – fetch event’o handling. Čia galime implementuoti mūsų cache strategijas:
„`javascript
self.addEventListener(‘fetch’, event => {
event.respondWith(
caches.match(event.request)
.then(response => {
// Cache hit – grąžiname response iš cache
if (response) {
return response;
}
return fetch(event.request).then(
response => {
// Tikriname ar gavome validų response
if(!response || response.status !== 200 || response.type !== ‘basic’) {
return response;
}
// Kloname response nes galime jį naudoti tik kartą
const responseToCache = response.clone();
caches.open(CACHE_NAME)
.then(cache => {
cache.put(event.request, responseToCache);
});
return response;
}
);
})
);
});
„`
Šitas kodas implementuoja Cache First strategiją. Bet realybėje norėsite kažko sudėtingesnio – skirtingų strategijų skirtingiems resursams. Štai kaip tai galima padaryti:
„`javascript
self.addEventListener(‘fetch’, event => {
const { request } = event;
const url = new URL(request.url);
// API calls – Network First
if (url.pathname.startsWith(‘/api/’)) {
event.respondWith(
fetch(request)
.then(response => {
const responseClone = response.clone();
caches.open(CACHE_NAME).then(cache => {
cache.put(request, responseClone);
});
return response;
})
.catch(() => {
return caches.match(request);
})
);
return;
}
// Static assets – Cache First
if (request.destination === ‘image’ ||
request.destination === ‘style’ ||
request.destination === ‘script’) {
event.respondWith(
caches.match(request)
.then(response => response || fetch(request))
);
return;
}
// Viskam kitam – Stale While Revalidate
event.respondWith(
caches.match(request)
.then(cachedResponse => {
const fetchPromise = fetch(request).then(networkResponse => {
caches.open(CACHE_NAME).then(cache => {
cache.put(request, networkResponse.clone());
});
return networkResponse;
});
return cachedResponse || fetchPromise;
})
);
});
„`
Cache versioning ir cleanup
Viena didžiausių problemų su cache – kaip jį atnaujinti kai pasikeitė aplikacija? Jei tiesiog pakeičiate failus serveryje, vartotojai vis tiek matys senus iš cache. Sprendimas – cache versioning.
Pastebėjote `CACHE_NAME` konstantą su `-v1` gale? Tai versijos numeris. Kai deploy’inate naują aplikacijos versiją, pakeičiate į `-v2`, `-v3` ir t.t. Bet tai tik pusė sprendimo – reikia išvalyti senus cache’us:
„`javascript
self.addEventListener(‘activate’, event => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
console.log(‘Deleting old cache:’, cacheName);
return caches.delete(cacheName);
}
})
);
})
);
});
„`
Activate event’as įvyksta kai naujas Service Worker perima kontrolę. Čia išvalome visus cache’us, kurių nėra whitelist’e. Tai užtikrina, kad seni resursai neužims vietos vartotojo device’e.
Dar vienas svarbus momentas – `self.clients.claim()`. Normaliai naujas Service Worker pradeda kontroliuoti pages tik po refresh’o. Jei norite, kad jis perimtų kontrolę iš karto:
„`javascript
self.addEventListener(‘activate’, event => {
event.waitUntil(
Promise.all([
// Cache cleanup kaip aukščiau
self.clients.claim()
])
);
});
„`
Cache size limitai ir strateginis resursų valdymas
Negalite cache’inti visko be galo. Naršyklės turi limitus – paprastai apie 50MB Chrome’e, bet gali skirtis. Kai viršijate limitą, naršyklė pradeda šalinti senus cache’us. Problema – ji nežino kurie jums svarbūs.
Geriausia praktika – būti selektyviam. Nekiškit į cache kiekvieno API response. Štai kaip galima implementuoti cache size limitą:
„`javascript
const limitCacheSize = (name, size) => {
caches.open(name).then(cache => {
cache.keys().then(keys => {
if(keys.length > size) {
cache.delete(keys[0]).then(() => {
limitCacheSize(name, size);
});
}
});
});
};
„`
Galite kviesti šitą funkciją po kiekvieno cache.put():
„`javascript
cache.put(request, response).then(() => {
limitCacheSize(CACHE_NAME, 50);
});
„`
Taip užtikrinsite, kad cache’e bus ne daugiau kaip 50 įrašų, šalinant seniausius.
Dar vienas approach – skirtingi cache’ai skirtingiems dalykams:
„`javascript
const STATIC_CACHE = ‘static-v1’;
const DYNAMIC_CACHE = ‘dynamic-v1’;
const IMAGE_CACHE = ‘images-v1’;
„`
Statiniams resursams galite turėti vieną cache be limito (jų nedaug ir retai keičiasi), dinaminiams – su limitu, paveikslėliams – atskirą su savo limitu. Taip turite geresnę kontrolę ir galite implementuoti skirtingas cleanup strategijas.
Debugging ir development workflow
Service Workers debugging gali būti pain. Jie cache’ina viską taip gerai, kad kartais nematote savo pakeitimų. Keletas tips’ų:
Chrome DevTools -> Application tab -> Service Workers sekcija. Čia galite:
– Unregister worker’į
– Update on reload (labai naudingas development’e)
– Bypass network for this request
– Matyt visus aktyvius workers
Cache Storage sekcijoje matote visus cache’us ir jų turinį. Galite ištrinti atskirų cache’ų arba atskirų įrašų. Development metu dažnai tiesiog ištrinu visus cache’us ir perkraunu page.
Svarbus dalykas – Service Worker update lifecycle. Kai pakeičiate service-worker.js failą, naujas worker’is neperima kontrolės iš karto. Jis laukia kol visi tab’ai su sena versija bus uždaryti. Development’e tai erzina, todėl naudoju `skipWaiting()`:
„`javascript
self.addEventListener(‘install’, event => {
self.skipWaiting(); // Development only!
// … kitas install kodas
});
„`
Production’e būkite atsargūs su skipWaiting() – gali sukelti issues jei sena page versija bando naudoti naujus cache’intus resursus.
Dar vienas lifesaver – console.log’ai Service Worker’yje matosi Chrome DevTools console, bet reikia filtruoti pagal source. Arba galite naudoti Chrome’s service worker specific console: chrome://serviceworker-internals/
Realūs use case’ai ir ką išmokau praktikoje
Dirbau projekte kur turėjome SPA su dideliu kiekiu paveikslėlių ir API calls. Pradžioje cache’inau viską agresyviai – rezultatas buvo greitas, bet vartotojai matė outdated data. Turėjau rasti balansą.
Sprendimas buvo toks: statiniai assets (JS, CSS) – cache first su versioning. Paveikslėliai – cache first, bet su stale-while-revalidate logika jei paveikslėlis senesnis nei 7 dienos. API calls – network first, bet su 3 sekundžių timeout – jei serveris neatsakė per 3 sek, rodome iš cache ir background’e tęsiame bandymą.
„`javascript
const fetchWithTimeout = (request, timeout = 3000) => {
return Promise.race([
fetch(request),
new Promise((_, reject) =>
setTimeout(() => reject(new Error(‘timeout’)), timeout)
)
]);
};
// API handling su timeout
if (url.pathname.startsWith(‘/api/’)) {
event.respondWith(
fetchWithTimeout(request)
.then(response => {
const clone = response.clone();
caches.open(DYNAMIC_CACHE).then(cache => {
cache.put(request, clone);
});
return response;
})
.catch(() => {
return caches.match(request).then(cached => {
if (cached) return cached;
// Jei nėra cache ir network failed – grąžiname custom error response
return new Response(JSON.stringify({
error: ‘Offline’,
message: ‘No cached data available’
}), {
headers: { ‘Content-Type’: ‘application/json’ }
});
});
})
);
}
„`
Kitas use case – offline page. Kai vartotojas visiškai offline ir bando pasiekti page, kurią neturime cache’e, rodome custom offline page:
„`javascript
// Install metu cache’iname offline page
self.addEventListener(‘install’, event => {
event.waitUntil(
caches.open(STATIC_CACHE).then(cache => {
return cache.addAll([
‘/offline.html’,
‘/styles/offline.css’
]);
})
);
});
// Fetch metu jei viskas fails – rodome offline page
self.addEventListener(‘fetch’, event => {
if (event.request.mode === ‘navigate’) {
event.respondWith(
fetch(event.request)
.catch(() => {
return caches.match(‘/offline.html’);
})
);
}
});
„`
Kai cache tampa jūsų geriausiu draugu (o ne priešu)
Implementavus Service Workers su protingu cache valdymu, mūsų aplikacijos load time sumažėjo nuo ~3 sekundžių iki ~0.5 sekundės repeat visitors. First-time visitors vis tiek mato normalų load, bet returning users gauna beveik instant experience. Lighthouse performance score pakilo nuo 65 iki 95.
Svarbiausia pamoka – nėra vienos teisingos strategijos. Reikia suprasti savo aplikacijos poreikius ir vartotojų elgesį. Jei turite daug static content – agresyviai cache’inkite. Jei real-time data kritinė – būkite atsargesni.
Taip pat neužmirškite monitoring’o. Implementuokite analytics, kad matytumėte cache hit rates, offline usage, error rates. Google Analytics palaiko offline tracking – events saugomi locally ir siunčiami kai vartotojas vėl online.
Service Workers nėra silver bullet, bet tinkamai naudojami gali transformuoti jūsų web app iš „dar vieno website” į kažką kas jaučiasi kaip native aplikacija. Vartotojai gal nesupras techninio aspekto, bet tikrai pajus skirtumą greityje ir patikimume. O tai galiausiai ir yra tikslas, ar ne?
