Download - Service Workers for Performance
Service Workers
The Practical Bits
@patmeenan
Patrick Meenan
Slides
Slideshare: http://www.slideshare.net/patrickmeenan
Twitter: @patmeenan (and to #velocityconf)
Velocity Site, attached to session information
Video: https://www.youtube.com/user/patmeenan
Office Hours: Friday, May 29 from 12:30pm-1:00pm@patmeenan
The Real Experts
Alex Russell@slightlylate
Jake Archibald@jaffathecake
Service Workers
DOM
Resource Fetching in Chrome
DOM
Resource Fetching in Chrome
In-memory Resource
Cache
DOM
Resource Fetching in Chrome
In-memory Resource
Cache
Resource Fetcher
DOM
Resource Fetching in Chrome
Resource Fetcher
Net Stack
DOM
Resource Fetching in Chrome
In-memory Resource
Cache
Resource Fetcher
Net Stack
Disk Cache
DOM
Resource Fetching in Chrome
In-memory Resource
Cache
Resource Fetcher
Net Stack Net
Disk Cache
DOM
Resource Fetching in Chrome
In-memory Resource
Cache
Resource Fetcher
Net Stack Net
Disk Cache
Service Worker
SW Added and Removed
Here!
CapabilitiesSees every request for your document
- Including cross-origin
- And headersCan synthesize responsesSupports fetchHas a programmable cache and Indexed DB
LimitationsHTTPS documents onlyNot active for first viewiFrames are separate documentsNon-CORS Cross-origin responses are opaqueNo global state
- or concept of a “page”No Sync API’s (localstorage, XHR, etc)
http://memegenerator.net/instance/62336209
Registering
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/my-app/sw.js', {
scope: '/my-app/'
});
}
https://github.com/slightlyoff/ServiceWorker/blob/master/explainer.md
Registering
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/my-app/sw.js', {
scope: '/my-app/'
});
}
https://github.com/slightlyoff/ServiceWorker/blob/master/explainer.md
Registering
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/my-app/sw.js', {
scope: '/my-app/'
});
}
https://github.com/slightlyoff/ServiceWorker/blob/master/explainer.md
Registering
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/my-app/sw.js', {
scope: '/my-app/'
});
}
https://github.com/slightlyoff/ServiceWorker/blob/master/explainer.md
Install / activateself.addEventListener('install', function(event) {
event.waitUntil(
fetchStuffAndInitDatabases()
);
});
self.addEventListener('activate', function(event) {
// You're good to go!
});
https://github.com/slightlyoff/ServiceWorker/blob/master/explainer.md
Network Intercepting
self.addEventListener('fetch', function(event) {
event.respondWith(new Response("Hello world!"));
});
https://github.com/slightlyoff/ServiceWorker/blob/master/explainer.md
Enough Theory – Let’s Use It
Offline Content
Most benefit for Single-Page Apps where app/data are separated
Service Workers become progressive enhancement for offline support
Offline Content
Pre-cache offline content
Pass live requests through when online
- Caching the responses
Serve cached responses when offline (if available)
Serve offline-specific versions otherwise
Offline Content – Pre-caching
self.addEventListener( ‘install’, function(event) {
event.waitUntil(
caches.open(‘my-offline-cache-v1’).then( function(cache) {
return cache.addAll([
‘/site.js’,
‘/images/offline.png’,
‘/offline.html’]);
}));});
Offline Content – Pre-caching
self.addEventListener( ‘install’, function(event) {
event.waitUntil(
caches.open(‘my-offline-cache-v1’).then( function(cache) {
return cache.addAll([
‘/site.js’,
‘/images/offline.png’,
‘/offline.html’]);
}));});
Offline Content – Pre-caching
self.addEventListener( ‘install’, function(event) {
event.waitUntil(
caches.open(‘my-offline-cache-v1’).then( function(cache) {
return cache.addAll([
‘/site.js’,
‘/images/offline.png’,
‘/offline.html’]);
}));});
Offline Content – Pre-caching
self.addEventListener( ‘install’, function(event) {
event.waitUntil(
caches.open(‘my-offline-cache-v1’).then( function(cache) {
return cache.addAll([
‘/site.js’,
‘/images/offline.png’,
‘/offline.html’]);
}));});
Offline Content – Pre-caching
self.addEventListener( ‘install’, function(event) {
event.waitUntil(
caches.open(‘my-offline-cache-v1’).then( function(cache) {
return cache.addAll([
‘/site.js’,
‘/images/offline.png’,
‘/offline.html’]);
}));});
Offline Content – Fetch Processing
self.addEventListener( 'fetch', function(event) {
if (navigator.online) {
event.respondWith( onlineRequest(event.request) );
} else {
event.respondWith( offlineRequest(event.request) );
}
});
Offline Content – Fetch Processing
self.addEventListener( 'fetch', function(event) {
if (navigator.online) {
event.respondWith( onlineRequest(event.request) );
} else {
event.respondWith( offlineRequest(event.request) );
}
});
Offline Content – Fetch Processing
self.addEventListener( 'fetch', function(event) {
if (navigator.online) {
event.respondWith( onlineRequest(event.request) );
} else {
event.respondWith( offlineRequest(event.request) );
}
});
Offline Content – Fetch Processing
self.addEventListener( 'fetch', function(event) {
if (navigator.online) {
event.respondWith( onlineRequest(event.request) );
} else {
event.respondWith( offlineRequest(event.request) );
}
});
Offline Content – Fetch Processing
self.addEventListener( 'fetch', function(event) {
if (navigator.online) {
event.respondWith( onlineRequest(event.request) );
} else {
event.respondWith( offlineRequest(event.request) );
}
});
Offline Content – Online Response
function onlineRequest(request) {
return caches.match(request)
.then(function(response) {
…
}
);
}
Offline Content – Online Response
function onlineRequest(request) {
return caches.match(request).then(function(response) {
if (response) {
return response;
} else {
return fetch(request);
}
});
}
Offline Content – Online Response
function onlineRequest(request) {
return caches.match(request).then(function(response) {
if (response) {
return response;
} else {
return fetch(request);
}
});
}
Offline Content – Online Response…
var fetchRequest = request.clone();
return fetch(fetchRequest).then( function(response) {
var responseToCache = response.clone();
caches.open(CACHE_NAME).then( function(cache) {
cache.put(request, responseToCache);
});
return response;
});
Offline Content – Online Response…
var fetchRequest = request.clone();
return fetch(fetchRequest).then( function(response) {
var responseToCache = response.clone();
caches.open(CACHE_NAME).then( function(cache) {
cache.put(request, responseToCache);
});
return response;
});
Offline Content – Online Response…
var fetchRequest = request.clone();
return fetch(fetchRequest).then( function(response) {
var responseToCache = response.clone();
caches.open(CACHE_NAME).then( function(cache) {
cache.put(request, responseToCache);
});
return response;
});
Offline Content – Online Response…
var fetchRequest = request.clone();
return fetch(fetchRequest).then( function(response) {
var responseToCache = response.clone();
caches.open(CACHE_NAME).then( function(cache) {
cache.put(request, responseToCache);
});
return response;
});
Offline Content – Online Response…
var fetchRequest = request.clone();
return fetch(fetchRequest).then( function(response) {
var responseToCache = response.clone();
caches.open(CACHE_NAME).then( function(cache) {
cache.put(request, responseToCache);
});
return response;
});
Offline Content – Online Response…
var fetchRequest = request.clone();
return fetch(fetchRequest).then( function(response) {
var responseToCache = response.clone();
caches.open(CACHE_NAME).then( function(cache) {
cache.put(request, responseToCache);
});
return response;
});
Offline Content – Offline Response
function offlineRequest(request) {
if (request.url.match(/\.png$/)) {
return caches.match(‘/images/offline.png’);
} else if (request.url.match(/\.html$/)) {
return caches.match(‘/offline.html’);
}
…
}
Offline Content – Offline Response
function offlineRequest(request) {
if (request.url.match(/\.png$/)) {
return caches.match(‘/images/offline.png’);
} else if (request.url.match(/\.html$/)) {
return caches.match(‘/offline.html’);
}
…
}
Other Uses
Not limited to applications designed for offline/SPALegacy apps + fetch intercept = Awesome possibilities
Custom Error Pages
Fetch Request NormallyFor non-200 responses serve a local error pageNot just server errors:
- DNS failures
- CDN/Proxy Errors
- Intermediaries
SPOF Prevention
Identify requests of interest
- 3rd-party javascript
- FontsSet a timerPass fetch requests through If timer expires before fetch completes generate error response
CDN/Origin Failover
Identify CDN requests by URLSet a timerPass fetch request through If timer expires, create fetch request to originRespond with first fetch request to complete
Multi-Origin/CDN
Identify CDN requests by URLReplace CDN domain with alternate CDN (or origin)Keep track of performance by originPrefer faster origin (keep measuring)
Could also race the CDNs in parallel
- Be careful about increased data
Stale-While-Revalidate
Respond with local cached resource
- Ignoring Expires, max-age, etc.Pass fetch request through in parallelUpdate cache with new response
Works best for known resources (analytics/ads JS, etc)
Prefetch
Custom response headers with prefetch resourcesWhen idle, prefetch suggested resources
Resource Prioritization/Scheduling
Track in-flight requestsMake scheduling decisions as new requests come inCustom application-aware scheduling logic
- Delay JS if it is known to be at the end
- Delay footer images
- etc
Delta CompressionDelta compression for build->build updates
- Include version in URL scheme
- Get latest version number from cache
- Request delta from server
- Apply patch
- Cache new version
New compression algorithms
Custom CompressionNew compression algorithms
- Brotli
- Fractal image compression
- JSON-specific dictionaries
- Application-specificProve-out new algorithms before standardizing…as long as it can be implemented in JS and code size is
reasonable
Incremental Progressive Images
Identify JPEG image requests from known originSynthesize responseRange request (or smarter) for first few scansStream initial range into synthesized responseRange request for remaining image (some point later)Append remaining data into synthesized response
Drawing Images Locally
930 x 11,362 px WebPageTest waterfall
351KB compressed PNG
42 MB in-memory (server) to generate
< 20KB compressed JSON data to describe
Prefer actual images for WPT waterfalls for easier embedding
- Otherwise SVG, Canvas or DOM would work
Drawing Images Locally*
Identify appropriate requests (i.e. waterfall.png?...)Fetch data necessary to draw imageDraw to canvasExtract as PNGSynthesize PNG response
* Work in progress, coming soon
Metrics/Debugging
Pass all requests throughPassively observe timings and successReport metrics backGives visibility into failures and requests that are in flight
- Unlike resource timing
And there’s more…
Required for…Push Notifications
- https://gauntface.com/blog/2014/12/15/push-notifications-service-worker
Background Sync- https://github.com/slightlyoff/BackgroundSync/blob/master/explainer.md
GeoFencing- https://github.com/slightlyoff/Geofencing
AvailabilityChrome Stable (40+)Opera (27+)Firefox (Nightly)
Composable
Service workers can “importScripts”Begging for a framework to handle pluggable pipeline
- With compatible plugins for processing
- Delta compression + scheduling + Stale While Revalidate…
- Just a matter of importing the libs