├── images ├── beast.jpg ├── beast.jxr ├── beast.webp ├── placeholder.jpg └── beast-savedata.jpg ├── service-worker.js ├── .eslintrc.js ├── imagebeast.min.js ├── imagebeast.js ├── readme.md └── index.html /images/beast.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanhume/image-beast/HEAD/images/beast.jpg -------------------------------------------------------------------------------- /images/beast.jxr: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanhume/image-beast/HEAD/images/beast.jxr -------------------------------------------------------------------------------- /images/beast.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanhume/image-beast/HEAD/images/beast.webp -------------------------------------------------------------------------------- /images/placeholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanhume/image-beast/HEAD/images/placeholder.jpg -------------------------------------------------------------------------------- /images/beast-savedata.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deanhume/image-beast/HEAD/images/beast-savedata.jpg -------------------------------------------------------------------------------- /service-worker.js: -------------------------------------------------------------------------------- 1 | (global => { 2 | 'use strict'; 3 | 4 | importScripts('./imagebeast.js'); 5 | optimize({ useWebp: true, useXr: true, useCache: true }); 6 | 7 | // Ensure that our service worker takes control of the page as soon as possible. 8 | global.addEventListener('install', event => event.waitUntil(global.skipWaiting())); 9 | global.addEventListener('activate', event => event.waitUntil(global.clients.claim())); 10 | })(self); 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "eslint:recommended", 3 | "parserOptions": { 4 | "sourceType": "module", 5 | }, 6 | "env": { 7 | "browser": true, 8 | "node": true, 9 | "mocha": true 10 | }, 11 | "globals": { 12 | "supertest": true 13 | }, 14 | "rules": { 15 | "global-require": 0, 16 | "no-cond-assign": [2, "except-parens"], 17 | "radix": 0, 18 | "no-unused-vars": [1, { "vars": "local", "args": "none" }], 19 | "no-else-return": 0, 20 | "no-console": 1, 21 | // Allow setting variables within ternary expressions 22 | "no-unused-expressions": [2, { "allowTernary": true }], 23 | "comma-dangle": 0, 24 | "eol-last": 0, 25 | "no-use-before-define": ["error", { "functions": false }], 26 | "space-before-function-paren": 0 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /imagebeast.min.js: -------------------------------------------------------------------------------- 1 | 'use strict';var dataCacheName=''+location.hostname+(location.port?':'+location.port:'')+'$$cache';function calculateSaveDataUrl(a){return a.substr(0,a.lastIndexOf('.'))+'-savedata'+a.substr(a.lastIndexOf('.'),a.length-1)}function fetchAndProcess(a,b,c){return regeneratorRuntime.async(function(h){for(;;)switch(h.prev=h.next){case 0:return h.next=2,regeneratorRuntime.awrap(caches.match(b));case 2:if(d=h.sent,!d){h.next=5;break}return h.abrupt('return',d);case 5:return h.next=7,regeneratorRuntime.awrap(fetch(b));case 7:if(e=h.sent,e.ok||a.includes('placeholder')){h.next=10;break}return h.abrupt('return',fetch('/images/placeholder.jpg'));case 10:if(!c){h.next=18;break}return h.next=13,regeneratorRuntime.awrap(caches.open(dataCacheName));case 13:return f=h.sent,f.put(b,e.clone()),h.abrupt('return',e);case 18:return h.abrupt('return',e);case 19:case'end':return h.stop();}},null,this)}function calculateReturnUrl(a,b,c,d){return a.get('accept').includes('webp')&&d?b.substr(0,b.lastIndexOf('.'))+'.webp':a.get('accept').includes('ms-photo')&&c?b.substr(0,b.lastIndexOf('.'))+'.jxr':void 0}function optimize(a){return regeneratorRuntime.async(function(c){for(;;)switch(c.prev=c.next){case 0:this.addEventListener('fetch',function(d){var e,f=d.request.headers,g=d.request.url;/\.jpg$|.gif$|.png$/.test(g)&&!g.includes('placeholder')&&(f.get('save-data')&&a.useSaveData?e=calculateSaveDataUrl(g):f.has('accept')&&(e=calculateReturnUrl(f,g,a.useXr,a.useWebp)),e==void 0&&(e=g),d.respondWith(fetchAndProcess(g,e,a.useCache)))});case 1:case'end':return c.stop();}},null,this)} -------------------------------------------------------------------------------- /imagebeast.js: -------------------------------------------------------------------------------- 1 | const dataCacheName = `${location.hostname}${(location.port ? ':' + location.port : '')}$$cache`; 2 | 3 | /** 4 | * Calculates the new URL that we want to use 5 | * @param {string} requestUrl 6 | */ 7 | function calculateSaveDataUrl(requestUrl){ 8 | return `${requestUrl.substr(0, requestUrl.lastIndexOf("."))}-savedata${requestUrl.substr(requestUrl.lastIndexOf("."), requestUrl.length - 1)}`; 9 | } 10 | 11 | /** 12 | * Fetch the resource and process the results 13 | * @param {string} returnUrl 14 | */ 15 | async function fetchAndProcess(requestUrl, returnUrl, useCache) { 16 | // Does the URL already exist in cache? 17 | const cacheResults = await caches.match(returnUrl); 18 | 19 | if (cacheResults) { 20 | return cacheResults; 21 | } 22 | 23 | // If not, we need to go and get it 24 | const returnedResponse = await fetch(returnUrl); 25 | 26 | // Was the request successful? 27 | if (!returnedResponse.ok && !requestUrl.includes('placeholder')) { 28 | return fetch('/images/placeholder.jpg'); 29 | } 30 | 31 | // Should we cache the resource? 32 | if (useCache) { 33 | const cache = await caches.open(dataCacheName); 34 | cache.put(returnUrl, returnedResponse.clone()); 35 | return returnedResponse; 36 | } else { 37 | return returnedResponse; // Don't cache 38 | } 39 | } 40 | 41 | /** 42 | * Calculates the URL to return based on the headers passed through. 43 | * @param {Object} acceptHeaders 44 | * @param {string} requestUrl 45 | * @param {boolean} useXr - Should we use JpegXR? 46 | * @param {boolean} useWebp - Should we use WebP? 47 | */ 48 | function calculateReturnUrl(acceptHeaders, requestUrl, useXr, useWebp) { 49 | 50 | // Is WebP supported? 51 | if (acceptHeaders.get('accept').includes('webp') && useWebp) { 52 | return `${requestUrl.substr(0, requestUrl.lastIndexOf("."))}.webp`; 53 | } 54 | 55 | // Is jpegXR supported? 56 | if (acceptHeaders.get('accept').includes('ms-photo') && useXr) { 57 | return `${requestUrl.substr(0, requestUrl.lastIndexOf("."))}.jxr`; 58 | } 59 | } 60 | 61 | /** 62 | * Choose the best image format based on the current configuration settings. 63 | * @param {Object} config 64 | */ 65 | async function optimize(config) { 66 | this.addEventListener('fetch', event => { 67 | 68 | let returnUrl; 69 | const headers = event.request.headers; 70 | const requestUrl = event.request.url; 71 | 72 | if (/\.jpg$|.gif$|.png$/.test(requestUrl) && !requestUrl.includes('placeholder')) { // Check for images and not our placeholder 73 | if (headers.get('save-data') && config.useSaveData) { // Check for the save data headers 74 | returnUrl = calculateSaveDataUrl(requestUrl); 75 | } 76 | else if (headers.has('accept')) { // Inspect the accept header for alternate image support 77 | returnUrl = calculateReturnUrl(headers, requestUrl, config.useXr, config.useWebp); 78 | } 79 | 80 | // Double check that we didnt miss any cases and fallback 81 | if (returnUrl === undefined) { returnUrl = requestUrl; } 82 | 83 | event.respondWith(fetchAndProcess(requestUrl, returnUrl, config.useCache)); 84 | } 85 | }); 86 | } 87 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | ![Image Beast](https://raw.githubusercontent.com/deanhume/image-beast/master/images/beast.jpg) 3 | 4 | ## The Image Beast - [10K Apart Entry](https://a-k-apart.com/gallery/The-Image-Beast) 5 | 6 | From the murky depths of the sea, comes the image beast! It’s here to tame your every image need with a handy [1 KB script](https://github.com/deanhume/image-beast/blob/master/imagebeast.min.js). There are so many great different image formats available these days, and many of them offer significant file size savings over the traditional formats (JPEG, GIF, PNG). Unfortunately not every browser provides support for all of these great image formats. We want to pass on these savings to all of our users. 7 | 8 | This is where the image beast comes in! Simply include this [tiny 1KB script](https://github.com/deanhume/image-beast/blob/master/imagebeast.min.js) in your Service Worker, create the appropriate image formats, and it will handle the rest for you. 9 | 10 | Serve the lightest, leanest images to your users and help them get the fastest page load times possible. Grrrrrr! 11 | The image beast does all of these things: 12 | 13 | - Serves WebP images to Google’s Chrome Browser 14 | - Serves JpegXR images to Microsoft’s Edge Browser 15 | - Serves lean images to Android users with the Data Saver feature enabled 16 | - Caches the images! 17 | 18 | ## Progressive Enhancement 19 | 20 | What about older browsers I hear you say? Well, the beast has them tamed too! Service Workers are the ultimate progressive enhancement and if your browser doesn’t support them they will simply return the original images as normal. If your browser does support Service Workers, then the image beast simply uses it slippery tentacles to decide the perfect image format to return. 21 | 22 | [WebP](https://developers.google.com/speed/webp/)? [JPEGXR](https://msdn.microsoft.com/en-gb/library/windows/desktop/hh707223.aspx)? Or even a low resolution version of an image? The image beast is in control. 23 | 24 | ## Show me an example! 25 | 26 | First up, you need to check if Service Workers are supported in your browser. If they are, create and register the Service Worker by adding the following code to your page. 27 | 28 | ```html 29 | 38 | 39 | ``` 40 | 41 | Next, simply import the script into your Service Worker (service-worker.js) and begin optimizing. 42 | 43 | ```js 44 | (global => { 45 | 'use strict'; 46 | 47 | importScripts('./imagebeast.min.js'); 48 | optimize({ useWebp: false, useXr: true, useSaveData: true, useCache: true }); 49 | 50 | // Ensure that our service worker takes control of the page as soon as possible. 51 | global.addEventListener('install', event => event.waitUntil(global.skipWaiting())); 52 | global.addEventListener('activate', event => event.waitUntil(global.clients.claim())); 53 | })(self); 54 | 55 | ``` 56 | 57 | As you can see from the code above, you can also configure image beast. For example, if you’d prefer not to return WebP images, then simply disable it in the config. 58 | 59 | If you’d like to see this in action, head over to [deanhume.github.io/image-beast](https://deanhume.github.io/image-beast/index.html) for a working example. 60 | 61 | ## How does it work under the hood? 62 | 63 | The image beast works by listening to the client hints that your browser sends through when it makes an HTTP request. Let's say you are using Google Chrome as your browser. By default, the browser will send through an Accept Request header and in the 64 | case of Chrome, it will send through a client hint of "image/webp" notifying the server that it supports WebP images. Using a Service Worker, we can tap into this request and return an alternate version of the image. 65 | 66 | Hey, but I don't use Google Chrome! Don't worry - the image beast has you covered. Microsoft's Edge currently has Service Workers under development and the image beast will return JPEGXR images for you if it detects that it can support it. The image beast is prowling and ready to pounce on every image format. 67 | 68 | ## Performance is key! 69 | 70 | Using the image beast, you are able to return the leanest possible images to your user's browser. Using the power of Service Workers, we can take this a step further and cache the image request once it has been made. This means that the user won't even have to make another HTTP request for the image and it will load instantly. It is built-in by default, but you can turn it off using the configuration options. 71 | 72 | The image beast also has another sneaky trick up it's tentacle. Users that use Android devices and have the [Data Saver feature](https://support.google.com/chrome/answer/2392284?hl=en-GB) enabled for their phone or tablet will have low resolution images returned to them. All you need to do is save a low resolution version of the image with "-savedata" appended to the file name (eg. beast-savedata.jpg) and the image beast will do the rest. In fact, even if you haven't provided a low resolution version of the image, it will simply return a tiny placeholder image in order 73 | to save your users data. 74 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Image Beast 6 | 7 | 8 | 9 | The image beast 10 |

The Image Beast

11 |

12 | From the murky depths of the sea, comes the image beast! It's here to tame your every image need with a handy 1 KB script. 13 | There are so many great different image formats available these days, and many of them offer significant file size savings over the traditional formats (JPEG, GIF, PNG). 14 | Unfortunately not every browser provides support for all of these great image formats. We want to pass on these savings to 15 | all of our users. 16 |
17 |
18 | This is where the image beast comes in! Simply include this tiny 1KB script in your Service Worker, create the appropriate image formats, and 19 | it will handle the rest for you. 20 |
21 | Serve the lightest, leanest images to your users and help them get the fastest page load times possible. Grrrrrr! 22 |
23 |
24 | The image beast does all of these things: 25 |

31 |

32 |

Progressive Enhancement

33 |

34 | What about older browsers I hear you say? Well, the beast has them tamed too! Service Workers are the ultimate progressive 35 | enhancement and if your browser doesn't support them they will simply return the images as normal. If your browser does support 36 | Service Workers, then the image beast simply uses it slippery tentacles to decide the perfect image format to return. 37 |
38 |
39 | WebP? JPEGXR? 40 | Or even a low resolution version of an image? The image beast is in control. 41 |

42 |

Show me an example!

43 |

44 | First up, you need to check if Service Workers are supported in your browser. If they are, create and register the Service Worker 45 | by adding the following code to your page. 46 |

47 |
 48 | (global => {
 49 |   'use strict';
 50 | 
 51 |   importScripts('./imagebeast.min.js');
 52 |   optimize({ useWebp: false, useXr: true, useSaveData: true, useCache: true });
 53 | 
 54 |   // Ensure that our service worker takes control of the page as soon as possible.
 55 |   global.addEventListener('install', event => event.waitUntil(global.skipWaiting()));
 56 |   global.addEventListener('activate', event => event.waitUntil(global.clients.claim()));
 57 | })(self);
58 |

59 | Next, simply import the script into your Service Worker (service-worker.js) and begin optimizing. 60 |

61 |
 62 |  // Register the service worker
 63 |  if ('serviceWorker' in navigator) {
 64 |  navigator.serviceWorker.register('./service-worker.js').then(function(registration) {
 65 |    // Registration was successful
 66 |    console.log('ServiceWorker registration successful with scope: ', registration.scope);
 67 |  })
 68 |  }
 69 | 
70 |

71 | As you can see from the code above, you can also configure image beast. For example, if you'd prefer not to return WebP images, then 72 | simply disable it in the config. 73 |

74 | If you'd like to see this in action, look no further than the page you are currently reading - it is using the image beast script! 75 | Open up your Developer Tools and see the beast in action. 76 |

77 |

How does it work under the hood?

78 |

79 | The image beast works by listening to the client hints that your browser sends through when it makes an HTTP request. Let's say you are using Google Chrome as your browser. 80 | By default, the browser will send through an Accept Request header and in the 81 | case of Chrome, it will send through a client hint of "image/webp" notifying the server that it supports WebP images. Using a Service Worker, 82 | we can tap into this request and return an alternate version of the image. 83 |
84 |
85 | Hey, but I don't use Google Chrome! Don't worry - the image beast has you covered. Microsoft's Edge currently has Service Workers under 86 | development and the image beast will return JPEGXR images for you if it detects that it can support it. The image beast is prowling and 87 | ready to pounce on every image format. 88 |

89 |

Performance is key!

90 |

91 | Using the image beast, you are able to return the leanest possible images to your user's browser. Using the power of Service Workers, 92 | We can take this a step further and cache the image request once it has been made. 93 | This means that the user won't even have to make another HTTP request for the image and it will load instantly. It is built-in by default, but you can turn it off using the configuration options. 94 |
95 |
96 | The image beast also has another sneaky trick up it's tentacle. Users that use Android devices and have the Data Saver feature enabled for their phone or tablet 97 | will have low resolution images returned to them. All you need to do is save a low resolution version of the image with "-savedata" appended to the file name (eg. beast-savedata.jpg) and 98 | the image beast will do the rest. In fact, even if you haven't provided a low resolution version of the image, it will simply return a tiny placeholder image in order 99 | to save your users data. 100 |

101 | 102 | 103 | 104 | --------------------------------------------------------------------------------