Last active
November 29, 2024 22:07
-
-
Save dsheiko/8a5878678371f950d37f3ee074fe8031 to your computer and use it in GitHub Desktop.
Service-worker to prefetch remote images (with expiration) and respond with fallback one when image cannot be fetched
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Service-worker demo</title> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<script> | |
if ( "serviceWorker" in navigator ) { | |
navigator.serviceWorker | |
.register( "./service-worker.js", { scope: './' }) | |
.then(function() { | |
console.log( "Service Worker Registered" ); | |
}) | |
.catch(function( err ) { | |
console.log( "Service Worker Failed to Register", err ); | |
}); | |
} | |
</script> | |
</head> | |
<style> | |
img { | |
display: block; | |
margin: 8px; | |
} | |
</style> | |
<body> | |
<img src="http://ipsumimage.appspot.com/216x120,ff7700?l=foo.png" alt="Image" itemprop="image"> | |
<img src="http://ipsumimage.appspot.com/216x120,ff7700?l=bar.png" alt="Image" itemprop="image"> | |
<img src="http://ipsumimage.appspot.com/216x120,ff7700?l=baz.png" alt="Image" itemprop="image"> | |
<img src="http://ipsumimage.appspot.com/216x120,ff7700?l=quiz.png" alt="Image" itemprop="image"> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Service worker interepts requests for images | |
* It puts retrieved images in cache for 10 minutes | |
* If image not found responds with fallback | |
*/ | |
var INVALIDATION_INTERVAL = 10 * 60 * 1000; // 10 min | |
var NS = "MAGE"; | |
var SEPARATOR = "|"; | |
var VERSION = Math.ceil( now() / INVALIDATION_INTERVAL ); | |
/** | |
* Helper to get current timestamp | |
* @returns {Number} | |
*/ | |
function now() { | |
var d = new Date(); | |
return d.getTime(); | |
} | |
/** | |
* Build cache storage key that includes namespace, url and record version | |
* @param {String} url | |
* @returns {String} | |
*/ | |
function buildKey( url ) { | |
return NS + SEPARATOR + url + SEPARATOR + VERSION; | |
} | |
/** | |
* The complete Triforce, or one or more components of the Triforce. | |
* @typedef {Object} RecordKey | |
* @property {String} ns - namespace | |
* @property {String} url - request identifier | |
* @property {String} ver - record varsion | |
*/ | |
/** | |
* Parse cache key | |
* @param {String} key | |
* @returns {RecordKey} | |
*/ | |
function parseKey( key ) { | |
var parts = key.split( SEPARATOR ); | |
return { | |
ns: parts[ 0 ], | |
key: parts[ 1 ], | |
ver: parseInt( parts[ 2 ], 10 ) | |
}; | |
} | |
/** | |
* Invalidate records matchinf actual version | |
* | |
* @param {Cache} caches | |
* @returns {Promise} | |
*/ | |
function purgeExpiredRecords( caches ) { | |
console.log( "Purging..." ); | |
return caches.keys().then(function( keys ) { | |
return Promise.all( | |
keys.map(function( key ) { | |
var record = parseKey( key ); | |
if ( record.ns === NS && record.ver !== VERSION ) { | |
console.log("deleting", key); | |
return caches.delete( key ); | |
} | |
}) | |
); | |
}); | |
} | |
/** | |
* Proxy request using cache-first strategy | |
* | |
* @param {Cache} caches | |
* @param {Request} request | |
* @returns {Promise} | |
*/ | |
function proxyRequest( caches, request ) { | |
var key = buildKey( request.url ); | |
// set namespace | |
return caches.open( key ).then( function( cache ) { | |
// check cache | |
return cache.match( request ).then( function( cachedResponse ) { | |
if ( cachedResponse ) { | |
console.info( "Take it from cache", request.url ); | |
return cachedResponse; | |
} | |
// { mode: "no-cors" } gives opaque response | |
// https://fetch.spec.whatwg.org/#concept-filtered-response-opaque | |
// so we cannot get info about response status | |
return fetch( request.clone() ) | |
.then(function( networkResponse ) { | |
if ( networkResponse.type !== "opaque" && networkResponse.ok === false ) { | |
throw new Error( "Resource not available" ); | |
} | |
console.info( "Fetch it through Network", request.url, networkResponse.type ); | |
cache.put( request, networkResponse.clone() ); | |
return networkResponse; | |
}).catch(function() { | |
console.error( "Failed to fetch", request.url ); | |
// Placeholder image for the fallback | |
return fetch( "./placeholder.jpg", { mode: "no-cors" }); | |
}); | |
}); | |
}); | |
} | |
self.addEventListener( "install", function( event ) { | |
event.waitUntil( self.skipWaiting() ); | |
}); | |
self.addEventListener( "activate", function( event ) { | |
event.waitUntil( purgeExpiredRecords( caches ) ); | |
}); | |
self.addEventListener( "fetch", function( event ) { | |
var request = event.request; | |
console.log( "Detected request", request.url ); | |
if ( request.method !== "GET" || | |
!request.url.match( /\.(jpe?g|png|gif|svg)$/ ) ) { | |
return; | |
} | |
console.log( "Accepted request", request.url ); | |
event.respondWith( | |
proxyRequest( caches, request ) | |
); | |
}); |
It's simply available for Service Workers https://developers.google.com/web/ilt/pwa/caching-files-with-service-worker
Thanks
Simple and effective, thanks @dsheiko!
Thanks for the solution, but this creates a different cache store per asset (I ended up with hundreds of MAGE|*
cache stores). I created a version (specially for NextJS or React apps) that uses a single cache store and every put
just adds a new entry to it like in this picture:
The adapted code: https://gist.github.com/itsjavi/f3913770eb34d6d752e780c46e80cdea
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Where is the variable
caches
created?