Jake Archibald

Follow this blog

Don't use functions as callbacks unless they're designed for it

Here's an old pattern that seems to be making a comeback: // Convert some numbers into human-readable strings: import { toReadableNumber } from 'some-library'; const readableNumbers = someNumbers.map(toReadableNumber); Where the implementation of toReadableNumber is like this: export function toReadableNumber(num) { // Return num as string in a human readable form. // Eg 10000000 might become '10,000,000' } Everything works great until some-library is updated, then everything breaks. But it isn't some-library's fault – they never designed toReadableNumber to be a callback to array.map. Here's the problem: // We think of: const readableNumbers = someNumbers.map(toReadableNumber); // …as being like: const readableNumbers = someNumbers.map((n) => toReadableNumber(n)); // …but it's more like: const readableNumbers = someNumbers.map((item, index, arr) => toReadableNumber(item, index, arr), ); We're also passing the index of the item in the array, and the array itself to toReadableNumber. This worked fine at first, because toReadableNumber only had one parameter, but in the new version: export function toReadableNumber(num, base = 10) { // Return num as string in a human readable form. // In base 10 by default, but this can be changed. } The developers of toReadableNumber felt they were making a backwards-compatible change. They added a new parameter, and gave it a default value. However, they didn't expect that some code would have already been calling the function with three arguments. toReadableNumber wasn't designed to be a callback to array.map, so the safe thing to do is create your own function that is designed to work with array.map: const readableNumbers = someNumbers.map((n) => toReadableNumber(n)); And that's it! The developers of toReadableNumber can now add parameters without breaking our code. The same issue, but with web platform functions Here's an example I saw recently: // A promise for the next frame: const nextFrame = () => new Promise(requestAnimationFrame); But this is equivalent to: const nextFrame = () => new Promise((resolve, reject) => requestAnimationFrame(resolve, reject)); This works today because requestAnimationFrame only does something with the first argument, but that might not be true forever. A extra parameter might be added, and the above code could break in whatever browser ships the updated requestAnimationFrame. The best example of this pattern going wrong is probably: const parsedInts = ['-10', '0', '10', '20', '30'].map(parseInt); If anyone asks you the result of that in a tech interview, I recommend rolling your eyes and walking out. But anyway, the answer is [-10, NaN, 2, 6, 12], because parseInt has a second parameter. Option objects can have the same gotcha Chrome 90 will allow you to use an AbortSignal to remove an event listener, meaning a single AbortSignal can be used to remove event listeners, cancel fetches, and anything else that supports signals: const controller = new AbortController(); const { signal } = controller; el.addEventListener('mousemove', callback, { signal }); el.addEventListener('pointermove', callback, { signal }); el.addEventListener('touchmove', callback, { signal }); // Later, remove all three listeners: controller.abort(); However, I saw an example where instead of: const controller = new AbortController(); const { signal } = controller; el.addEventListener(name, callback, { signal }); …they did this: const controller = new AbortController(); el.addEventListener(name, callback, controller); As with the callback examples, this works today, but it might break in future. An AbortController was not designed to be an option object to addEventListener. It works right now because the only thing AbortController and the addEventListener options have in common is the signal property. If, say in future, AbortController gets a controller.capture(otherController) method, the behaviour of your listeners will change, because addEventListener will see a truthy value in the capture property, and capture is a valid option for addEventListener. As with the callback examples, it's best to create an object that's designed to be addEventListener options: const controller = new AbortController(); const options = { signal: controller.signal }; el.addEventListener(name, callback, options); // Although I find this pattern easier when multiple things // get the same signal: const { signal } = controller; el.addEventListener(name, callback, { signal }); And that's it! Watch out for functions being used as callbacks, and objects being used as options, unless they were designed for those purposes. Unfortunately I'm not aware of a linting rule that catches it (edit: looks like there's a rule that catches some cases, thanks James Ross!). TypeScript doesn't solve this Edit: When I first posted this, it had a little note at the end showing that TypeScript doesn't prevent this issue, but I still got folks on Twitter telling me to "just use TypeScript", so let's look at it in more detail. TypeScript doesn't like this: function oneArg(arg1: string) { console.log(arg1); } oneArg('hello', 'world'); // ^^^^^^^ // Expected 1 arguments, but got 2. But it's fine with this: function twoArgCallback(cb: (arg1: string, arg2: string) => void) { cb('hello', 'world'); } twoArgCallback(oneArg); …even though the result is the same. Therefore TypeScript is fine with this: function toReadableNumber(num): string { // Return num as string in a human readable form. // Eg 10000000 might become '10,000,000' return ''; } const readableNumbers = [1, 2, 3].map(toReadableNumber); If toReadableNumber changed to add a second string param, TypeScript would complain, but that isn't what happened in the example. An additional number param was added, and this meets the type constraints. Things get worse with the requestAnimationFrame example, because this goes wrong after a new version of a browser is deployed, not when a new version of your project is deployed. Additionally, TypeScript DOM types tend to lag behind what browsers ship by months. I'm a fan of TypeScript, this blog is built using TypeScript, but it does not solve this problem, and probably shouldn't. Most other typed languages behave differently to TypeScript here, and disallow casting callbacks in this way, but TypeScript's behaviour here is intentional, otherwise it'd reject the following: const numbers = [1, 2, 3]; const doubledNumbers = numbers.map((n) => n * 2); …since the callback given to map is cast to a callback with more params. The above is an extremely common pattern in JavaScript, and totally safe, so it's understandable that TypeScript made an exception for it. The question is "is the function intended to be a callback to map?", and in a JavaScript world that isn't really solvable with types. Instead, I wonder if new JS and web functions should throw if they're called with too many arguments. This would 'reserve' additional parameters for future use. We couldn't do it with existing functions, as that would break backwards compatibility, but we could show console warnings for existing functions that we might want to add parameters to later. I proposed this idea, but folks don't seem too excited about it 😀. Things are a bit tougher when it comes to option objects: interface Options { reverse?: boolean; } function whatever({ reverse = false }: Options = {}) { console.log(reverse); } You could say that APIs should warn/fail if the object passed to whatever has properties other than reverse. But in this example: whatever({ reverse: true }); …we're passing an object with properties like toString, constructor, valueOf, hasOwnProperty etc etc since the object above is an instance of Object. It seems too restrictive to require that the properties are 'own' properties (that isn't how it currently works at runtime), but maybe we could add some allowance for properties that come with Object. Thanks to my podcast husband Surma for proof-reading and suggestions, and Ryan Cavanaugh for correcting me on the TypeScript stuff.

CSS paint API: Being predictably random

Take a look at this: '; initial-value: black; inherits: true; } @property --pixel-gradient-seed { syntax: ''; initial-value: 1; inherits: true; } @property --pixel-gradient-size { syntax: ''; initial-value: 8px; inherits: true; } @property --confetti-density { syntax: ''; initial-value: 200; inherits: true; } @property --confetti-seed { syntax: ''; initial-value: 10; inherits: true; } @property --confetti-length-variance { syntax: ''; initial-value: 15px; inherits: true; } @property --confetti-weight-variance { syntax: ''; initial-value: 4px; inherits: true; } Space invaders If you're using a browser that supports the CSS paint API, the element will have a 'random' pixel-art gradient in the background. But it turns out, doing random in CSS isn't as easy as it seems… Initial implementation This isn't a full tutorial on the CSS paint API, so if the below isn't clear or you want to know more, check out the resources on houdini.how, and this great talk by Una. First up, register a paint worklet: CSS.paintWorklet.addModule(`worklet.js`); Painting happens in a worklet so it doesn't block the main thread. Here's the worklet: class PixelGradient { static get inputProperties() { // The CSS values we're interested in: return ['--pixel-gradient-color', '--pixel-gradient-size']; } paint(ctx, bounds, props) { // TODO: We'll get to this later } } // Give our custom painting a name // (this is how CSS will reference it): registerPaint('pixel-gradient', PixelGradient); And some CSS: /* The end colour of the gradient */ @property --pixel-gradient-color { syntax: '<color>'; initial-value: black; inherits: true; } /* The size of each block in the gradient */ @property --pixel-gradient-size { syntax: '<length>'; initial-value: 8px; inherits: true; } .pixel-gradient { --pixel-gradient-color: #9a9a9a; background-color: #8a8a8a; /* Tell the browser to use our worklet for the background image */ background-image: paint(pixel-gradient); } @property tells the browser the format of the custom properties. This is great as it means values can animate, and things like --pixel-gradient-size can be specified in em, %, vw etc etc – they'll be converted to pixels for the paint worklet. Right ok, now let's get to the main bit, the painting of the element. The input is: ctx: A subset of the 2d canvas API. bounds: The width & height of the area to paint. props: The computed values for our inputProperties. Here's the body of the paint method to create our random gradient: // Get styles from our input properties: const size = props.get('--pixel-gradient-size').value; ctx.fillStyle = props.get('--pixel-gradient-color'); // Loop over columns for (let x = 0; x < bounds.width; x += size) { // Loop over rows for (let y = 0; y < bounds.height; y += size) { // Convert our vertical position to 0-1 const pos = (y + size / 2) / bounds.height; // Only draw a box if a random number // is less than pos if (Math.random() < pos) ctx.fillRect(x, y, size, size); } } So we've created a blocky gradient that's random, but there's a higher chance of a block towards the bottom of the element. Job done? Well, here it is: function addButtons(el, btns) { for (const [label, callback] of btns) { const btn = document.createElement('button'); btn.classList.add('btn'); btn.textContent = label; btn.addEventListener('click', callback); el.append(btn); } } function callbackForEl(callback) { return (event) => callback(event.target.closest('.full-figure').querySelector('.target-el')); } const initialButtonSet = [ ['Animate width', callbackForEl(el => el.classList.toggle('width'))], ['Animate height', callbackForEl(el => el.classList.toggle('height'))], ['Animate colours', callbackForEl(el => el.classList.toggle('colours'))], ['Animate box size', callbackForEl(el => el.classList.toggle('block-size'))], ['Change text', callbackForEl(el => el.textContent = el.textContent.trim() === 'Hello' ? 'World' : 'Hello')], ['Animate box-shadow', callbackForEl(el => el.classList.toggle('box-shadow'))], ['Animate blur', callbackForEl(el => el.classList.toggle('blur'))], ]; if (demosSupported) { addButtons(document.querySelector('.v1-buttons'), initialButtonSet); } else { document.querySelector('.v1-buttons').textContent = `Your browser does not support this demo`; } One of the things I love about the paint API is how easy it is to create animations. Even for animating the block size, all I had to do is create a CSS transition on --pixel-gradient-size. Anyway, play with the above or try resizing your browser. Sometimes the pattern in the background changes, sometimes it doesn't. The paint API is optimised with determinism in mind. The same input should produce the same output. In fact, the spec says if the element size and inputProperties are the same between paints, the browser may use a cached copy of our paint instructions. We're breaking that assumption with Math.random(). I'll try and explain what I see in Chrome: Why does the pattern change while animating width / height / colour / box size? These change the element size or our input properties, so the element has to repaint. Since we use Math.random(), we get a new random result. Why does it stay the same while changing the text? This requires a repaint, but since the element size and input remain the same, the browser uses a cached version of our pattern. Why does it change while animating box-shadow? Although the box-shadow change means the element needs repainting, box-shadow doesn't change the element size, and box-shadow isn't one of our inputProperties. It feels like the browser could use a cached version of our pattern here, but it doesn't. And that's fine, the spec doesn't require the browser to use a cached copy here. Why does it change twice when animating blur? Hah, well, animating blur happens on the compositor, so you get an initial repaint to lift the element onto its own layer. But, during the animation, it just blurs the cached result. Then, once the animation is complete, it drops the layer, and paints the element as a regular part of the page. The browser could use a cached result for these repaints, but it doesn't. How the above behaves may differ depending on the browser, multiplied by version, multiplied by display/graphics hardware. I explained this to my colleagues, and they said "So what? It's fun! Stop trying to crush fun Jake". Well, I'm going to show you that you can create pseudorandom effects with paint determinism and smooth animation while having fun. Maybe. Making random, not random Computers can't really do random. Instead, they take some state, and do some hot maths all over it to create a number. Then, they modify that state so the next number seems unrelated to the previous ones. But the truth is they're 100% related. If you start with the same initial state, you'll get the same sequence of random numbers. That's what we want – something that looks random, but it's 100% reproducible. The good news is that's how Math.random() works, the bad news is it doesn't let us set the initial state. Instead, let's use another implementation that does let us set the initial state: function mulberry32(a) { return function () { a |= 0; a = (a + 0x6d2b79f5) | 0; var t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } // Using an initial state of 123456: const rand = mulberry32(123456); rand(); // 0.38233304349705577 rand(); // 0.7972629074938595 rand(); // 0.9965302373748273 This gist has a great collection of random number generators. I picked mulberry32 because it's simple, and good enough for visual randomness. I want to stress that I'm only recommending this for visual randomness. If you're implementing your own cryptography, this is the only piece of advice I'm qualified to give: don't. I'm not saying mulberry32 is bad either. I'm just saying, if all your buttcoins get stolen because you were influenced by this article, don't come crying to me. Anyway, here's mulberry32 in action: div { --size: 10px; width: var(--size); height: var(--size); border-radius: var(--size); background: #036b58; position: absolute; top: 50%; transform: translate(-50%, -50%); opacity: 0.3; } Seed: Output: Distribution: Generate number Start over function mulberry32(a) { return function () { a |= 0; a = (a + 0x6d2b79f5) | 0; var t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } const form = document.querySelector('.random-form'); const seedInput = document.querySelector('#seed-input'); const restartBtn = document.querySelector('.btn-restart'); const result = document.querySelector('.random-output'); const distribution = document.querySelector('.distribution'); let rand; const restart = (event) => { event.preventDefault(); rand = undefined; result.textContent = ''; distribution.textContent = ''; } seedInput.addEventListener('input', restart); restartBtn.addEventListener('click', restart); form.addEventListener('submit', (event) => { event.preventDefault(); if (!rand) rand = mulberry32(seedInput.valueAsNumber); const num = rand(); result.textContent = num; const div = document.createElement('div'); div.style.left = num * 100 + '%'; distribution.append(div); }); Notice how for a given seed, the sequence of seemingly random numbers is the same every time. Are you having fun yet?? Let's put mulberry32 to work here… Making paint deterministic We'll add another custom property for the seed: @property --pixel-gradient-seed { syntax: '<number>'; initial-value: 1; inherits: true; } …which we'll also add to our inputProperties. Then, we can modify our paint code: const size = props.get('--pixel-gradient-size').value; ctx.fillStyle = props.get('--pixel-gradient-color'); // Get the seed… const seed = props.get('--pixel-gradient-seed').value; // …and create a random number generator: const rand = mulberry32(seed); for (let x = 0; x < bounds.width; x += size) { for (let y = 0; y < bounds.height; y += size) { const pos = (y + size / 2) / bounds.height; // …and use it rather than Math.random() if (rand() < pos) ctx.fillRect(x, y, size, size); } } And here it is: const secondButtonSet = [ ...initialButtonSet, ['Increment seed', callbackForEl(el => { el.style.setProperty( '--pixel-gradient-seed', el.computedStyleMap().get('--pixel-gradient-seed').value + 1 ); })] ]; if (demosSupported) { addButtons(document.querySelector('.v2-buttons'), secondButtonSet); } else { document.querySelector('.v2-buttons').textContent = `Your browser does not support this demo`; } And now animating width, colour, shadow, and blur isn't glitching! Buuuuut we can't say the same for animating height and block size. Let's fix that! So much fun, right?? Handling rows and columns Right now we're calling rand() for every block. Take a look at this: const pixelGridButtons = [ ['Animate width', callbackForEl(el => el.classList.toggle('width'))], ['Animate height', callbackForEl(el => el.classList.toggle('height'))], ]; const pixelGridGap = 6; const pixelGridSize = 42; let width, height; addButtons(document.querySelector('.pixel-grid-buttons-v1'), pixelGridButtons); function createDrawCallback(el, callback) { let drawOnIntersection = true; let intersecting = false; new IntersectionObserver((entries) => { const wasIntersecting = intersecting; intersecting = entries[0].isIntersecting; if (!wasIntersecting && intersecting && drawOnIntersection) { drawOnIntersection = false; callback(width, height); } }).observe(el); new ResizeObserver(([entry]) => { if ('devicePixelContentBoxSize' in entry) { width = entry.devicePixelContentBoxSize[0].inlineSize; height = entry.devicePixelContentBoxSize[0].blockSize; } else { width = entry.contentRect.width * devicePixelRatio; height = entry.contentRect.height * devicePixelRatio; } if (intersecting) { callback(width, height); return; } drawOnIntersection = true; }).observe(el, {box: ['device-pixel-content-box']}); } { const canvas = document.querySelector('.pixel-grid-v1'); const ctx = canvas.getContext('2d'); createDrawCallback(canvas, (width, height) => { const bounds = canvas.getBoundingClientRect(); canvas.width = width; canvas.height = height; ctx.scale(devicePixelRatio, devicePixelRatio); ctx.font = '15px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; let count = 0; // Loop over columns for (let x = pixelGridGap; x + pixelGridSize Let's say each square is a block, and the numbers represent the number of times rand() is called. When you animate width, the numbers stay in the same place, but when you animate height, they move (aside from the first column). So, as the height changes, the randomness of our pixels changes, which makes it look like the noise is animating. Instead, we want something more like this: addButtons(document.querySelector('.pixel-grid-buttons-v2'), pixelGridButtons); { const canvas = document.querySelector('.pixel-grid-v2'); const ctx = canvas.getContext('2d'); createDrawCallback(canvas, (width, height) => { const bounds = canvas.getBoundingClientRect(); canvas.width = width; canvas.height = height; ctx.scale(devicePixelRatio, devicePixelRatio); ctx.font = '15px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; let xCount = 0; // Loop over columns for (let x = pixelGridGap; x + pixelGridSize …where our random values have two dimensions. Thankfully, we already have two dimensions to play with, the number of times rand() is called, and the seed. Have you ever had this much fun?? Being randomly predictable in two dimensions This time, we'll reseed our random function for each column: const size = props.get('--pixel-gradient-size').value; ctx.fillStyle = props.get('--pixel-gradient-color'); let seed = props.get('--pixel-gradient-seed').value; for (let x = 0; x < bounds.width; x += size) { // Create a new rand() for this column: const rand = mulberry32(seed); // Increment the seed for next time: seed++; for (let y = 0; y < bounds.height; y += size) { const pos = (y + size / 2) / bounds.height; if (rand() < pos) ctx.fillRect(x, y, size, size); } } And here it is: if (demosSupported) { addButtons(document.querySelector('.v3-buttons'), secondButtonSet); } else { document.querySelector('.v3-buttons').textContent = `Your browser does not support this demo`; } Now height and block size animate in a more natural way! But there's one last thing to fix. By incrementing the seed by 1 for each column we've introduced visual predictability into our pattern. You can see this if you 'increment seed' – instead of producing a new random pattern, it shifts the pattern along (until it gets past JavaScript's maximum safe integer, at which point spooky things happen). Instead of incrementing the seed by 1, we want to change it in some way that feels random, but is 100% deterministic. Oh wait, that's what our rand() function does! In fact, let's create a version of mulberry32 that can be 'forked' for multiple dimensions: function randomGenerator(seed) { let state = seed; const next = () => { state |= 0; state = (state + 0x6d2b79f5) | 0; var t = Math.imul(state ^ (state >>> 15), 1 | state); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; return { next, // Instead of incrementing, set the seed // to a 'random' 32 bit value: fork: () => randomGenerator(next() * 2 ** 32), }; } We use a random 32 bit value, since that's the amount of state mulberry32 works with. Then our paint method can use that: const size = props.get('--pixel-gradient-size').value; ctx.fillStyle = props.get('--pixel-gradient-color'); const seed = props.get('--pixel-gradient-seed').value; // Create our initial random generator: const randomXs = randomGenerator(seed); for (let x = 0; x < bounds.width; x += size) { // Then fork it for each column: const randomYs = randomXs.fork(); for (let y = 0; y < bounds.height; y += size) { const pos = (y + size / 2) / bounds.height; if (randomYs.next() < pos) ctx.fillRect(x, y, size, size); } } And here it is: if (demosSupported) { addButtons(document.querySelector('.v4-buttons'), secondButtonSet); } else { document.querySelector('.v4-buttons').textContent = `Your browser does not support this demo`; } Now changing the seed produces an entirely new pattern. Bringing back the fun Ok, I admit that the animated noise effect was cool, but it was out of our control. Some folks react badly to flashing images and randomly changing visuals, so it's definitely something we do want to have under our control. However, now we have --pixel-gradient-seed defined as a number, we can animate it to recreate the animated noise effect: @keyframes animate-pixel-gradient-seed { from { --pixel-gradient-seed: 0; } to { --pixel-gradient-seed: 4294967295; } } .animated-pixel-gradient { background-image: paint(pixel-gradient); animation: 60s linear infinite animate-pixel-gradient-seed; } /* Be nice to users who don't want that kind of animation: */ @media (prefers-reduced-motion: reduce) { .animated-pixel-gradient { animation: none; } } And here it is: if (demosSupported) { addButtons(document.querySelector('.v5-buttons'), [ ...secondButtonSet, ['Toggle noise animation', callbackForEl(el => el.classList.toggle('animate-noise'))] ]); } else { document.querySelector('.v5-buttons').textContent = `Your browser does not support this demo`; } Now we can choose to animate the noise when we want, but keep it stable at other times. But what about random placement? Some CSS paint effects work with random placement of objects rather than random pixels, such as confetti/firework effects. You can use similar principles there too. Instead of placing items randomly around the element, split your elements up into a grid: // We'll split the element up // into 300x300 cells: const gridSize = 300; const density = props.get('--confetti-density').value; const seed = props.get('--confetti-seed').value; // Create our initial random generator: const randomXs = randomGenerator(seed); for (let x = 0; x < bounds.width; x += gridSize) { // Fork it for each column: const randomYs = randomXs.fork(); for (let y = 0; y < bounds.height; y += gridSize) { // Fork it again for each cell: const randomItems = randomYs.fork(); for (let _ = 0; _ < density; _++) { const confettiX = randomItems.next() * gridSize + x; const confettiY = randomItems.next() * gridSize + y; // TODO: Draw confetti at // confettiX, confettiY. } } } This time we have 3 dimensions of randomness – rows, columns, and density. Another advantage of using cells is the density of confetti will be consistent no matter how big the element is. Like this: Density: Length variance: Weight variance: if (demosSupported) { addButtons(document.querySelector('.confetti-buttons'), [ ['Animate width', callbackForEl(el => el.classList.toggle('width'))], ['Animate height', callbackForEl(el => el.classList.toggle('height'))], ['Change text', callbackForEl(el => el.textContent = el.textContent.trim() === 'Hello' ? 'World' : 'Hello')], ['Increment seed', callbackForEl(el => { el.style.setProperty( '--confetti-seed', el.computedStyleMap().get('--confetti-seed').value + 1 ); })], ['Toggle animate seed', callbackForEl(el => el.classList.toggle('animate-seed'))], ]); const confetti = document.querySelector('.confetti'); const confettiForm = document.querySelector('.confetti-form'); confettiForm.style.display = ''; confettiForm.addEventListener('submit', (e) => e.preventDefault()); confettiForm.addEventListener('input', () => { const data = new FormData(confettiForm); confetti.style.setProperty('--confetti-density', data.get('density')); confetti.style.setProperty('--confetti-length-variance', data.get('length') + 'px'); confetti.style.setProperty('--confetti-weight-variance', data.get('weight') + 'px'); }); } else { document.querySelector('.confetti-buttons').textContent = `Your browser does not support this demo`; } And now the density can be changed/animated without creating a whole new pattern each time! See, that was fun, right? Right? RIGHT???? If you want to create your own stable-but-random effects, here's a gist for the randomGenerator function.

AVIF has landed

Back in ancient July I released a video that dug into how lossy and lossless image compression works and how to apply that knowledge to compress a set of different images for the web. Well, that's already out of date because AVIF has arrived. Brilliant. AVIF is a new image format derived from the keyframes of AV1 video. It's a royalty-free format, and it's already supported in Chrome 85 on desktop. Android support will be added soon, Firefox is working on an implementation, and although it took Safari 10 years to add WebP support, I don't think we'll see the same delay here, as Apple are a member of the group that created AV1. What I'm saying is, the time to care about AVIF is now. You don't need to wait for all browsers to support it – you can use content negotiation to determine browser support on the server, or use <picture> to provide a fallback on the client: .img-d-4 .st0{fill:#5F6464; white-space: pre;} .img-d-4 .st1{font-family:Inconsolata; font-weight: bold;} .img-d-4 .st2{font-size:25.7851px;} .img-d-4 .st3{fill:#C92C2C;} .img-d-4 .st4{fill:#309D47;} .img-d-4 .st5{fill:#1990B8;} .img-d-4 .st6{fill:none;} .img-d-4 .st7{font-family:'Just Another Hand';} .img-d-4 .st8{font-size:41px;} .img-d-4 .st9{fill:none;stroke:#ED1F24;stroke-width:3;stroke-miterlimit:10;} .img-d-4 .st10{fill:#ED1F24;}<picture> <source type="image/avif" srcset="snow.avif"> <img alt="Hut in the snow" src="snow.jpg"></picture> If this type is supported, use this …else this Also, Squoosh now supports AVIF, which is how I compressed the examples in this post. Let's take a look at how AVIF performs against the image formats we already know and love… '); background-size: 20px 20px; } .image-tabs-transformer { display: grid; align-items: stretch; justify-items: stretch; } .image-tabs-transformer > * { grid-area: 1/1; } .image-tabs-tabs { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; border-top: 7px solid #ffe454; } .image-tabs input[type=radio] { position: absolute; opacity: 0; pointer-events: none; } .image-tabs-label { padding: 0.7em 0.7em; text-align: center; cursor: pointer; line-height: 1.3; font-size: 0.9rem; } input[type=radio]:checked + .image-tabs-label { background: #ffe454; } input[type=radio]:focus-visible + .image-tabs-label { background: #b9b9b9; } input[type=radio]:focus-visible:checked + .image-tabs-label { background: #ffc254; } .image-tabs-tab { display: grid; } @keyframes loading-fade-in { from { opacity: 0; animation-timing-function: ease-in-out; } } .image-tabs-loading { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, .62); display: flex; align-items: center; justify-content: center; animation: 300ms loading-fade-in; } .image-tabs-loading loading-spinner { --color: #fff; } F1 photo OriginalJPEG (74.4 kB)WebP (43 kB)AVIF (18.2 kB) import { h, render } from '/c/preact.module-5a21588d.js';import Component from '/c/ImageTabs-3c72409a.js';render(h(Component, {"ratio":1.777,"maxWidth":960,"initial":3,"category":"f1","images":[["Original","/c/f1-near-lossless-75baee9a.avif"],["JPEG (74.4 kB)","/c/f1-good-b8b556ff.jpg"],["WebP (43 kB)","/c/f1-good-40a27ec4.webp"],["AVIF (18.2 kB)","/c/f1-good-a14c8cc5.avif"]]}), document.querySelector('.post-component-1')); I picked this image because it's a photo with a mixture of low frequency detail (the road) and high frequency detail (parts of the car livery). Also, there are some pretty sharp changes of colour between the red and blue. And I like F1. Roughly speaking, at an acceptable quality, the WebP is almost half the size of JPEG, and AVIF is under half the size of WebP. I find it incredible that AVIF can do a good job of the image in just 18 kB. Before I compare things further: What is 'acceptable quality'? For the majority of images on the web, my rules are: If a user looks at the image in the context of the page, and it strikes them as ugly due to compression, then that level of compression is not acceptable. But, one tiny notch above that boundary is fine. It's ok for the image to lose noticeable detail compared to the original, unless that detail is significant to the context of the image. Context is key here. Image compression should be judged at the size it'll be presented to the user, and in a similar surrounding. If you're presenting a picture as a piece of art to be examined, quality and detail preservation become more important, but that's an edge case. Most images I see on the web are a much higher quality than they need to be, which results in a slower experience for users. I'm generally impressed by The Guardian's use of images. Take this article. If I open the image at the top of the article and zoom in, I can see the distinctive WebP artefacts. The street has been smoothed. There's some ringing around the graffiti. But we shouldn't optimise the user experience for people who might zoom in looking for flaws. When I look at the image within the article, in the size and context it's presented, I just see someone cycling past a closed pub, which is the intent of the image. The compression used there produces a small resource size, which means I saw the image quickly. In this article, I'm optimising images as if they were appearing in an article, where their CSS width is around 50% of their pixel width, meaning they're optimised for high-density displays. Technique Well, 'technique' might be too strong a word. To compress the images I used Squoosh. I zoomed the image out to 50%, dragged the quality slider down until it looked bad, then moved it back a bit. If the codec had an 'effort' setting, I set it to maximum. I also used one or two advanced settings, and I'll point those out along the way. But these are just my reckons. I'm comparing the images using the human balls of eye I keep safely inside my skull, rather than any kind of algorithm that tries to guess how humans perceive images. And of course, there are biases with human perception. In fact, when I showed this article to Kornel Lesiński (who actually knows what he's talking about when it comes to image compression), he was unhappy with my F1 comparison above, because the DDSIM score of the JPEG is much lower than the others, meaning it's closer in quality to the original image, and… he's right. I struggled to compress the F1 image as JPEG. If I went any lower than 74 kB, the banding on the road became really obvious to me, and some of the grey parts of the road appeared slightly purple in a noticeable way, but Kornel was able to tweak the quantization tables in MozJPEG to get a better result: OriginalJPEG (74.4 kB)JPEG + Kornel powers (65.5 kB)WebP (43 kB)AVIF (18.2 kB) import { h, render } from '/c/preact.module-5a21588d.js';import Component from '/c/ImageTabs-3c72409a.js';render(h(Component, {"ratio":1.777,"maxWidth":960,"initial":2,"category":"f1","images":[["Original","/c/f1-near-lossless-75baee9a.avif"],["JPEG (74.4 kB)","/c/f1-good-b8b556ff.jpg"],["JPEG + Kornel powers (65.5 kB)","/c/f1-kornel-64b41798.jpg"],["WebP (43 kB)","/c/f1-good-40a27ec4.webp"],["AVIF (18.2 kB)","/c/f1-good-a14c8cc5.avif"]]}), document.querySelector('.post-component-2')); Although I'm happy to spend time manually compressing key images of a web site, I don't really have the skills to tweak a JPEG encoder in that way. So the results in this post are also a reflection of what the codec can do with my moderate talent and perseverance. I also realise that manually tuning codec settings per image doesn't scale. If you need to automate image compression, you can figure out the settings manually from a few representative images, then add a bit of extra quality for safety, and use those settings in an automated tool. I'm providing the full output for each image so you can make your own judgement, and you can try with your own images using Squoosh. *cough* Sorry about all that. Just trying to get ahead of the "what if"s, "how about"s, and "well actually"s. Back to the F1 image Let's take a closer look and see how the codecs work: OriginalJPEG (74.4 kB)JPEG + Kornel powers (65.5 kB)WebP (43 kB)AVIF (18.2 kB) import { h, render } from '/c/preact.module-5a21588d.js';import Component from '/c/ImageTabs-3c72409a.js';render(h(Component, {"ratio":1.777,"maxWidth":960,"initial":4,"transform":"scale(6.5) translate(-9.6%, 8.9%)","category":"f1","images":[["Original","/c/f1-near-lossless-75baee9a.avif"],["JPEG (74.4 kB)","/c/f1-good-b8b556ff.jpg"],["JPEG + Kornel powers (65.5 kB)","/c/f1-kornel-64b41798.jpg"],["WebP (43 kB)","/c/f1-good-40a27ec4.webp"],["AVIF (18.2 kB)","/c/f1-good-a14c8cc5.avif"]]}), document.querySelector('.post-component-3')); The fine detail of the road is lost in all of the compressed versions, which I'm ok with. However, you can see the difference in detail Kornel was talking about. Look at the red bodywork in the original, there are three distinct parts – the mirror, the wing connecting the bargeboard, and the top of the sidepod. In the AVIF, the smoothing removes the division between these parts, but they're still mostly there in the JPEG, especially the 74 kB version. In the JPEG version you can also see the individual 8x8 blocks of the DCT, but they aren't obvious when zoomed out. WebP avoids this blockiness using decoding filters, and by, well, just being better. AVIF does much better at preserving sharp lines, but introduces smoothing. These are all ways of reducing data in the image, but the artefacts in AVIF are much less ugly. If you're thinking "wait, what's he talking about? The AVIF is really blocky around the red/blue", well, chances are you're looking at it in Chrome 85. There's a bug in the decoder when it comes to upscaling the colour detail. This is mostly fixed in 86, although there are some edge cases where it still happens. If you want more details on how lossy codecs work, check out my talk starting at 4:44. At equal file sizes One way to make the differences between the codecs really obvious is to test them at roughly the same file size: OriginalJPEG (20.7 kB)WebP (22.1 kB)AVIF (18.2 kB) import { h, render } from '/c/preact.module-5a21588d.js';import Component from '/c/ImageTabs-3c72409a.js';render(h(Component, {"ratio":1.777,"maxWidth":960,"initial":1,"category":"f1","images":[["Original","/c/f1-near-lossless-75baee9a.avif"],["JPEG (20.7 kB)","/c/f1-match-ff75e344.jpg"],["WebP (22.1 kB)","/c/f1-match-7a06c7c4.webp"],["AVIF (18.2 kB)","/c/f1-good-a14c8cc5.avif"]]}), document.querySelector('.post-component-4')); I couldn't even get the JPEG and WebP down to 18 kB, even at lowest settings, so this isn't a totally fair test. The JPEG suffers from awful banding, which started to appear as soon as I went below 74 kB. The WebP is much better, but there's still noticeable blockiness compared to the AVIF. I guess that's what a decade or two of progress looks like. Conclusion Unless it's automated, offering up 3 versions of the same image is a bit of a pain, but the savings here are pretty significant, so it seems worth it, especially given the number of users that can already benefit from AVIF. Here's a full-page comparison of the results. Ok, next image: Flat illustration OriginalTraced SVG (12.5 kB)PNG 68 colour (16.3 kB)WebP 68 colour lossless (12.9 kB)AVIF 68 colour lossless (41.7 kB) import { h, render } from '/c/preact.module-5a21588d.js';import Component from '/c/ImageTabs-3c72409a.js';render(h(Component, {"ratio":1.333,"maxWidth":400,"initial":3,"category":"team","images":[["Original","/c/team-lossless-d3132e77.webp"],["Traced SVG (12.5 kB)","/c/team-traced-1935e563.svg"],["PNG 68 colour (16.3 kB)","/c/team-good-df0b10e6.png"],["WebP 68 colour lossless (12.9 kB)","/c/team-good-fe689431.webp"],["AVIF 68 colour lossless (41.7 kB)","/c/team-lossless-83459c1d.avif"]]}), document.querySelector('.post-component-5')); This is an illustration by Stephen Waller. I picked it because of the sharp edges and solid colours, so it's a good test of lossless compression. The image doesn't look like it has a lot of colours, but due to the antialiasing around the edges, it has thousands. I was able to reduce the colours to 68 before things started looking bad. This makes a huge difference for WebP lossless and PNG, which switch to 'paletted' mode when there are 256 colours or fewer, which compresses really well. In the same way AVIF is derived from the keyframes of AV1 video, WebP's lossy compression is based on the keyframes of VP8 video. However, lossless WebP is a different codec, written from scratch. It's often overlooked, but it outperforms PNG every time. I don't have the original vector version of this image, but I created a 'traced' SVG version using Adobe Illustrator to get a very rough feel for how SVG would perform. What's notable is how badly AVIF performs here. It does have a specific lossless mode, but it isn't very good. But wait… Why not lossy? I went straight for palette reduction and lossless compression with this image, because experience has taught me lossy compression always does a bad job on these kinds of images. Or so I thought… OriginalTraced SVG (12.5 kB)PNG 68 colour (16.3 kB)WebP 68 colour lossless (12.9 kB)AVIF (8.69 kB) import { h, render } from '/c/preact.module-5a21588d.js';import Component from '/c/ImageTabs-3c72409a.js';render(h(Component, {"ratio":1.333,"maxWidth":400,"initial":4,"category":"team","images":[["Original","/c/team-lossless-d3132e77.webp"],["Traced SVG (12.5 kB)","/c/team-traced-1935e563.svg"],["PNG 68 colour (16.3 kB)","/c/team-good-df0b10e6.png"],["WebP 68 colour lossless (12.9 kB)","/c/team-good-fe689431.webp"],["AVIF (8.69 kB)","/c/team-good-e05dee63.avif"]]}), document.querySelector('.post-component-6')); Turns out lossy AVIF can handle solid colour and sharp lines really well, and produces a file quite a bit smaller than the SVG. I disabled chroma subsampling in the AVIF to keep the colours sharp. A closer look OriginalTraced SVG (12.5 kB)PNG 68 colour (16.3 kB)WebP 68 colour lossless (12.9 kB)AVIF (8.69 kB) import { h, render } from '/c/preact.module-5a21588d.js';import Component from '/c/ImageTabs-3c72409a.js';render(h(Component, {"ratio":1.333,"maxWidth":1000,"transform":"translate(-1%, 59%) scale(2.5)","initial":4,"category":"team","images":[["Original","/c/team-lossless-d3132e77.webp"],["Traced SVG (12.5 kB)","/c/team-traced-1935e563.svg"],["PNG 68 colour (16.3 kB)","/c/team-good-df0b10e6.png"],["WebP 68 colour lossless (12.9 kB)","/c/team-good-fe689431.webp"],["AVIF (8.69 kB)","/c/team-good-e05dee63.avif"]]}), document.querySelector('.post-component-7')); I expected a lossy codec to destroy the edges, but it looks great! There's a very slight bit of blurring above the glasses of the guy on the left, and on the ear of the guy on the right. If anything, AVIF has introduced some sharpening – see the left-hand side of the glasses. That kind of sharpening is usually produced by palette reduction, but here it's just how AVIF works due to the directional transforms and filters. The PNG and WebP have sharp edges particularly around the green shirt due to the palette reduction, but it isn't really noticeable at normal size. Of course, the SVG looks super sharp due to vector scaling, but you can see where the tracing lost details around the hair and pocket of the guy on the right. At equal file sizes Let's push the other codecs down to the size of the AVIF: OriginalJPEG (8.7 kB)PNG 8 colour (8.21 kB)WebP lossy (8.65 kB)AVIF (8.69 kB) import { h, render } from '/c/preact.module-5a21588d.js';import Component from '/c/ImageTabs-3c72409a.js';render(h(Component, {"ratio":1.333,"maxWidth":400,"initial":1,"category":"team","images":[["Original","/c/team-lossless-d3132e77.webp"],["JPEG (8.7 kB)","/c/team-match-1b140c61.jpg"],["PNG 8 colour (8.21 kB)","/c/team-match-3ade1b5d.png"],["WebP lossy (8.65 kB)","/c/team-match-a2c3054d.webp"],["AVIF (8.69 kB)","/c/team-good-e05dee63.avif"]]}), document.querySelector('.post-component-8')); Things aren't as bad as they were with the F1 image, but the JPEG is very noisy and changes the colours significantly, the WebP is blurry, and the PNG shows that, well, you need more than 8 colours. Conclusion AVIF has kinda blown my mind here. It's made me reconsider the kinds of images lossy codecs are suited to. But all said and done, a proper SVG is probably the right choice here. But even if SVG couldn't be used, the difference between the PNG and AVIF is only a few kB. In this case it might not be worth the complexity of creating different versions of the image. Here's a full-page comparison of the results. Right, it's time for the next image… Heavy SVG Original SVG (82.3 kB)Optimised SVG (30.8 kB)PNG 256 colour (84.6 kB)WebP (50.6 kB)AVIF (13.3 kB) import { h, render } from '/c/preact.module-5a21588d.js';import Component from '/c/ImageTabs-3c72409a.js';render(h(Component, {"ratio":1.5,"maxWidth":500,"initial":4,"category":"car","images":[["Original SVG (82.3 kB)","/c/car-original-2b40b809.svg"],["Optimised SVG (30.8 kB)","/c/car-good-fd3c16a0.svg"],["PNG 256 colour (84.6 kB)","/c/car-good-5d242821.png"],["WebP (50.6 kB)","/c/car-good-d9fa9011.webp"],["AVIF (13.3 kB)","/c/car-good-dfe22c28.avif"]]}), document.querySelector('.post-component-9')); I find it incredible that this image was created with SVG. However, it comes at a cost. The number of shapes and filters involved means it takes a lot of CPU for the browser to render it. It's one of those edge cases where it's better to avoid the original SVG, even if the alternative is larger. PNG struggles here due to the smooth gradients. I reduced the colours to 256, but I had to dither them to avoid visible banding, which also hurt compression. WebP performs significantly better by mixing lossy compression with an alpha channel. However, the alpha channel is always encoded losslessly in WebP (except for a bit of palette reduction), so it suffers in a similar way to PNG when it comes to the transparent gradient beneath the car. AVIF aces it again at a significantly smaller size, even compared to the SVG. Part of AVIF's advantage here is it supports a lossy alpha channel. A closer look Original SVG (82.3 kB)Optimised SVG (30.8 kB)PNG 256 colour (84.6 kB)WebP (50.6 kB)AVIF (13.3 kB) import { h, render } from '/c/preact.module-5a21588d.js';import Component from '/c/ImageTabs-3c72409a.js';render(h(Component, {"ratio":1.5,"maxWidth":1000,"transform":"scale(2.3) translate(2%, -9%)","initial":4,"category":"car","images":[["Original SVG (82.3 kB)","/c/car-original-2b40b809.svg"],["Optimised SVG (30.8 kB)","/c/car-good-fd3c16a0.svg"],["PNG 256 colour (84.6 kB)","/c/car-good-5d242821.png"],["WebP (50.6 kB)","/c/car-good-d9fa9011.webp"],["AVIF (13.3 kB)","/c/car-good-dfe22c28.avif"]]}), document.querySelector('.post-component-10')); When zoomed into the PNG, you can see the effects of the palette reduction. The WebP is getting blurry, and suffers from some colour noise. The AVIF looks similar to the WebP, but at a much smaller size. Interestingly, the AVIF just kinda gives up drawing the bonnet, but it's hardly noticeable when it's zoomed out. At equal file sizes As always, let's push the other formats down to the size of the AVIF: PNG 12 colour (14.1 kB)WebP (13.7 kB)AVIF (13.3 kB) import { h, render } from '/c/preact.module-5a21588d.js';import Component from '/c/ImageTabs-3c72409a.js';render(h(Component, {"ratio":1.5,"maxWidth":500,"initial":1,"category":"car","images":[["PNG 12 colour (14.1 kB)","/c/car-match-00064d96.png"],["WebP (13.7 kB)","/c/car-match-a8b7056d.webp"],["AVIF (13.3 kB)","/c/car-good-dfe22c28.avif"]]}), document.querySelector('.post-component-11')); The PNG version looks kinda cool! Whereas the WebP version makes me want to clean my glasses. Conclusion Yeahhhh going from 86/50 kB down to 13 kB is a huge saving, and worth the extra effort. Here's a full-page comparison of the results. Ok, one more: Illustration with gradients OriginalJPEG (81.7 kB)WebP 256 colour lossless (170 kB)WebP lossy (26.8 kB)AVIF (12.1 kB) import { h, render } from '/c/preact.module-5a21588d.js';import Component from '/c/ImageTabs-3c72409a.js';render(h(Component, {"ratio":1.786,"maxWidth":960,"initial":4,"category":"machine","images":[["Original","/c/machine-lossless-7afce17d.webp"],["JPEG (81.7 kB)","/c/machine-good-d440d82e.jpg"],["WebP 256 colour lossless (170 kB)","/c/machine-dithered-3e64641f.webp"],["WebP lossy (26.8 kB)","/c/machine-good-0df66b48.webp"],["AVIF (12.1 kB)","/c/machine-good-312c3829.avif"]]}), document.querySelector('.post-component-12')); This is another one from Stephen Waller. I picked this because it has a lot of flat colour and sharp lines, which usually points to lossless compression, but it also has a lot of gradients, which lossless formats can struggle with. Even if I take the colours down to 256 and let WebP work its lossless magic, the result is still 170 kB. In this case, the lossy codecs perform much better. I disabled chroma subsampling for the JPEG and AVIF, to keep the colours sharp. Unfortunately lossy WebP doesn't have this option, but it has "Sharp YUV", which tries to reduce the impact of the colour resolution reduction. JPEG doesn't do a great job here – anything lower than 80 kB starts to introduce obvious blockiness. WebP handles the image much better, but again I'm staggered by how well AVIF performs. A closer look OriginalJPEG (81.7 kB)WebP 256 colour lossless (170 kB)WebP lossy (26.8 kB)AVIF (12.1 kB) import { h, render } from '/c/preact.module-5a21588d.js';import Component from '/c/ImageTabs-3c72409a.js';render(h(Component, {"ratio":1.786,"maxWidth":960,"transform":"scale(6) translate(4.3%, 27.3%)","initial":4,"category":"machine","images":[["Original","/c/machine-lossless-7afce17d.webp"],["JPEG (81.7 kB)","/c/machine-good-d440d82e.jpg"],["WebP 256 colour lossless (170 kB)","/c/machine-dithered-3e64641f.webp"],["WebP lossy (26.8 kB)","/c/machine-good-0df66b48.webp"],["AVIF (12.1 kB)","/c/machine-good-312c3829.avif"]]}), document.querySelector('.post-component-13')); The JPEG is pretty noisy when zoomed in, and you can start to see the 8x8 blocks in the background. With the reduced-palette WebP, you can start to see the effects of palette reduction, especially in the elf's hat. The lossy WebP is pretty blurry, and suffers from colour artefacts, which are a side-effect of "Sharp YUV". The AVIF has really clean colours, but some blurring, and even changes some of the shapes a bit – the circle looks almost octagonal due to the edge detection. But c'mon, 12 Kb! At equal file sizes For one last time, let's push the other codecs down to AVIF's size: OriginalJPEG (12.3 kB)WebP (12.6 kB)AVIF (12.1 kB) import { h, render } from '/c/preact.module-5a21588d.js';import Component from '/c/ImageTabs-3c72409a.js';render(h(Component, {"ratio":1.786,"maxWidth":960,"initial":1,"category":"machine","images":[["Original","/c/machine-lossless-7afce17d.webp"],["JPEG (12.3 kB)","/c/machine-match-9fd6a83f.jpg"],["WebP (12.6 kB)","/c/machine-match-4b701a82.webp"],["AVIF (12.1 kB)","/c/machine-good-312c3829.avif"]]}), document.querySelector('.post-component-14')); At these sizes, JPEG has done its own art, and the WebP looks blocky and messy. Conclusion In this case, WebP offers a huge drop in size compared to the JPEG, so it's definitely worth providing the WebP to browsers that support it. However, the difference between the WebP and AVIF isn't huge, so it might not be worth creating an AVIF too. Here's a full-page comparison of the results. So, is AVIF the champion? I was initially sceptical of AVIF – I don't like the idea that the web has to pick up the scraps left by video formats. But wow, I'm seriously impressed with the results above. That said, it isn't perfect. Progressive rendering Because AVIF is an off-cut of a video format, it's missing some useful image features and optimisations that aren't relevant to video: JPEG, WebP, and AVIF loading at 2g speeds The above shows a high-resolution (2000x1178), high-quality image loading at 2g speeds. To get roughly the same quality, the JPEG is 249 kB, the WebP is 153 kB, and the AVIF is 96 kB. Although they're all loading at the same rate, the much-larger JPEG feels faster because of how it renders in multiple passes. WebP renders from top to bottom, which isn't as good, but at least you see the progress. Unfortunately, with AVIF it's all-or-nothing. Video doesn't need to render a partial frame, so it isn't something the format is set up to do. It's possible to have a top-to-bottom render like WebP, but the implementation would be complicated, so we're unlikely to see it in browsers in the foreseeable future. Because of this, AVIF feels better suited to smaller quicker-loading images. But that still covers most images on the web. Maybe this could be solved if the format could provide a way to embed a 'preview' version of the image at the start of the file. The browser would render this if it doesn't have the rest of the file. Because it's a different image, the developer would get to choose the quality, resolution, and even apply filters like blurring: Full AVIF (98 kB)Half resolution, low quality (4.57 kB)Half resolution, blurred (2.79 kB) import { h, render } from '/c/preact.module-5a21588d.js';import Component from '/c/ImageTabs-3c72409a.js';render(h(Component, {"ratio":1.786,"maxWidth":960,"initial":1,"images":[["Full AVIF (98 kB)","/c/cat-70cac942.avif"],["Half resolution, low quality (4.57 kB)","/c/cat-preview-4c3194f0.avif"],["Half resolution, blurred (2.79 kB)","/c/cat-blur-fc31e647.avif"]]}), document.querySelector('.post-component-15')); Adding 5 kB to a big image like this seems worth it to get a low-quality early render. Here's what it would look like: JPEG, WebP, and 'progressive' AVIF loading at 2g speeds I've proposed this to the AVIF spec folks. Encoding time Encoding AVIF takes a long time in general, but it's especially bad in Squoosh because we're using WebAssembly, which doesn't let us use SIMD or multiple threads. Those features are starting to arrive to standards and browsers, so hopefully we'll be able to improve things soon. At an 'effort' of 2, it takes a good few seconds to encode. 'Effort' 3 is significantly better, but that can take a couple of minutes. 'Effort' 10 (which I used for images in this article) can take over 10 minutes to encode a single image. AVIF supports tiling images, which chops the image into smaller blocks that can be encoded and decoded separately. This is interesting for encoding, because it means the blocks can be encoded in parallel, making full use of CPU cores, although Squoosh doesn't take advantage of this yet. The command line tools are orders of magnitude faster. You can either compile libavif yourself, or on OSX, install it via Homebrew: brew install joedrago/repo/avifenc There's also a Rust implementation, cavif. My current workflow is to use Squoosh to figure out decent settings at 'effort' 2, then use libavif to try the same settings at 'effort' 10. Hopefully we can speed up the Squoosh version soon! Decoding time There's also a question of CPU usage vs other formats when it comes to decoding, but I haven't dug into that yet. Although AV1 is starting to get hardware support, I'm told that dedicated hardware will be tuned for video, and not so great at decoding a page full of images. What about JPEG-XL and WebPv2? One of the reasons we built Squoosh is so developers could bypass the claims made about particular codecs, and instead just try it for themselves. JPEG-XL isn't quite ready yet, but we'll get it into Squoosh as soon as possible. In the meantime, I'm trying to take JPEG-XL's claims of superiority with a pinch of salt. However, there's a lot to get excited about. JPEG-XL is an image format, rather than an off-cut of a video format. It supports lossless and lossy compressions, and progressive multi-pass rendering. It looks like the lossless compression will be better than WebP's, which is great. However, the lossy compression is tuned for high quality rather than 'acceptable quality', so it might not be a great fit for most web images. But, the benefit of multi-pass rendering might mean it's worth taking a hit when it comes to file size. I guess we'll wait and see! There aren't many details around WebPv2 yet, so again it's best to wait and see until we can test it with our own images. And that's it! Phew! I didn't expect this post to get so long. I wanted to include a dive into the more obscure settings these codecs offer, but I'll save that for another day. I really enjoyed building the demos for this article. In case you want to dig into the details: I built a Preact component to handle image loading and decoding, so AVIF/WebP works even without browser support. A worker handles the actual decoding, using the WebAssembly decoders from Squoosh. I'd usually use comlink to help with worker communication, but lack of worker-module compatibility meant I went for something smaller/hackier instead. I wanted the demos on this page to be part of the static build to avoid layout shifting, but I didn't want to re-render the whole page with JS (a pattern you see a lot with things like Gatsby and Next.JS). I hacked together a solution where my markdown contains <​script type="component">, which is replaced with the HTML for that component when the markdown is parsed, and becomes live on the client. The full page compare view uses the two-up and pinch-zoom web components from Squoosh. Here's the progressive image loading demo. It uses a TransformStream in a service worker to throttle the image data. For the talk rather than this article, I built a tool that lets you experiment with chroma subsampling. Also from the talk, I built a tool to visualise the DCT patterns that form an 8x8 block. Thanks to Kornel Lesiński, Surma, Paul Kinlan, Ingvar Stepanyan, and Sam Jenkins for proof-reading and fact checking! And since publishing, thanks to Hubert Sablonnière and Mathias Bynens for more typo-busting.

A padlock problem

There's a difference between what the browser 🔒 means to users, vs what it means to browsers. To users, it means "the page is secure", but to the browser: The certificate dialog in Chrome …it means the "connection" is secure. This is because the security check happens as part of setting up the connection, before any HTTP communication happens. Then, many HTTP responses can be received through that connection. What's the problem? Things get tricky when the browser displays content without needing a connection at all. This can happen when content comes from the HTTP cache, or a service worker. Soooo what do browsers do? Firefox and Chrome cache the service worker's certificate. When you visit a page served by a service worker, they show a 🔒, and if you click on it, you get details about that cached certificate. Safari on the other hand doesn't display a 🔒. It doesn't say the page is "not secure", but it doesn't explicitly say it's secure either. However, Safari is moving to the same model as Firefox and Chrome. When it comes to the HTTP cache, all browsers behave the same. Responses are cached along with their certificate, and the 🔒 is displayed using information from that cached certificate. Is that a problem? Neither model is perfect. It seems weird to say the "connection is secure" when there's no connection. Also, you might end up seeing certificate information that has since expired. You might say that cached content should expire when the 'associated' certificate expires, but it isn't that simple. What if some JavaScript writes information to IndexedDB, then the certificate 'associated' with the JavaScript expires, or is even revoked? Should IndexedDB also be cleared? No, we already have a per-resource expiration mechanism for the HTTP cache via Cache-Control, and we already have clear-site-data to use as a panic button. The certificate is only used to verify data as it crosses from the internet to the user's local machine. The current Safari model doesn't make a claim about the connection, since it doesn't exist, which makes sense. But the absence of the 🔒 might be a red flag to users that are trained to spot it as a signal of safety. I'm not sure what the right answer is. Maybe we should show the 🔒, but when it's clicked we could say something along the lines of "this content was served securely without a connection". It's a tricky problem.

Different versions of your site can be running at the same time

It's pretty easy for a user to be running an old version of your site. Not only that, but a user could be running many different versions of your site at the same time, in different tabs, and that's kinda terrifying. For instance: A user opens your site. You deploy an update. The user opens one of your links in a new tab. You deploy an update. Now there are three versions of your site in-play. Two on the user's machine, and a third on your server. This can cause all sorts of exciting breakages that we don't often consider. It's unlikely though, right? Eh, it depends. There's a number of things that can increase the chance of this happening: You deploy often. We're taught that's a good thing, right? Deploy early and often. But if you're updating your site multiple times a day, that increases the chance that the version on the server won't match the version in the user's tab. It also increases the chance that the user may have multiple versions running in different tabs. Your navigations are client-side. A full page refresh is a great opportunity to pick up the latest stuff from the server. However, if your navigations are managed by JS, it keeps the user on the current version of the site for longer, making it more likely that they're out of sync with the server. The user is likely to have multiple tabs open to your site. For instance, I have 3 tabs pointing to various parts of the HTML spec, 11 to GitHub, 2 to Twitter, 2 to Google search, and 9 to Google Docs. Some of those I opened independently, some of them I created by opening links 'in a new tab'. Some of these have been open for longer than others, increasing the chance that they're running different deploys of those sites. Your site uses offline-first patterns via a service worker. Since you're loading content from a cache, which may have been populated days ago, it's likely to be out of sync with the server. However, since only one service worker can be active within a registration, it decreases the chance of the user running multiple versions of your site in different tabs. Combine a few of those, and suddenly it becomes likely that the user will be out of sync with the server, or they end up with multiple tabs running different versions. What can go wrong? Stuff changed What if the user loads one of your pages, you deploy an update, then the user clicks a button: btn.addEventListener('click', async () => { const { updatePage } = await import('./lazy-script.js'); updatePage(); }); The JavaScript running on your page is V1, but ./lazy-script.js is now V2. This can land you with all kinds of potential mis-matches: The content added by updatePage relies on CSS from V2, so the result looks broken/weird. updatePage tries to update the element with class name main-content, but in V1 that was named main-page-content, so the script throws. updatePage has been renamed updateMainComponent, so updatePage() throws an error. This is a problem with anything lazy-loaded that's co-dependent with other things on the page, including CSS and JSON. Solutions You could use a service worker to cache the current version. The service worker lifecycle won't let a new version take over until everything using the current version goes away. However, that means caching lazy-script.js up-front, which maybe defeats the point in terms of bandwidth saving. Alternatively, you could follow caching best-practices and revision your files so their content is immutable. Instead of ./lazy-script.js, it would be ./lazy-script.a837cb1e.js. But… Stuff went missing What if the user loads one of your pages, you deploy an update, then the user clicks a button: btn.addEventListener('click', async () => { const { updatePage } = await import('./lazy-script.a837cb1e.js'); updatePage(); }); In the latest deployment the lazy script was updated, so its URL has changed to lazy-script.39bfa2c2.js or whatever. In ye olde days, we'd push immutable assets to some kind of static host such as Amazon S3 as part of the deploy script. This meant both lazy-script.a837cb1e.js and lazy-script.39bfa2c2.js would exist on the server, and everything would work fine. However, with newer containerised/serverless build systems, the old static assets are likely gone, so the above script will fail with a 404. This isn't just a problem with lazy-loaded scripts, it's a problem with anything lazy-loaded, even something as simple as: <img src="article.7d62b23c.jpg" loading="lazy" alt="…" /> Solutions Going back to separate static hosts seems like a step backwards, especially as serving across multiple HTTP connections is bad for performance in an HTTP/2 world. I'd like to see teams like Netlify and Firebase offer a solution here. Perhaps they could provide an option to keep pattern-matched resources around for some amount of time after they're missing from the latest build. Of course, you'd still need a purge option to get rid of scripts that contain security bugs. Alternatively, you could handle import errors: btn.addEventListener('click', async () => { try { const { updatePage } = await import('./lazy-script.a837cb1e.js'); updatePage(); } catch (err) { // Handle the error somehow } }); But you aren't given the reason for failure here. A connection failure due to user being offline, syntax error in the script, and 404 during the fetch are indistinguishable in the code above. However, a custom script loader built on top of fetch() would be able to make the distinction. The try/catch needs to be in the V1 code, meaning V1 needs to be prepared for errors introduced by V2, and I don't know about you, but I don't always have total foresight for that kind of stuff. Again, you could use a service worker here, as it gives V2 some control over what to do with V1 pages, even if it's just forcibly reloading them: // In the V2 service worker addEventListener('activate', async (event) => { for (const client of await clients.matchAll()) { // Reload the page client.navigate(client.url); } }); This is pretty disruptive to the user, but it gives you a way out if V1 is totally unprepared for V2, and you just need it gone. Storage schemas change What if the user loads one of your pages, you deploy an update, then the user loads another one of your pages in another tab. Now the user has two versions of your site running. Both have: function saveUserSettings() { localStorage.userSettings = getCurrentUserSettings(); } But what if V2 introduces some new user settings? What if it changed the name of some of the settings? We often remember to migrate stored data from V1 to the V2 format, but what if the user continues to interact with V1? A number of fun things could happen when V1 tries to save user settings: It could save things using old names, meaning the settings would work in V1, but be out of sync with V2 – the settings have forked. It could discard anything it doesn't recognise, meaning it deletes settings created by V2. It could put storage into a state that V2 can no longer understand (especially if V2 believes it has already migrated data), creating breakages in V2. Solutions You could give your deployments version numbers, and make them show errors if storage was altered by a later version than the current tab version. function saveUserSettings() { if (Number(localStorage.storageVersion) > app.version) { // WHOA THERE! // Display some informative message to the user, then… return; } localStorage.userSettings = getCurrentUserSettings(); } IndexedDB kinda sends you down this path by design. It has versioning built in, and it won't let V2 connect to the database until all V1 connections have closed. However, V2 can't force the V1 connections to close, meaning V2 is left in a blocked state unless you fully prepared V1 for the arrival of V2. If you're using a service worker and serving all content from a cache, the service worker lifecycle will prevent two tabs running different versions unless you say otherwise. And again, V2 can forcibly reload V1 pages if V1 was totally unprepared for V2's arrival. API response changes What if the user loads one of your pages, you deploy an update, then the user clicks a button: btn.addEventListener('click', async () => { const data = await fetch('/user-details').then((r) => r.json()); updateUserDetailsComponent(data); }); However, the response format returned by /user-details has changed since the deployment, so updateUserDetailsComponent(data) throws or behaves in usual ways. Solutions Out of all the scenarios in this article, this is the one that's usually handled pretty well in the wild. The easiest thing to do is version your app, and send that along with the request: btn.addEventListener('click', async () => { const data = await fetch('/user-details', { headers: { 'x-app-version': '1.2.3' }, }).then((r) => r.json()); updateUserDetailsComponent(data); }); Now your server can either return an error, or return the data format required by that version of the client. Server analytics can monitor the usage of old versions, so you know when it's safe to remove code for handling old versions. Are you prepared for multiple versions of your site running at once? I'm not trying to finger-wag – a lot of the things I work on use serverless builds, so they're vulnerable to at least some of the breakages I've outlined, and some of the solutions I've presented here are a bit a weak, but they're the best I've got. I wish we had better tools to deal with this. Do you have better ways of dealing with these situations? Let me know in the comments:

Event listeners and garbage collection

Imagine a bit of code like this: async function showImageSize(url) { const blob = await fetch(url).then((r) => r.blob()); const img = await createImageBitmap(blob); updateUISomehow(img.width, img.height); } btn1.onclick = () => showImageSize(url1); btn2.onclick = () => showImageSize(url2); This has a race condition. If the user clicks btn1, then btn2, it's possible that the result for url2 will arrive before url1. This isn't the order the user clicked the buttons, so the user is left looking at incorrect data. Sometimes the best way to solve this is to queue the two actions, but in this case it's better to 'abort' the previous showImageSize operation, because the new operation supersedes it. fetch supports aborting requests, but unfortunately createImageBitmap doesn't. However, you can at least exit early and ignore the result. I wrote a little helper for this: async function abortable(signal, promise) { if (signal.aborted) throw new DOMException('AbortError', 'AbortError'); return Promise.race([ promise, new Promise((_, reject) => { signal.addEventListener('abort', () => { reject(new DOMException('AbortError', 'AbortError')); }); }), ]); } And here's how you'd use it: let controller; async function showImageSize(url) { // Abort any previous instance of this if (controller) controller.abort(); try { const { signal } = (controller = new AbortController()); const blob = await fetch(url, { signal }).then((r) => r.blob()); const img = await abortable(signal, createImageBitmap(blob)); updateUISomehow(img.width, img.height); } catch (err) { if (err.name === 'AbortError') return; throw err; } } btn1.onclick = () => showImageSize(url1); btn2.onclick = () => showImageSize(url2); Problem solved! I tweeted about it and got this reply: Isn't this missing { once: true } to not leak the listener? — Felix Becker (@felixfbecker) And that's a good question! What's the problem? Let's make a more 'extreme' version: async function abortable(signal, promise) { if (signal.aborted) throw new DOMException('AbortError', 'AbortError'); // Allocate 100mb of memory const lotsOfMemory = new Uint8Array(1000 * 1000 * 100); return Promise.race([ promise, new Promise((_, reject) => { signal.addEventListener('abort', () => { // Log it part of it console.log('async task aborted', lotsOfMemory[0]); reject(new DOMException('AbortError', 'AbortError')); }); }), ]); } In this version, I allocate 100mb of memory in a Uint8Array. That object is referenced in the 'abort' listener, so it needs to stay in memory. But for how long? 'abort' may never fire. But also, 'abort' may fire multiple times. If you call controller.abort() multiple times, the browser will only fire the 'abort' event once. But it's a regular DOM event, so there's nothing stopping anyone from doing something weird like this: signal.dispatchEvent(new Event('abort')); signal.dispatchEvent(new Event('abort')); signal.dispatchEvent(new Event('abort')); So, is each call of abortable leaking 100mb of memory? The original version of abortable didn't allocate 100mb of course, but it still adds an event listener to an object. Is that leaking? Is it actually a problem? Let's test it by creating 10 async tasks that just wait around: const resolvers = []; async function asyncTask() { const controller = new AbortController(); await abortable( controller.signal, new Promise((resolve) => { resolvers.push(resolve); }), ); console.log('async task complete'); } for (let i = 0; i < 10; i++) asyncTask(); And let's poke that with Chrome's DevTools: And yes, our large objects are hanging around in memory. But that's understandable, because the async task hasn't completed. Let's complete them: while (resolvers[0]) { const resolve = resolvers.shift(); resolve(); } And see if there's a change: Yes! All of our objects have been garbage collected. So, the answer is: no, abortable doesn't leak. Here's the demo I used for the videos, so you can try it yourself. But, why? Here's a less-code example: async function abortable(signal, promise) { if (signal.aborted) throw new DOMException('AbortError', 'AbortError'); return Promise.race([ promise, new Promise((_, reject) => { signal.addEventListener('abort', () => { reject(new DOMException('AbortError', 'AbortError')); }); }), ]); } async function demo() { const controller = new AbortController(); const { signal } = controller; const img = await abortable(signal, someAsyncAPI()); } demo(); The event listener added to signal, and everything that listener can access, needs to stay in memory as long as the 'abort' event can fire. There are two ways it can fire: Something calls signal.dispatchEvent(new Event('abort')). The browser decides to dispatch the 'abort' event on signal, which only happens the first time controller.abort() is called. While we're waiting for someAsyncAPI() to resolve, there are live references to signal and controller within demo(). But, once someAsyncAPI() resolves, demo() pops off the stack. It no longer holds references to signal or controller. With those gone, the browser realises signal can no longer receive events, and that event listener will never be called, so it can be garbage collected along with anything it references. And that's it! Browsers are generally pretty smart when it comes to callbacks: fetch(url).then( () => console.log('It worketh!'), () => console.log('It didnth!'), ); In this case you have two callbacks, but only one is ever called. The browser knows it can GC both once the promise resolves. Same goes for this: function demo() { const xhr = new XMLHttpRequest(); xhr.addEventListener('load', () => console.log('It worketh!')); xhr.addEventListener('error', () => console.log('It didnth!')); xhr.open('GET', url); xhr.send(); } Once either 'load' or 'error' fires, the browser sets a flag on the xhr instance to say "I hereby shall not fire any more events on this object", and since you no longer have a reference to xhr, you can't fire events on it either, all the event listeners can be GCed. These are browser optimisations, rather than spec'd behaviours. If you're unsure if a particular thing will be correctly GCed, test it!

Service workers at TPAC

Last month we had a service worker meeting at the W3C TPAC conference in Fukuoka. For the first time in a few years, we focused on potential new features and behaviours. Here's a summary: Resurrection finally killed reg.unregister(); If you unregister a service worker registration, it's removed from the list of registrations, but it continues to control existing pages. This means it doesn't break any ongoing fetches etc. Once all those pages go away, the registration is garbage collected. However, we had a bit in the spec that said if a page called serviceWorker.register() with the same scope, the unregistered service worker registration would be brought back from the dead. I'm not sure why we did this. I think we were worried about pages 'thrashing' registrations. Anyway, it was a silly idea, so we removed it. // Old behaviour: const reg1 = await navigator.serviceWorker.getRegistration(); await reg1.unregister(); const reg2 = await navigator.serviceWorker.register('/sw.js', { scope: reg1.scope, }); console.log(reg1 === reg2); // true! Well, it might be false if reg1 wasn't controlling any pages. Ugh, yes, confusing. Anyway: // New behaviour: const reg1 = await navigator.serviceWorker.getRegistration(); await reg1.unregister(); const reg2 = await navigator.serviceWorker.register('/sw.js', { scope: reg1.scope, }); console.log(reg1 === reg2); // Always false Now, reg2 is guaranteed to be a new registration. Resurrection has been killed. We agreed on this in 2018, and it's being implemented in Chrome, and already implemented in Firefox and Safari. GitHub issue. Chrome ticket. Firefox ticket. WebKit ticket. self.serviceWorker Within a service worker, it's kinda hard to get a reference to your own ServiceWorker instance. self.registration gives you access to your registration, but which service worker represents the one you're currently executing? self.registration.active? Maybe. But maybe it's self.registration.waiting, or self.registration.installing, or none of those. Instead: console.log(self.serviceWorker); The above will give you a reference to your service worker no matter what state it's in. This small feature was agreed across browsers, spec'd and is being actively developed in Chrome. GitHub issue. Chrome ticket. Firefox ticket. WebKit ticket. Page lifecycle and service workers I'm a big fan of the page lifecycle API as it standardises various behaviours browsers have already done for years, especially on mobile. For example, tearing pages down to conserve memory & battery. Also, the session history can contain DOM Documents, more commonly known as the 'back-forward page cache', or 'bfcache'. This has existed in most browsers for years, but it's something pretty recent to Chrome. This means pages can be: Frozen - The page can be accessed via a visible tab (either as a top level page, or an iframe within it), which isn't currently selected. The event loop is paused, so the page isn't using CPU. The page is fully in memory, and can be unfrozen without losing any state. If the user focuses this tab, the page will be unfrozen. Bfcached - Like frozen, but this page can't be accessed via a tab. It exists as a history item within a browsing context. If a there's session navigation to this item (eg using back/forward), the page will be unfrozen. Discarded - The page can be accessed via a visible tab, which isn't currently selected. However, the tab is only really a placeholder. The page has been fully unloaded and is no longer using memory. If the user focuses this tab, the page will be reloaded. We needed to figure out how these states fit with particular service worker behaviours: A new service worker will remain waiting until all pages controlled by the current active service worker are gone (which can be skipped using skipWaiting()). clients.matchAll() will return objects representing pages. We decided: Frozen pages will be returned by clients.matchAll() by default. Chrome would like to add an isFrozen property to client objects, but Apple folks objected. Calls to client.postMessage() against frozen clients will be buffered, as already happens with BroadcastChannel. Bfcached & discarded pages will not show up in clients.matchAll(). In future, we might provide an opt-in way to get discarded clients, so they can be focused (eg in response to a notification click). Frozen pages will count towards preventing a waiting worker from activating. Bfcached & discarded pages will not preventing a waiting worker from activating. If a controller for a bfcached page becomes redundant (because a newer service worker has activated), that bfcached page is dropped. The item remains in session history, but it will have to fully reload if it's navigated to. I even put all of my art skills to the test: I'm sure that clears everything up. Now we just have to spec it. GitHub issue about frozen documents & service workers. GitHub issue about bfcache & service workers. GitHub issue about discarded tabs. Attaching state to clients While we were discussing the page lifecycle stuff, Facebook folks mentioned how they use postMessage to ask clients about their state, eg "is the user currently typing a message?". We also noted that we've talked about adding more state to clients (size, density, standalone mode, fullscreen etc etc), but it's difficult to draw a line. Instead, we discussed allowing developers to attach clonable data to clients, which would show up on client objects in a service worker. // From a page (or other client): await clients.setClientData({ foo: 'bar' }); // In a service worker: const allClients = await clients.matchAll(); console.log(allClients[0].data); // { foo: 'bar' } or undefined. It's early days, but it felt like this would avoiding having to back-and-forth over postMessage. GitHub issue. Immediate worker unregistration As I mentioned earlier, if you unregister a service worker registration it's removed from the list of registrations, but it continues to control existing pages. This means it doesn't break any ongoing fetches etc. However, there are cases where you want the service worker to be gone immediately, regardless of breaking things. One customer here is Clear-Site-Data. It currently unregisters service workers as described above, but Clear-Site-Data is a "get rid of everything NOW" switch, so the current behaviour isn't quite right. Regular unregistration will stay the same, but I'm going to spec a way to immediately unregister a service worker, which may terminate running scripts and abort ongoing fetches. Clear-Site-Data will use this, but we may also expose it as an API: reg.unregister({ immediate: true }); Asa Kusuma from LinkedIn has written tests for Clear-Site-Data. I just need to do the spec work, which unfortunately is easier said than done. Making something abortable involves going through the whole spec and defining how aborting works at each point. GitHub issue. URL pattern matching Oooo this is a big one. We use URL matching all over the platform, particularly in service workers and Content-Security-Policy. However, the matching is really simple – exact matches or prefix matches. Whereas developers tend to use things like path-to-regexp. Ben Kelly proposed we bring something like-that-but-not-that to the platform. It would need to be a bit more restrictive than path-to-regexp, as we'd want to be able to handle these in shared processes (eg, the browser's network process). RegExp is really complicated and allows for various denial of service attacks. Browser vendors are happy with developers locking up their own sites on purpose, but we don't want to make it possible to lock up the whole browser. Here's Ben's proposal. It's pretty ambitious, but it'd be great if we could be more expressive with URLs across the platform. Examples like this are pretty compelling: // Service worker controls `/foo` and `/foo/*`, // but does not control `/foobar`. navigator.serviceWorker.register(scriptURL, { scope: new URLPattern({ baseUrl: self.location, path: '/foo/?*', }), }); Streams as request bodies For years now, you can stream responses: const response = await fetch('/whatever'); const reader = response.body.getReader(); while (true) { const { done, value } = await reader.read(); if (done) break; console.log(value); // Uint8Array of bytes } The spec also says you can use a stream as the body to a request, but no browser implemented it. However, Chrome has decided to pick it up again, and Firefox & Safari folks said they would too. let intervalId; const stream = new ReadableStream({ start(controller) { intervalId = setInterval(() => { controller.enqueue('Hello!'); }, 1000); }, cancel() { clearInterval(intervalId); }, }).pipeThrough(new TextEncoderStream()); fetch('/whatever', { method: 'POST', body: stream, headers: { 'Content-Type': 'text/plain; charset=UTF-8' }, }); The above sends "hello" to the server every second, as part of a single HTTP request. It's a silly example, but it demonstrates a new capability – sending data to the server before you have the whole body. Currently you can only do something like this in chunks, or using a websocket. A practical example would involve uploading something that was inherently streaming to begin with. For example, you could upload a video as it was encoding, or recording. HTTP is bidirectional. The model isn't request-then-response – you can start receiving the response while you're still sending the request body. However, at TPAC, browser folks noted that exposing this in fetch was really complicated given the current networking stacks, so the initial implementations of request-streams won't yield the response until the request is complete. This isn't too bad – if you want to emulate bidirectional communication, you can use one fetch for the upload, and another for the download. GitHub issue. Execute after response This has become a pretty common pattern in service workers: addEventListener('fetch', (event) => { event.respondWith( (async function () { const response = await getResponseSomehow(); event.waitUntil(async function () { await doSomeBookkeepingOrCaching(); }); return response; })(), ); }); However, some folks were finding that some of their JavaScript running in waitUntil was delaying return response, and were using setTimeout hacks to work around it. To avoid this hack, we agreed on event.handled, which is a promise that resolves once the fetch event has provided a response, or deferred to the browser. addEventListener('fetch', (event) => { event.respondWith( (async function () { const response = await getResponseSomehow(); event.waitUntil(async function () { // And here's the new bit: await event.handled; await doSomeBookkeepingOrCaching(); }); return response; })(), ); }); GitHub issue. The privacy of background sync & background fetch Firefox has an implementation of background sync, but it's blocked on privacy concerns, which are shared by Apple folks. Background sync gives you a service worker event when the user is 'online', which may be straight away, but may be sometime in the future, after the user has left the site. As the user has already visited the site as a top level page (as in, the origin is in the URL bar, unlike iframes), Chrome is happy allowing a small, conservative window of execution later on. Facebook have experimented with this to send analytics and ensure the delivery of chat messages, and found it performs better than things like sendBeacon. Mozilla and Apple folks are worried about cases where the 'later on' is much later on. It could be either side of a flight, where the change in IP is exposing more data than the user would want. Especially as, in Chrome, the user isn't notified about these pending actions. Mozilla and Apple folks are much happier with the background fetch model, which shows a persistent notification for the duration of the fetches, and allows the user to cancel. Google Search has used background sync to fetch content when online, but they could have used background fetch to achieve a similar thing. There wasn't really a conclusion to this discussion, but it feels like Apple may implement background fetch rather than background sync. Mozilla may do the same, or make background sync more user-visible. Content indexing Rayan Kanso presented the content indexing proposal, which allows a site to declare content that's available offline, so the browser/OS can present this information elsewhere, such as the new tab page in Chrome. There were some concerns that sites could use this to spam whatever UI these things appear in. But the browser would be free to ignore/verify any content it was told about. This proposal is pretty new. It was presented to the group as a heads-up. Launch event Raymes Khoury gave us an update on the launch event proposal. This is a way for PWAs to control multiple windows. For example, when a user clicks a link to your site, and doesn't explicitly suggest how the site should open (eg "open in new window"), it'd be nice if developers could decide whether to focus an existing window used by the site, or open a new window. This mirrors how native apps work today. Again this, was just a heads-up about work that was continuing. Declarative routing I presented developer feedback on my declarative routing proposal. Browsers are interested in it, but are more interested in general optimisations first. Fair enough! It's a big API & a lot of work. Seems best to hold off until we know we really need it. Top-level await in service workers Top-level await is now a thing in JavaScript! However, it's essential that service workers start up as fast as possible, so any use of top-level await in a service worker would likely be an anti-pattern. We had 3 options: Option 1: Allow top level await. Service worker initialisation would be blocked on the main script fully executing, including awaited things. Because it's likely an anti-pattern, we'd advocate against it using it, and perhaps show warnings in devtools. You can already do something like this in a service worker: const start = Date.now(); while (Date.now() - start < 5000); …and block initial execution for 5 seconds, so is await any different? Well maybe, because async stuff can have less predictable performance (such as networking), so the problem may not be obvious during development. Option 2: Ban it. The service worker would throw on top-level await, so it wouldn't install, and there'd be an error in the console. Option 3: Allow top level await, but the service worker is considered ready once initial execution + microtasks are done. That means the awaits will continue to run, but events may be called before the script has 'completed'. Adding events after execution + microtasks is not allowed, as currently defined. We decided that option 3 was too complicated, option 1 didn't really solve the problem, so we're going with option 2. So, in a service worker: // If ./bar or any of its static imports use a top-level await, // this will be treated as an error // and stops the service worker from installing. import foo from './bar'; // This top-level await causes an error // and stops the service worker from installing. await foo(); // This is fine. // Also, dynamically imported modules and their // static imports may use top-level await, // since they aren't blocking service worker start-up. const modulePromise = import('./utils'); GitHub issue. Fetch opt-in / opt-out Facebook folks have noticed service workers creating a performance regression on requests that go straight to the network. That makes sense. If a request passes through the service worker, and the result was to just do whatever the browser would have done anyway, the service worker is just overhead. Facebook folks have been asking for a way to, for particular URLs, say "this doesn't need to go via a service worker". Kinuko Yasuda presented a proposal, and we talked around it a bit and settled on a design a bit like this: addEventListener('fetch', event => { event.setSubresourceRoutes({ includes: paths, excludes: otherPaths, }); event.respondWith(…); }); If the response is for a client (a page or a worker), it will only consult the service worker for subresource requests which prefix-match paths in the includes list (which defaults to all paths), and doesn't consult the service worker for requests which prefix-match paths in the excludes list. I was initially unsure about this proposal, because the routing information lives with the page rather than the service worker, and I generally prefer the service worker to be in charge. However, other fetch behaviour already sits with the page, such as CSP, so I don't think it's a big deal. The API isn't totally elegant, so hopefully we can figure that out, but Facebook have offered to do the implementation work in Chromium, and we're happy for it to go to origin trial so they can see if it solves the real-world problems. If it looks like a benefit, we can look at tweaking the API. And that's it! There were other things we didn't have time to discuss, so we're probably going to have another in-person meeting mid-2020. In the mean time, if you have feedback on the above, let me know in the comments or on GitHub. Oh, also, the venue was next to a baseball stadium, and there was a pub that had windows into the stadium. That's the second most amazing thing about the pub. The most amazing thing? I asked for a gin & tonic, and I was faced with a clarifying question I'd never experienced before… "Pint?" Well… Cheers!

Probably?

Remy Sharp asked a question on Twitter that got me thinking about probability for the first time in a while. The problem Get your copybooks out now! Remy is using an image service that has an API which returns a URL for one of its images, picked at random. Remy makes five requests to the service, to get five image URLs. But, if the API returns a URL he already has, he replaces it with the result of an additional request. This may still return a duplicate, in which case he keeps it. Remy's question is: Does the 'additional request' trick improve the chances of getting a unique set of images?. Judging by the Twitter poll, a lot of folks were unsure about the answer. So, to avoid doing the work I'm supposed to be doing, let's investigate. Figuring it out, the lazy way Maths can give us an exact answer for questions like this, but maths is hard. Instead, let's made a computer do it for us, by writing a little simulation. Rather than picking a random URL, I'll simplify it to picking a random number: function isRandomPickUnique(totalPossibleChoices, amountToPick, allowSecondChance) { // A set to hold all of our picks. const set = new Set(); // Loop for the number of items to pick. for (let i = 0; i < amountToPick; i++) { // Pick a random integer between 0 and totalPossibleChoices: const num = Math.floor(Math.random() * totalPossibleChoices); // If the number isn't already in our set, // hurrah we've picked a unique number! if (!set.has(num)) { // Remember it, // and continue to the next iteration of the loop. set.add(num); continue; } // Otherwise, we've picked a duplicate. // If we're not allowed a second chance, we lose. if (!allowSecondChance) return false; // Otherwise, let's have another go: const secondNum = Math.floor(Math.random() * totalPossibleChoices); // If it's duplicate again, we lose. if (set.has(secondNum)) return false; // Otherwise, remember what we picked. set.add(secondNum); } // Everything we picked was unique! return true; } (This function suffers from the boolean trap, but it's just a quick test, gimmie a break). Now we can run the test a bunch of times (in this case, as many times as we can in half a second), and figure out the average number of true responses. const timeToRun = 500; const start = Date.now(); let uniqueSets = 0; let iterations = 0; while (Date.now() - start < timeToRun) { iterations++; if (isRandomPickUnique(100, 5, false)) uniqueSets++; } console.log('Chance of unique set:', uniqueSets / iterations); Job done! Total choices: Pick: Seconds to run: Results: … … Recalc And there we go, the "second chance" significantly improves the odds of a unique set of URLs. The longer you let the test run, the more accurate the answer is. However, by swapping computer thinking time for our own thinking time, we can get a fully accurate answer… Figuring it out, the maths way Logically, if something has a less-than-certain chance of happening, the chance of it happening twice in a row is always less than the chance of it happening once. Because of this, the "second chance" is always going to improve the odds of getting a unique set of URLs. But how can we figure out the exact probability? If all outcomes have an equal chance of happening (like the flip of a coin, or the roll of a dice), the probability is: winningOutcomes / possibleOutcomes …so the chance of rolling a 3 on a 6-sided dice is 1/6. The chance of rolling an even number is 3/6 (since there are three winning outcomes), which simplifies to 1/2. Calculating probability of one event and another To calculate the chance of two things happening, it's: firstProbability * secondProbability So the chance of rolling a 3 on a 6-sided dice (1/6), then flipping a coin to heads (1/2), is 1/6 * 1/2, which is 1/12. This makes sense, there are twelve possible outcomes, one for each number on the dice + tails, and again for each number on the dice + heads, and only one outcome is a win. Calculating the probability of getting five unique image URLs Ignoring the "second chance" rule for now, we now have everything we need to figure out the chance of picking five unique URLs randomly from a set. Let's say totalImages is the number of image URLs the API can pick from. With the first pick, any image is a win, so the number of winning outcomes is the same as the number of possible outcomes: totalImages / totalImages But the second pick is different, as there's one image URL we must avoid: (totalImages - 1) / totalImages For the third pick, there's now two to avoid: (totalImages - 2) / totalImages And so on. We multiply the probabilities together, and get: // First pick. totalImages / totalImages // Second pick. * (totalImages - 1) / totalImages // Third pick. * (totalImages - 2) / totalImages // Fourth pick. * (totalImages - 3) / totalImages // Fifth pick. * (totalImages - 4) / totalImages Or using JavaScript: let probability = 1; for (let i = 0; i < numberToPick; i++) { probability *= (totalImages - i) / totalImages; } console.log(probability); But how do we cater for "second chance"? Calculating probability of one event or another If you want to calculate the odds of either thing happening, it's: firstProbability + secondProbability However, there's a gotcha here. If the two events are dependant, that needs to be factored into the probability of the second event. If we calculate the chance of rolling a 3 on a 6-sided dice (1/6), or flipping a coin to heads (1/2), we wouldn't bother flipping the coin if we rolled a 3. If we roll a 3, the result of the coin has no impact on the result. We already won. The coin gives us our "second chance", but we only use it if our first chance failed. 1/6th of the time we'd win just using the dice, but 5/6th of the time we'd also use the coin, giving us a 1/2 second chance. firstProbability + (1 - firstProbability) * secondProbability Or in this case 1/6 + 5/6 * 1/2, which multiplies to 1/6 + 5/12, which adds to 7/12. This makes sense as there are twelve possible outcomes, one for each number on the dice + tails, one of which is a winning outcome, and again for each number on the dice + heads, all six of which are winning outcomes, making seven winning outcomes in total. Calculating the probability of getting five unique image URLs, including second chances We can now figure out the chance of picking five unique URLs randomly from a set, including the "second chance" rule. Again, with the first pick, any image is a win, so the number of winning outcomes is the same as the number of possible outcomes: totalImages / totalImages With the second pick, there's one image to avoid. But if we do pick it, we get to try again. // First try. (totalImages - 1) / totalImages // But add the probability of another try, if the first try fails: + 1 / totalImages * (totalImages - 1) / totalImages Here, 1 / totalImages is the chance we picked a duplicate, which we multiply by (totalImages - 1) / totalImages, the chance of avoiding a duplicate a second time. For the third pick, there's now two to avoid: // First try. (totalImages - 2) / totalImages // But add the probability of another try, if the first try fails: + 2 / totalImages * (totalImages - 2) / totalImages And so on. We multiply the probabilities together, and get: // First pick. totalImages / totalImages // Second pick. * ( (totalImages - 1) / totalImages + 1 / totalImages * (totalImages - 1) / totalImages ) // Third pick. * ( (totalImages - 2) / totalImages + 2 / totalImages * (totalImages - 2) / totalImages ) // Fourth pick. * ( (totalImages - 3) / totalImages + 3 / totalImages * (totalImages - 3) / totalImages ) // Fifth pick. * ( (totalImages - 4) / totalImages + 4 / totalImages * (totalImages - 4) / totalImages ) Or using JavaScript: let probability = 1; for (let i = 0; i < numberToPick; i++) { probability *= // First try. (totalImages - i) / totalImages // Possible second try. + i / totalImages * (totalImages - i) / totalImages; } console.log(probability); Job done! Total choices: Pick: Seconds to run: Sim results: … … Exact results: … … Recalc const formatter = new Intl.NumberFormat('en-gb', { style: 'percent', maximumFractionDigits: 3 }); const format = v => formatter.format(v); const forms = document.querySelectorAll('.probability-form'); const noSecondChanceResultEls = document.querySelectorAll('.no-second-chance-result'); const secondChanceResultEls = document.querySelectorAll('.second-chance-result'); const noSecondChanceResultRealEl = document.querySelector('.no-second-chance-result-real'); const secondChanceResultRealEl = document.querySelector('.second-chance-result-real'); let workers = []; let busy = false; const workerScript = ` function isRandomPickUnique(totalPossibleChoices, amountToPick, allowSecondChance) { // A set to hold all of our picks. const set = new Set(); // Loop for the number of items to pick. for (let i = 0; i { const { totalPossibleChoices, amountToPick, allowSecondChance, duration } = event.data; const timeToRun = duration * 1000; const start = Date.now(); let uniqueSets = 0; let iterations = 0; while (Date.now() - start obj.addEventListener('message', r, { once: true })); } function restartWorkers() { for (const worker of workers) worker.terminate(); workers = [new Worker(workerURL), new Worker(workerURL)]; } restartWorkers(); async function getBruteForceResults(totalPossibleChoices, amountToPick, duration) { if (busy) restartWorkers(); busy = true; const resultPromises = [false, true].map(async (allowSecondChance, i) => { const worker = workers[i]; worker.postMessage({ allowSecondChance, totalPossibleChoices, amountToPick, duration }); const event = await nextEvent(worker, 'message'); const { uniqueSets, iterations } = event.data; return uniqueSets / iterations; }); const results = await Promise.all(resultPromises); busy = false; return results; } function getResults(totalPossibleChoices, amountToPick) { let noSecondChanceResult = 1; for (let i = 0; i calculate(event.currentTarget)); form.addEventListener('submit', (event) => { event.preventDefault(); calculate(event.currentTarget); }); } And this gives us an exact result (almost) instantly. If I just wanted a rough one-off answer, like Remy did, I'd just write a simulation. I'd only go straight for the 'maths' solution if it was particularly simple, and I'd probably still write the simulation to verify my maths. Right, I guess I better get back to the work I was supposed to be doing…

Who has the fastest website in F1?

I was trying to make my predictions for the new Formula One season by studying the aerodynamics of the cars, their cornering speeds, their ability to run with different amounts of fuel. Then it hit me: I have no idea what I'm doing. So, I'm going to make my predictions the only way I know how: By comparing the performance of their websites. That'll work right? If anything, it'll be interesting to compare 10 sites that have been recently updated, perhaps even rebuilt, and see what the common issues are. I'll also cover the tools and techniques I use to test web performance. Methodology I'm going to put each site through WebPageTest to gather the data on Chrome Canary on a Moto G4 with a 3g connection. Although a lot of us have 4g-enabled data plans, we frequently drop to a slower connection. A consistent 3g connection will also suggest what the experience would be like in poor connectivity. Trying to use a site while on poor connectivity is massively frustrating, so anything sites can do to make it less of a problem is a huge win. In terms of the device, if you look outside the tech bubble, a lot of users can't or don't want to pay for a high-end phone. To get a feel for how a site performs for real users, you have to look at mid-to-lower-end Android devices, which is why I picked the Moto G4. I'm using Chrome Canary because WPT seems to be glitchy with regular Chrome right now. Yeah, not a great reason, but the test wasn't useful otherwise. Oh, and whenever I talk about resource sizes, I'm talking about downloaded bytes, which means gzip/brotli/whatever. Calculating race time The amount of bandwidth your site uses matters, especially for users paying for data by the megabyte. However I'm not going to directly score sites based on this. Instead, I'm going to rate them on how long it takes for them to become interactive. By "interactive", I mean meaningful content is displayed in a stable way, and the main thread is free enough to react to a tap/click. Caching is important, so I'm going to add the first load score to the second load score. There's some subjectivity there, so I'll try and justify things as I go along. Issues with the test I'm not comparing how 'good' the website is in terms of design, features etc etc. In fact, about:blank would win this contest. I love about:blank. It taught me everything I know. I'm only testing Chrome. Sorry. There's only one of me and I get tired. In fact, with 10 sites to get through, it's possible I'll miss something obvious, but I'll post the raw data so feel free to take a look. I only used "methodology" as a heading to try and sound smart. But really, this is fun, not science. Also, and perhaps most importantly, the results aren't a reflection of the abilities of the developers. We don't know how many were on each project, we don't know what their deadline was or any other constraints. My goal here is to show how I audit sites, and show the kind of gains that are available. Bonus round: images For each site I'll take a look at their images, and see if any savings can be made there. My aim is to make the image good-enough on mobile, so I'll resize to a maximum of 1000px wide, for display at no larger than 500px on the device's viewport (so it looks good on high density mobile devices). Mostly, this is an excuse to aggressively push Squoosh. Ok. It's lights out, and away we go… Mercedes Mercedes make cars. You might have seen one on a road. They're a big company, so their F1 team is a sub brand. They're also one of the few teams with the same drivers as 2018, so this might not be a 'new' site. Let's take a look: Website. WebPageTest results. First lap Ok, this is going to be a longer section than usual as I go through the tools and techniques in more detail. Stick with me. The following sections will be shorter because, well, the technique is mostly the same. I like WebPageTest as it runs on a real devices, and provides screenshots and a waterfall. From the results above, the median first run is 'Run 3', so let's take a look at the 'Filmstrip view': In terms of user experience, we've got 6.8s of nothing, then a spinner until 13.7s. Showing a spinner is definitely better than showing nothing, but only just. A full-page spinner is basically an admission of being too slow and apologising to the user. But can it be avoided in this case? WebPageTest's waterfall diagram allows us to match this up with network and main thread activity: You don't need to use WebPageTest to get this kind of overview. Chrome DevTools' "Performance" panel can give you the same data. Notice how the waterfall has 'steps'. As in, entry 23 is on another step from the things before, and step 50 is on yet another step. This suggests that something before those things prevented them from downloading earlier. Usually this means the browser didn't know in advance that it needed that resource. The green vertical line shows 'first render', but we know from the filmstrip that it's just a spinner. Let's dig in. The green line appears after the first 'step' of items in the waterfall. That suggests one or more of those resources was blocking the render. Viewing the source of the page shows many <script src> tags in the head. <head> … <script type="text/javascript" src="https://www.mercedesamgf1.com/en/wp-includes/js/jquery/jquery.js?ver=1.12.4" ></script> <script type="text/javascript" src="https://www.mercedesamgf1.com/en/wp-includes/js/jquery/jquery-migrate.min.js?ver=1.4.1" ></script> <script type="text/javascript" src="https://www.mercedesamgf1.com/wp-content/plugins/duracelltomi-google-tag-manager/js/gtm4wp-form-move-tracker.js?ver=1.8.1" ></script> <script type="text/javascript" src="https://www.mercedesamgf1.com/wp-content/plugins/duracelltomi-google-tag-manager/js/gtm4wp-social-tracker.js?ver=1.8.1" ></script> <script type="text/javascript" src="https://www.mercedesamgf1.com/wp-content/plugins/duracelltomi-google-tag-manager/js/analytics-talk-content-tracking.js?ver=1.8.1" ></script> <script type="text/javascript" src="https://www.mercedesamgf1.com/wp-content/plugins/events-calendar-pro/src/resources/js/widget-this-week.min.js?ver=4.0.6" ></script> … </head> These block rendering by default. Adding the defer attribute prevents them blocking rendering, but still lets them download early. Ire Aderinokun wrote an excellent article on this if you want to know more. Adding defer to those scripts will allow the page to render before those scripts have executed, but there might be some additional work to ensure it doesn't result in a broken render. Sites that have been render-blocked by JS often come to rely on it. Ideally pages should render top-to-bottom. Components can be enhanced later, but it shouldn't change the dimensions of the component. I love this enactment of reading the Hull Daily Mail website, especially how they capture the frustration of layout changes as well as popups. Don't be like that. For instance, interactive elements like the carousel could display the first item without JS, and JS could add the rest in, plus the interactivity. This is what I'd like to see. The before-JS render is missing the elements that require JS for interactivity. Once the JavaScript loads, it adds them in. Looking at the page source, the HTML contains a lot of content, so they're already doing some kind of server render, but the scripts prevent them from taking advantage of it. Over on WebPageTest you can click on items in the waterfall for more info. Although at this point I find Chrome Devtools' "Network" panel easier to use, especially for looking at the content of the resources. The render-blocking scripts weigh in around 150k, which includes two versions of jQuery. There's over 100k of CSS too. CSS also blocks rendering by default, but you want it to block for initial styles, else the user will see a flicker of the unstyled page before the CSS loads. Chrome Devtools' code coverage tool says over 80% of the CSS is unused for first render, along with 75% of the JS. These should be split up so the page is only loading what it needs. For JavaScript, modern build tools like webpack, rollup.js, and Parcel support code-splitting for JavaScript. Splitting CSS isn't as easy. Keeping CSS tightly coupled with their components makes it easier to identify CSS that isn't needed for a particular page. I really like CSS modules as a way to enforce this. There are tools that automate extracting 'above the fold' CSS. I've had mixed results with these, but they might work for you. The second 'step' of the waterfall contains some render-altering fonts. The browser doesn't know it needs fonts until it finds some text that needs them. That means the CSS downloads, the page is laid out, then the browser realises it needs some fonts. <link rel="preload" as="font" href="…"> in the <head> would be a quick-win here. This means the browser will download the fonts within the first step of the waterfall. For more info on preloading, check out Yoav Weiss' article. Also, the fonts weigh in around 350k, which is pretty heavy. 280k of this is in TTF. TTF is uncompressed, so it should at least be gzipped, or even better use woff2, which would knock around 180k off the size. font-display: optional could be considered here, but given it's part of corporate identity, sighhhhhhhh it might not get past the brand folks. There's another render-blocking script in this part of the waterfall. The download starts late because it's at the bottom of the HTML, so it should be moved to the <head> and given the defer attribute. Then, the Moto 4 gets locked up for 3 seconds. You can see this from the red bar at the bottom of the waterfall. WebPageTest shows little pink lines next to script when they're using main-thread time. If you scroll up to row 19, you can see it's responsible for a lot of this jank. Second lap Let's take a look at the second load: Aside from the HTML, none of the resources have Cache-Control headers. But browser heuristics step in, and as a result the cache is used for most assets anyway. This is kinda cheating, and unlikely to reflect reality, but hey, I can't change the rules now. For a refresher on caching, check out my article on best practices. The lack of caching headers seems like an oversight, because a lot of the assets seem to have version numbers. Most of the work has been done, it just lacks a little server configuration. Unfortunately, despite the heuristic caching, it still takes 5.8s to get into a useable state, and this is all down to JavaScript execution. The fixes I mentioned above would cover this too. Apply code splitting, and allow rendering before JS. Images They've done a pretty good job of image compression here. They appear to be using some JavaScript to load smaller versions of images for mobile devices. You don't need JavaScript for this. Using JS for this delays image loading. Using Squoosh I could get the first image down from 50k to 29k without significant loss. See the results and judge for yourself. Result '); background-color: rgba(0, 0, 0, 0.68); opacity: 0.6; } .final-results ul { display: block; padding: 0; margin: 0; position: relative; width: 857px; margin-left: 54px; padding-right: 230px; } .final-results li { display: flex; position: relative; width: -moz-max-content; width: max-content; margin: 13px 0; box-shadow: 0 6px 9px rgba(0, 0, 0, 0.41); } .final-results li::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 40%; background: rgba(255, 255, 255, 0.36); } .final-results .name { display: block; background: linear-gradient(to bottom, #c1c1c1, #5f5f5f); color: #fff; padding: 0 0.5rem; text-shadow: 0 1px 2px #000; font-weight: bold; } .final-results .time { display: block; background: linear-gradient(to bottom, #585858, #000); color: #fff; padding: 0 0.5rem; } .final-results.wait-for-anim li { opacity: 0.001; will-change: transform; } @keyframes result-slide-in { from { transform: translate(300px); } to { opacity: 1; } } .final-results.anim li { animation: result-slide-in 0.8s ease-out both; } Mercedes 19.5s { if (entry.isIntersecting) fig.classList.add('anim'); }, { threshold: 1 }).observe(fig); } animateFinalResults(); So, Mercedes storm into 1st place and last place at the same time, as often happens with the first result. good HTTPS good HTTP/2 good Gzip, except TTF good Minification good Image compression bad Render-blocking scripts bad Late-loading fonts bad No cache control bad Unnecessary CSS bad Unnecessary JS bad Badly compressed fonts My gut feeling is the quick-wins (preloading, compressing fonts) would half this time. I imagine the refactoring of the JS and CSS would be a much bigger task, but would bring big results. Ferrari Ferrari also make cars. You might have seen one on a road, driven by a wanker (except my friend Matt who bucks this trend. Hi Matt!). They have a new driver this year, and a slightly different livery, so this site may have been changed fairly recently. Website. WebPageTest results. First lap In terms of user experience there's 33s of nothing, some content at 34.3s, but things move around until 36s. This is around twice as long as Mercedes, so things aren't looking good. Let's dive in: Wow that's a lot of requests! But, Ferrari use HTTP/2, so it isn't such a big deal. I should mention, this article is a real workout for your scrolling finger. The stand-out issue is that huge row 16. It's a render-blocking script. It's also on another server, so it needs to set up a new HTTP connection, which takes time. You can see this in the waterfall by the thinner green/orange/purple line which signifies the various stages of setting up a connection. However, the biggest issue with that script, is it's 1.8mb. There's also an additional 150k script that isn't minified, and other scripts which sit on different servers. The CSS is 66k and 90% unused, so this could benefit from splitting. There are a few fonts that would benefit from <link rel="preload"> but they're pretty small. Let's face it, everything is small compared to the JS. Oddly, Chrome's coverage tool claims 90% of the JS is used on page load, which beggars belief. I dug into their JavaScript and saw a lot of locale data in there, which should be split out, but then I saw a large chunk of base64. You'll never guess what it is. It's this: No, not the whole thing. The logo. No, not the whole logo, that's SVG. But the horse, the horse is a base64 PNG within the SVG: Look at it. It's beautiful. It's 2300x2300. It's 1.7mb. 90% of their performance problem is a massive bloody horse. That logo appears across the main Ferrari site too, so it's probably something the creator of the F1 site had little control over. I wonder if they knew. Again, there seems to be server rendering going on, but it's rendered useless by the script. There also seems to be multiple versions of the same image downloading. Second lap The site has ok caching headers, so I'm surprised to see the browser revalidating some of those requests. The big problem here is still the JS, which takes up two seconds of main thread time. The main-thread then gets locked up until 10.1s. This isn't by JavaScript. I'm not 100% sure what it is, but I suspect it's image-decoding that horse. Images First things first, let's tackle that logo. SVG might be the best format for this, but with the bitmap-horse replaced with a much simpler vector-horse (Vector Horse's debut album is OUT NOW). However, I don't have a vector version of the horse, so I'll stick with a bitmap. The logo is only ever displayed really-tiny in the top-left, at a height of 20px. Using Squoosh, I'm going to make a 60px version so it stays sharp on high-density devices. That takes the image from 1.7mb, to a 2.6k PNG, or a 1.7k WebP. See the results. One of the initial images on the site is of the 2019 Ferrari. However, it's 1620px wide, and not really optimised for mobile. I took the image down from 134k to 23k as JPEG, or 16k as WebP without significant loss. But, you can be the judge. WebP shines here because JPEG struggles with smooth gradients – at lower sizes it creates banding which is really noticeable. The page also contains little track maps that aren't initially displayed. These should be lazy-loaded, but they could also do with a bit of compression. I was able to get one of them down from 145k to 27k as a PNG without noticeable loss. Again, you be the judge. Result Mercedes 19.5s Ferrari 46.1s Last place for now. Most of their problem is down to one horse. I don't think anyone did that deliberately, but RUM & build time metrics would have made it obvious. good HTTPS good HTTP/2 good Gzip good Caching bad Render-blocking scripts bad Unnecessary JS bad Unnecessary CSS bad Unminified scripts bad Unstable render bad Poor image compression bad Late-loading fonts bad Main thread lock-up bad Horse Red Bull Red Bull aren't a car company. They sell drinks of a flavour I can only describe as "stinky medicine". But, they're also a much more modern, tech-savvy company, so it'll be interesting to see if it shows here. Website. WebPageTest results. First lap The user experience is 4.9s of nothing, but the result is a broken UI. Things sort themselves out at around 6.5s. There's a font switch at 9.5s, and a horrendous cookie warning at 16s. But, I'd call this visually ready at 6.5s. Unfortunately we can't call this page ready at 6.5s, as the main thread is locked up until 11s. Still, this takes it into 1st place by a couple of seconds. The story here is very similar to the previous sites. The page contains what looks like a server render, including minified HTML, but render-blocking scripts prevent it being shown. The scripts should use defer. The CSS is 90% unused, and the JS is ~75% unused, so code-splitting and including only what's needed for this page would have a huge benefit. This might help with the main thread lock-ups too. Again, the fonts start loading way too late. <link rel="preload"> would be a quick win here. The icon font times-out, which causes a broken render. There isn't really a good reason to use icon fonts these days. Icon fonts should be replaced with SVG. The site does use <link rel="preload">, but it's mostly used for JS and CSS, which don't really need preloading as they're already in the <head>. Worse, they preload a different version of the scripts to the ones they use on the page, so they're doubling the download. Chrome's console shows a warning about this. Second lap Thanks to decent caching, we get a render really quickly. However, JS bogs down the main thread for many seconds afterwards, so the page isn't really interactive until the 4.8s mark. Ideally the JavaScript would be chunked up so it only hits the main thead during idle time, or just as the user needs it. Images Like Mercedes, Red Bull uses responsive images, but use JavaScript to make it work. Still, this is better than serving full size images to mobile. The images could be smaller though. Taking the first image on their page, Using Squoosh I can take it down from 91k to 36k as a JPEG, or 22k as WebP. See the results. They also use a spritesheet, which might not be necessary thanks to HTTP/2. By bringing the palette down to 256 colours, I can take it down from 54k to 23k as PNG, or 18k as WebP. See the results. Result Mercedes 19.5s Ferrari 46.1s Red Bull 15.8s Despite issues, Red Bull are straight into 1st place by almost 4 seconds. Nice work! good HTTPS good HTTP/2 good Gzip good Minification, including HTML good Image compression good Caching bad Render-blocking scripts bad Main thread lock-up bad Unnecessary CSS bad Unnecessary JS bad Late-loading fonts bad Unnecessary icon fonts Renault Back to folks that make cars. Car adverts tend to be awful, but I reckon Renault make the worst ones. Also, they may have spent too much money on one of their drivers and not have enough left over for their website. Let's see… Website. WebPageTest results. First lap The user gets 5.8s of nothing, then a broken render until 7.8s, but the intended content of the page doesn't arrive until 26.5s. Also, asking for notification permission on load should be a black flag situation, but I'll be kind and ignore it. As with previous sites, render-blocking scripts account for the first 5.8s of nothing. These should use defer. However, this script doesn't get the page into an interactive state, it delivers a broken render. Then, something interesting happens. The scripts that are needed to set up the page are at the bottom of the HTML, so the browser gives these important scripts a low priority. You can see from the waterfall that the browser starts the download kinda late, but not that late. The darker area of the bars indicates the resource is actively downloading, but in this case the scripts are left waiting until other things such as images download. To fix this, these important scripts should be in the <head> and use the defer attribute. The page should be fixed so the before-JS render is usable. The CSS is 85% unused, and the JS is ~55% unused, so it would benefit from splitting. As with the other pages the fonts load late. It's especially bad here as images steal all the bandwidth (more on that in a second). Preloading fonts is a huge & quick win, and icon fonts should be replaced with SVG. Second lap The caching is pretty good here, but a few uncached scripts push the complete render back to 5.9s. The first lap fixes would help here, along with some Cache-Control headers. Images Images play quite a big part in the performance here. I took their first carousel image and put it through Squoosh. This took the size from 314k down to 59k as a JPEG, or 31k as WebP. I don't think the compression is too noticeable, especially as text will be rendered over the top. Judge for yourself. Their driver pictures are PNGs, which is a bad choice for photo data. I can get one of them from 1mb down to 21k as a JPEG. See the results. Result Mercedes 19.5s Ferrari 46.1s Red Bull 15.8s Renault 32.4s The download priority of those important scripts hits hard here, and the images still all the bandwidth. These things can be easily fixed, but are slowing the site down by 15+ seconds. good HTTPS good HTTP/2 good Gzip good Minification good Caching bad Image compression bad Render-blocking scripts bad Unstable render bad Main thread lock-up bad Unnecessary CSS bad Unnecessary JS bad Late-loading fonts bad Unnecessary icon fonts Haas Haas are the newest team in F1 (unless you count rebrands), so their website should be pretty new. They've kept their drivers from last year, despite one of them managing to crash at slow speeds for no reason, and blame another driver who wasn't there. Their car looks pretty different this year with the arrival of a new sponsor, Rich Energy. Y'know, Rich Energy. The drink. You must have heard of it. Rich Energy. It's definitely not some sort of scam. Website. WebPageTest results. First lap The user gets 4.5s of nothing, and then it's interactive! Pretty good! It takes a long time for that first image to show, but hey, it doesn't block interactivity. It terms of improvements, it's a similar story. There's a server render, but it's blocked by render-blocking scripts in the <head>. These come from a couple of different servers, so they pay the price of additional HTTP connections. But, the amount of script blocking render is much smaller than other sites we've looked at so far. Only a fraction of the JS and CSS is used, so splitting those up would really improve things. The main CSS isn't minified which hurts load time slightly. Again, font preloading would help here. Second lap A good caching setup reduces the amount of network traffic. However a combination of image decoding, JavaScript, and layout hits hard, locking up the main thread until the 8 second mark. Their second load is slower than their first. Because their overall load time for the first load is network-limited, things like image decoding and script execution happen gradually. In this case, it all lands at once. Images Things aren't great here. The first couple of carousel images are 3mb each, and look like they came straight off a digital camera. I guess they were uploaded via a CMS which doesn't recompress the images for the web. Using Squoosh (have I mentioned Squoosh yet?), I can take those thumbnails from 3mb to around 56k as a JPEG, and 44k as WebP. See the results. Result Mercedes 19.5s Ferrari 46.1s Red Bull 15.8s Renault 32.4s Haas 12.5s Despite problems, they jump into 1st place! However, it feels like it could be a lot faster by solving some of the main-thread issues. good HTTPS good HTTP/2 good Gzip good Minification (mostly) good Caching bad Image compression bad Render-blocking scripts bad Main thread lock-up bad Unnecessary CSS bad Unnecessary JS bad Late-loading fonts McLaren McLaren do sell the occasional car, but they're a racing team through and through. Although, in recent years, they've been a pretty slow racing team, and as a result they've lost their star driver. But will the website reflect the problems they're having on track? Website. WebPageTest results. First lap In terms of user experience, the user gets nothing for the first 10.6 seconds, but the content jumps around until 24.3 seconds. The first problem we see in the waterfall is the amount of additional connections needed. Their content is spread across many servers. Their main CSS is 81k, but 90% unused for the initial render. Ideally the stuff needed for first render would be inlined, and the rest lazy-loaded. Then there's a request for some fonts CSS (row 6). It's on yet another server, so we get another connection, and it serves a redirect to yet another server (row 10). The CSS it eventually serves is 137k and uncompressed, and is mostly base64 encoded font data. This is the biggest thing blocking first render. It's bad enough when text rendering is blocked on font loading, but in this case the whole page is blocked. Ideally, required fonts should be served as their own cachable resources, and preloaded. Then we get their JS bundle, which is 67% unused, so could benefit from some code splitting. It's also located at the bottom of the HTML, so it'd benefit from being in the <head> with defer. This would make it start downloading much sooner. Then we get a request for 'jsrender', which seems to be the framework this site uses. This sits on yet another server, so they pay the price of yet another connection. The content that appears at 24s is included as JSON within the HTML, and it isn't clear why it takes so long to render. Their main script uses a lot of main thread time, so maybe it's just taking ages to process all the template data, and isn't giving priority to the stuff that needs to appear at the top of the page. This is the first site so far that serves its content in JavaScript rather than HTML. Second lap Caching headers are mostly absent, but browser heuristics make it look better than it is. The number of connections needed still hits hard, as does the JS processing time. The content doesn't appear until 16.4s, and it's worth noting that the main thread is still really busy at this point. Images They have a 286k spritesheet which needs breaking up, and perhaps replaced with SVG. Usually I would try to reduce the palette on something like this, but it loses too much detail. Anyway, I can get it down from 286k to 238k as a PNG, and 145k as WebP without any loss at all. See the results. They avoid downloading large images until the user scrolls, which is nice. Taking one of the images that arrives, I can get it down from 373k to 59k as a JPEG and 48k as WebP. See the results. Result Mercedes 19.5s Ferrari 46.1s Red Bull 15.8s Renault 32.4s Haas 12.5s McLaren 40.7s Not back-of-the-grid, but close. To be competitive, this site needs proper caching, fewer connections, and a decent before-JavaScript render. good HTTPS good HTTP/2 good Gzip good Minification good Lazy-loading images bad Render-blocking scripts bad Unstable render bad No cache control bad Image compression bad Main thread lock-up bad Unnecessary CSS bad Unnecessary JS bad Late-loading fonts bad Too many HTTP connections Racing Point Lance Stroll wanted to be a racing driver, so daddy bought him an entire F1 team. That team is called Racing Point because I dunno maybe they were trying to pick the dullest name possible. Website. WebPageTest results. First lap The user gets nothing for 45s, but the intended top-of-page content isn't ready until the 70s mark. Also, the main thread is too busy for interaction until 76s. The page starts by downloading 300k of CSS that's served without minification, and without gzip. Minifying and gzipping would be a quick win here, but with over 90% of the CSS unused, it'd be better to split it up and serve just what this page needs. Also, this CSS imports more CSS, from Google Fonts, so it pays the cost of another connection. But the main problem is the browser doesn't know it needs the fonts CSS until the main CSS downloads, and by that time there's a lot of stuff fighting for bandwidth. <link rel="preload"> would be a huge and quick win here, to get that CSS downloading much earlier. It's the CSS that's currently blocking first render. The site also suffers from late-loading scripts. These scripts are at the bottom of the <body>, so they don't block rendering. However, if this JS is going to pop-in content at the top of the page, that space should be reserved so it doesn't move content around. Ideally a server-render of the carousel's first frame should be provided, which the JS can enhance. Also, these scripts should be in the <head> with defer so they start downloading earlier. Like the CSS, they lack minification and gzipping, which are quick wins. The JS is also 64% unused, so could be split up. The carousel JS waits until the images (or at least the first image) has downloaded before displaying the carousel. Unfortunately the first image is 3mb. There's also a 6mb image on the page, but that isn't blocking that initial content render. These images need compressed (more on that in a second). The carousel should also avoid waiting on the images, and maybe provide a low resolution version while the full image downloads. The main thread is then locked up for a bit. I haven't dug into why, but I suspect it's image decoding. Second lap The site has pretty good caching headers, so very little is redownloaded second time around. However, we don't get the main content until 8.2s due to main thread contention. This is a mixture of JavaScript, but mostly (I suspect) image decoding. Images For this one, image performance really matters in terms of first render. I got that 3mb image down to 57k as a JPEG, and 39k as WebP. The WebP is noticeably missing detail, but WebP's artefacts are less ugly than JPEG's so we can afford to go lower, especially since this image sits behind content. You be the judge. Also, their logo is 117k. I'm sure the brand folks would like it to load a bit quicker. By resizing it and reducing colours to 100, I got it to 13k as a PNG, and 12k as WebP. See the results. An SVG might be even smaller. Result Mercedes 19.5s Ferrari 46.1s Red Bull 15.8s Renault 32.4s Haas 12.5s McLaren 40.7s Racing Point 84.2s You might have to scroll a bit to see this one. The problem here is compression and timing. With image, CSS, JS compression, and some CSS preloading, this score would be < 20s. With a good server render of the carousel, it could be < 10s. Unfortunately it's back-of-the-grid for Racing Point. good HTTPS good HTTP/2 good Caching good No render-blocking scripts bad No gzip bad No minification bad Image compression bad Unstable layout bad Main thread lock-up bad Unnecessary CSS bad Unnecessary JS bad Late-loading fonts bad Late-loading CSS bad Late-loading JS Alfa Romeo Alfa Romeo make cars, but they don't really make the one they race. The engine is a Ferrari, and the aero is the work of Sauber, which was the name of this team before 2019. But, a new brand often come with a new website. Let's see how it performs… Website. WebPageTest results. First lap The user gets nothing for 7.9s, but they only get a spinner until 15.6s. You have to scroll to get content, but I'll count this as interactive. The render is blocked until the Google Fonts CSS downloads, which points to a similar problem as Racing Point. And yep, their main CSS (row 2) imports the Google Fonts CSS (row 9). This should be preloaded to allow the two to download in parallel. It's especially bad here, as the font they're downloading is Roboto, which already exists on Android, so no fonts are actually needed. The main CSS is pretty small, but still 85% unused, so splitting and perhaps inlining would help a lot here. After parsing the HTML, the browser discovers a load of images it needs to download, then a couple of scripts at the bottom of the page. However, these scripts are essential to the rendering of the page, so they're loading far too late. A quick win would be to move them to the <head> (they already have defer). This would save around 8 seconds. The JS is also 85% unused, so splitting would definitely help. This page shouldn't need a spinner. Instead, it could have a before-JS render. The page is mostly static, so there aren't too many challenges here. Second lap The site makes excellent use of caching, but the JS-driven rendering slows things down. The main thread is jammed for a few seconds thanks to a combination of JavaScript and image decoding, getting to render in 4.5s. The things which would make the first lap faster would also help here. Images The image right at the top of the page is 236k. Using Squoosh, I managed to get this down to 16k as a JPEG, and 9k as a PNG. See the results. The page also loads a lot of logos. None of them are particularly big themselves, but they add up. I took one example from 18k down to 7k as a PNG. See the results. This kind of saving across all the logos would be significant. Result Mercedes 19.5s Ferrari 46.1s Red Bull 15.8s Renault 32.4s Haas 12.5s McLaren 40.7s Racing Point 84.2s Alfa Romeo 20.1s My gut tells me the quick-wins would knock 10 seconds off this time. good HTTPS good HTTP/2 good Caching good Gzip good Minification bad Render-blocking scripts bad Image compression bad Main thread lock-up bad Unnecessary CSS bad Unnecessary JS bad Late-loading fonts bad Late-loading CSS bad Late-loading JS Toro Rosso It's the "stinky medicine" folks again. They fund two 'independent' teams in F1, with this one being a kinda "B team". But, can they beat their sister team on the web? Website. WebPageTest results. First lap In terms of user experience, the user gets 4.7s of nothing. But, text doesn't start to appear until 5.4s. Then, more text appears gradually, and seems complete at 5.8s. Not bad! Unfortunately the main thread is blocked until 8s. This looks like image decoding, but I'm not sure. Very much like the other sites we've seen, they serve HTML but it's blocked by render-blocking scripts. They should use defer. The CSS is 90% unused, and the JS is over 50% unused. Splitting would help here. There are also late-loading scripts that would benefit from being in the <head> with defer. There's also some unminified scripts thrown in there too. But, despite this, their JS & CSS isn't too big. The biggest performance problem this site has is fonts. That's why the text comes in late. The fonts are late-loading, so preloading them would have a huge benefit here. The fonts are also a little big. Using woff2 would be a quick win here, but it might be worth considering getting rid of some of the fonts all together. But yeahhhhh that can be a tough argument to have. Second lap We get content at 5s, but the main thread is locked until 7s. Again, I think this is down to images. The caching headers are mostly great, except for the fonts which only cache for 30 seconds. That's why we see so many revalidations in the waterfall above. Allowing the fonts to cache for longer would have saved a good few seconds here. Images You'll never guess what. I decided to use Squoosh here. Their top image isn't really optimised for mobile. I managed to get it down from 152k to 26k as a JPEG. See the results. Result Mercedes 19.5s Ferrari 46.1s Red Bull 15.8s Renault 32.4s Haas 12.5s McLaren 40.7s Racing Point 84.2s Alfa Romeo 20.1s Toro Rosso 12.8s So close! Only a couple of tenths off Haas. With font preloading and some image compression, they'd easily be in 1st place. good HTTPS good HTTP/2 good Gzip good Minification (mostly) good Caching (mostly) bad Image compression bad Render-blocking scripts bad Main thread lock-up bad Unnecessary CSS bad Unnecessary JS bad Late-loading fonts Williams Williams are probably my favouite team. They're relatively small, independent, and have an incredible history. However, their last few seasons have been awful. Hopefully that won't reflect on their website… Website. WebPageTest results. First lap In terms of user experience, the user sees nothing until 7.9s, but then they have visible content. The first thing that stands out here is all the additional connections. I thought this meant they were using a lot of different servers, but a closer look shows they're using old HTTP/1. This means the browser has to set up separate connections for concurrent downloads. A switch to HTTP/2 would be a big win. But look, those fonts are arriving nice and early. A quick look at their source shows they're using <link rel="preload">, yay! Then we get their CSS, which is fairly small but still 90% unused. Splitting and inlining these would show a big improvement. Then we get a few render-blocking scripts in the head. These should be deferd to allow the server render to show before JS loads. The page is pretty much static so this shouldn't be too hard. Their main JS loads at the end of the document, and it's a whopping 430k. This page barely needs any JS at all, so I'm pretty sure that can be significantly reduced, if not discarded. Thankfully it doesn't block render. Second lap The caching headers are good, so very few requests are made on the second run. However, HTTP/1 slows down the initial request, then that massive script arrives from the cache and takes up 1.5s of main thread time. In total, it takes 6.2s to render. The fixes from the first run will also fix these issues. Images I took a look at the main image. It's a wide image, but the sides aren't shown on mobile. Also, it has a dark overlay over the top, which reduces the fidelity of the image. I cropped the image a bit, and applied the overlaid colour to the image, so the compressor could optimise for it. Using Squoosh (drink!), I got the size from 267k to 48k as a JPEG, and 34k as WebP. Since text is rendered over the top, it might be reasonable to compress it more. See the results. Result Mercedes 19.5s Ferrari 46.1s Red Bull 15.8s Renault 32.4s Haas 12.5s McLaren 40.7s Racing Point 84.2s Alfa Romeo 20.1s Toro Rosso 12.8s Williams 14.1s Despite issues, this is one of the fastest times. With HTTP/2, it might have jumped into 1st place. good HTTPS good Gzip good Minification good Caching good Preloading fonts bad HTTP/1 bad Render-blocking scripts bad Image compression bad Main thread lock-up bad Unnecessary CSS bad Unnecessary JS Fantasy F1 I'm going to throw the official fantasy F1 site into the mix too. It's my blog I can do whatever I want. Website. WebPageTest results. First lap In terms of user experience, the user gets nothing until 12s. But the text for the main call-to-action doesn't display until 14.5s. The main thread looks blocked until at least the 15s mark. This is the first site we've seen with a near-empty <body>, and also the first to use one of the modern frameworks (Ember). Initial render is blocked on CSS and render-blocking scripts. Because the render is JavaScript driven, the browser doesn't know much about the resources it needs to download until Ember renders the page. That's why we see a load of requests begin around the 9.5s mark: The page would benefit massively from a server render. But, given there isn't really any interactivity on this page, they could consider removing the JS all together, or preloading it for when it is needed. It certainly doesn't need 700k of JS. Netflix removed React from their landing page and saw a huge speed boost. The same could be done here. The CSS is 95% unused, so it could be split up so this page can use the necessary parts. The button text appears late because of late-loading fonts. Which is also excasserbated by the JavaScript-render. As with other pages, this page would benefit hugely from font preloading. Also some of the fonts are TTFs. They're gzipped, but woff2 would be much smaller. Second lap The JS URLs look versioned, but their Cache-Control header requires the browser to check for an update every time. You can see that in the 304 responses in the waterfall. Once the scripts have landed, they lock up the main thread until at least the 7s mark. Again, removing JavaScript and making this a mostly static page would improve things massively. Images In terms of images, I took a look at the main background image at the top of the page. The team have clearly tried to make this small, because the JPEG compression is pretty visible. However, it has the same problem as the Williams image – most of it isn't visible on mobile, and it has an overlay. I baked the overlay into the image, cropped it a bit, and took it down from 170k down to 54k as a JPEG, and 43k as WebP. It might be possible to go lower, but I was already dealing with an image with a lot of compression artefacts. See the result. There's also a 219k PNG. I think it's a PNG because it has transparency, but it's only ever displayed over a white background. I gave it a solid background, and took it down to 38k as a JPEG, and 20k as WebP. See the result. Another image caught my eye. There's an image of the Spanish flag as an SVG (29k), which can be optimised via SVGOMG to 15k. However, the Spanish flag is pretty intricate, and displayed really tiny on the site (30x22). I got it down to 1.2k as a PNG, and 800b as WebP. See the result. Although SVG is usually the best choice for logos, sometimes a PNG/WebP is better. Result good HTTPS good HTTP/2 good Gzip good Minification bad Render-blocking scripts bad Main thread lock-up bad Image compression bad Unnecessary CSS bad Unnecessary JS bad Poor caching bad Late-loading fonts bad Late-loading images And that's the last site. How do the all compare? Final results Ok here we go! Mercedes 19.5s Ferrari 46.1s Red Bull 15.8s Renault 32.4s Haas 12.5s McLaren 40.7s Racing Point 84.2s Alfa Romeo 20.1s Toro Rosso 12.8s Williams 14.1s Fantasy F1 22.0s Congratulations to the Haas team who cross the finishing line first, and have the fastest website in Formula One. Toro Rosso are really close, and Williams complete the podium. Final thoughts This has been kinda eye-opening for me. In my job, I feel I spend a lot of time trying to convince users of frameworks to get a meaningful static/server render as part of their build process early on. It has a huge impact on performance, it gives you the option of dropping the client-side part of the framework for particular pages, and it's incredibly hard to add later. However, none of the teams used any of the big modern frameworks. They're mostly Wordpress & Drupal, with a lot of jQuery. It makes me feel like I've been in a bubble in terms of the technologies that make up the bulk of the web. It's great to see HTTPS, HTTP/2, gzip, minification, and decent caching widely used. These are things folks in the performance community have been pushing for a long time, and it seems like it's paid off. However, one of the bits of performance advice we've been pushing the longest is "don't use render-blocking scripts", yet every site has them. Most of the sites have some sort of server/static render, but the browser can't display it due to these blocking scripts. Pretty much every site would massively benefit from font preloading. Although I'm not suprised it's missing from most sites, as preloading hasn't been in browsers as long as other performance primitives. I guess code splitting is newish too, which is why there isn't a lot of it. It feels like the quick-wins would cut the times down by 25%, if not more. An unblocked render would bring many down to the 5 second mark. Some of the issues feel like they may have happened after the site was launched. Eg, a too-big image was uploaded, or a massive horse was added to the header. RUM metrics and build reporting would have helped here – developers would have seen performance crashing as the result of a particular change. They aren't totally comparable, but this page (which is kinda huge and has a lot of images) would score 4.6s on the same test (results). Squoosh would score 4.2s (results), but we spent a lot of time on performance there. Most of this is down to an unblocked initial render and code splitting. Anyway, does this mean Haas are going to win the 2019 Formula One season? Probably. I mean, it makes sense right?

A declarative router for service workers

I'm looking for feedback on this API. It isn't yet supported in any standard or browser. In the very early days of service workers (while they were still named "navigation controllers") we had the idea of a declarative router. This provided a high-level API to define the behaviour of particular routes, so the service worker wouldn't need to be started. We dropped the idea because we didn't want to optimise for things without clear evidence of what needed optimising, and we didn't want to design high-level APIs before common behaviours became clear. Well, 5 years later, things are somewhat clearer now, and it seems like we'd benefit from something like this. But, I'd like you to be the judge. The proposal The idea is you can define routes during the install event of the service worker: addEventListener('install', (event) => { const router = event.router; router.get( new RouterIfURLStarts('/avatars/'), [new RouterSourceCache(), new RouterSourceNetwork()], ); router.get( new RouterIfURLEnds('.mp4', { ignoreSearch: true }), new RouterSourceNetwork(), ); router.get( new RouterIfURL('/', { ignoreSearch: true }), new RouterSourceCache('/shell.html'), ); // … }); The above defines three routes. The first matches GET requests for URLs that begin /avatars/, and looks for a match in the cache, and failing that, goes to the network. The second matches GET requests for URLs that end .mp4 (ignoring any query string), and goes straight to the network. The third matches GET requests for / (ignoring any query string) and serves a particular item from the cache. For any fetch that would usually trigger a fetch event in a particular service worker, the routes would be checked in the order they were declared. If the conditions of a particular route are met, then that route will be used instead of triggering a fetch event. These routes are scoped to this service worker. If you update the service worker to one without any routes, all the routes are gone. This is different to things like the cache API and IndexedDB, which live independently of the service worker. router.add, router.get router.add(conditions, sources); router.get(conditions, sources); conditions is a single condition, or an array of conditions for this route. All conditions must match for the route to be used. sources is a single source, or an array of sources. A source is somewhere the service worker should attempt to get a response from. Each is tried in sequence until a response is found, and that response is used. router.get is the same as router.add, except it includes the condition that the request must use the GET method. Conditions I'm not sure which conditions we'd want to launch with, but here are a few I have in mind: RouterIfMethod router.add( new RouterIfMethod('POST'), // … ); A condition that checks the method of the request. RouterIfURL router.add( new RouterIfURL(url, { ignoreSearch }), // … ); A condition that checks the URL of the request. The whole URL must match, although the query string is ignored if ignoreSearch is true. If url is not absolute, it's resolved against the URL of the service worker. RouterIfURLStarts router.add( new RouterIfURLStarts(url), // … ); A condition that checks if the URL of the request starts with url. If url is not absolute, it's resolved against the URL of the service worker. This means /articles/ will match a same-origin request to /articles/2019/css/. RouterIfURLEnds router.add( new RouterIfURLEnds(end, { ignoreSearch }), // … ); A condition that checks if the URL of the request ends with end. This means new RouterIfURLEnds('.jpg', { ignoreSearch: true }) will match requests to any origin where the path ends .jpg. RouterIfDate router.add( new RouterIfDate({ from, to }) // … ); A condition that checks the current UTC date. from defaults to 0, to defaults to infinity. This kinda feels like an edge case to me, but it's something Facebook are very interested in. They want to use it to 'expire' an old service worker. Eg, if the service worker is 5 days old, go straight to the network for everything. Combining conditions router.add( [ new RouterIfURLStarts('/'), new RouterIfURLEnds('.jpg', { ignoreSearch: true }), ] // … ); The above matches same-origin requests with a path that ends .jpg. Shortcuts It might be intuitive to allow strings to be used as conditions: '/' means new RouterIfURL('/', { ignoreSearch: true }). '/articles/*' means new RouterIfURLStarts('/articles/'). '*.jpg' means new RouterIfURLEnds('.jpg', { ignoreSearch: true }). This means you could do: router.get('/', new RouterSourceCache('/shell.html')); router.get('*.mp4', new RouterSourceNetwork()); Sources Again, this is something that can be expanded over time. Here are a few: RouterSourceNetwork router.add( conditions, new RouterSourceNetwork({ request, requireOkStatus }), ); Try to get a response from the network. The current request will be used, although you can provide your own using the request option. A 404 will be treated as a successful response, unless requireOkStatus is true. RouterSourceCache router.add( conditions, new RouterSourceCache({ request, cacheName, ignoreSearch, ignoreMethod, ignoreVary }), ); Try to get a response from the cache API. The current request will be used, although you can provide your own using the request option. The rest of the options are the same as caches.match. RouterSourceFetchEvent router.add( conditions, new RouterSourceFetchEvent({ id }), ); If a router matches, it's used instead of dispatching a fetch event. RouterSourceFetchEvent allows you to override this and fire a fetch event. For example: addEventListener('install', (event) => { event.router.get('/articles/*', [ new RouterSourceCache({ ignoreSearch: true }), new RouterSourceFetchEvent({ id: 'article-not-in-cache' }), ]); // … }); addEventListener('fetch', (event) => { if (event.routerDispatchId === 'article-not-in-cache') { // Get the article from the network & cache it } // … }); I haven't thought much about the naming of routerDispatchId, but you get the picture. This allows you to fall back to JS logic in cases the router can't handle. Goals Ok, that's the proposal. Here's the requirements that led me to the above design: Handle fetches without starting the service worker Right now, each request from a controlled page dispatches a fetch event in the service worker. This means each request results in executing JS, which may also incur the cost of starting up the service worker. If you're responding from the cache, you make a net performance gain, but if the service worker requests the response from the network (which is the default action without a service worker), then the service worker start-up and execution time is a waste. Declarative routes allow you to avoid this cost for requests where the logic is simple and common. However, this means the route logic needs to work without running JavaScript, that's why the proposal uses things like RouterIfURL rather than plain if statements. It also means we can't use callbacks. Things like RegExp are tricky as there's currently no definition for how they could work without JavaScript. This is complicated further when you consider the state held within a RegExp object. Also, writing URL paths in RegExp suuuuuuucks /\/wtf\/is\/this\//. Be composable I've been looking at APIs like Express and Workbox when designing this. Looking at registerRoute in Workbox, I wanted to avoid the pattern where the 'handler' is in-between two conditions. Instead, I want to group the conditions together at the start. I also wanted to avoid separate strategies for "network first", "network only", "cache first" etc etc. Instead, by having a network and a cache source, you can achieve all of these patterns by changing the combination and order. Be a layer on top of the 'fetch' event You shouldn't be able to do anything with the router that you can't already do within a fetch event. If we want to add new behaviours, we should add them at a lower level first. This means the router can be polyfilled as a fetch event listener. Avoid nibbling at the problem We took a swing at some performance issues with navigation preload. I'm still happy with this API, as it's pretty low-level. However, I'm worried about adding a series of APIs that deal with individual performance issues. For example: addEventListener('install', (event) => { event.router.get( new RouterIfDate({ to: Date.now() + fiveDays }), new RouterSourceNetwork(), ); event.router.get('*.mp4', new RouterSourceNetwork()); }); …could instead be something like: addEventListener('install', (event) => { event.expireFetchEvent(Date.now() + fiveDays); event.addPathExclusion({ method: 'GET', endsWith: '.mp4' }); }); …where we battle each use-case with a separate API. However, the composability of the router feels like it would cover more use-cases and involve fewer independent moving parts for developers to learn. Avoid state across service workers The router could be stored along with the service worker registration, and be mutated from both pages and service workers (this is now navigation preload works). However, this would create a footgun where service worker v1 adds some routes, v2 removes some, v3 adds some more… if the user jumps from v1 to v3 they could end up in a broken state that's really hard to debug. You'd probably need an upgrade system similar to IndexedDB to prevent this, and if you're looking to IndexedDB for API guidance… well it's probably best to rethink. So yeah, I think it's important to tie the state to a single service worker. Routes would need to be defined before the install event completes, so the routes a service worker activates with are the same throughout its life. Feedback please! Is this an API you'd be happy using? Is it something you'd use even if you weren't trying to optimise for a particular performance problem? Is there anything missing? The goal isn't to replace the fetch event, we're only trying to avoid it for common cases where the execution of the service worker outweighs its result. Leave your comments below, or if you want to dive into more of the technical details see the GitHub thread. Update: I've also explored a similar API that doesn't involve as many constructors.

What happens when packages go bad?

I built spritecow.com back in 2011, and I no longer actively maintain it. A few months ago, a user berated me for using a crypto currency miner on the site without their informed consent. And sure enough, the site's JS had a small addition that loaded the mining JS, and sent the result somewhere else. I'm still not 100% sure what happened, I guess someone had gained access to the Amazon S3 bucket that hosted the site and made changes. The bucket was owned by an agency I used to work for, and I no longer had access to it. Either they were careless with the key, or I was. I dunno, maybe I accidentally committed it and failed to purge it from git history. I was still in control of the domain, so I was able to deploy the site somewhere else, without the coin miner, and point the domain at the new location. *dons flat cap* In my day, you knew a site was hacked because you'd be greeted with green-on-black text stating the site was "0wned" by the "hackersaurus" and their "l33t crew". You'd also get a few animated GIFs of skulls, and if you were really lucky, a picture of a big ol' arse. But now… now it's all stealthy crypto bullshit. This hack was a break-in, but recent events got me thinking about attacks where the malicious code is unwittingly invited in. When packages go bad In case you missed the drama, here's roughly what happened: The original author of event-stream was no longer interested in maintaining the code. Someone offered to take on the burden, and the original author agreed. The new owner turned out to have malicious intents, and modified event-stream in a way that made targeted changes to the build of another app, Copay (a bitcoin management Electron app), which used event-stream as a dependency. As a result, some versions of Copay would essentially try to rob users. I think the author of event-stream did the wrong thing when they handed over the keys to a package without any sort of vetting. But, before this publicised incident, I might have done the same thing, and I'm sure many others have done the same. So where does that leave us? A small JS project can easily build up 1000+ npm dependencies. Some of these will be executed in a Node.js context, others will run in the browser. Perhaps apps that deal with user data, especially passwords and finances should audit every package, and every change to those packages. Perhaps we need some kind of chain-of-trust for audited packages. But we don't have that, and for most of us, auditing all packages is a non-starter. The remaining option is to treat all npm packages as potentially hostile, and that's kinda terrifying. We recently launched squoosh.app. It's a 'static' web app in that it does heavy lifting on the frontend, but it doesn't have a server component, and it doesn't store or transfer user data. The code is on GitHub, and Netlify builds and deploys the 'live' branch. It feels like it has a small attack surface, yet we use over 1700 packages. We only have 49 direct dependencies, so the vast majority of these are dependencies of dependencies of dependencies etc etc. The web app itself doesn't use a lot of third party code, so most of these packages are part of the build system. As a thought exercise, I explored the kind of powers an evil dependency could have, and what, if anything, could be done to prevent it. I got some friends to review a draft of this article, and they pointed out there's a lot of crossover with David Gilbertson's article from earlier this year. They're right. It's kinda gutting. Anyway, I'm told there's enough difference to warrant posting this, but I'm worried people are just being nice. You be the judge… Attack the developer When a module from a package is executed in a node context it has the same powers as the user that called node. This means it can do whatever you can do from the CLI, including: Transferring your SSH keys elsewhere. Deleting stuff, or encoding it and holding it to ransom. Crawling your other projects for secret stuff. Of course, npm will delete packages that are found to do evil stuff, but as we've seen with the event-stream case, it's possible for a malicious package to go unnoticed for months. You don't even need to require() the package to give it access to your system. npm packages can have a post-install hook in their package.json: { "name": "node-sass", "scripts": { "postinstall": "node scripts/build.js" } } If you run npm install on a project, each package can run CLI commands on your behalf. Squoosh might be an interesting target for this, because the folks running npm install are often Googlers, so the SSH keys might be particularly valuable (although a combination of passphrases & two-factor auth will reduce the usefulness of a key). Googlers are also a good target for corporate espionage, where the attacker would search the local filesystem and network for company secrets. One solution is to do what browsers do – sandbox the script. This can be done using a virtual machine that can only access a single directory of the host. Docker makes this relatively easy, and the virtual machines can be easily torn down and rebuilt. Maybe this is something npm should do by default. Of course, some packages need wider system access of course, but these could be run with an --unsafe-no-sandbox flag, or whatever. Attack the server Basically the same as above, but the attacker aims to infect the server. From there they could monitor and edit other things on the server, perhaps data and code belonging to other sites. The solution is the same, sandbox the code. This is standard practice for a lot of hosts already, and Netlify is no exception. I'm going to talk a lot about Netlify, since it's what we're using, but the same stuff applies to Firebase, Now, App Engine etc etc. Attack the user When we push to the live branch, Netlify clones it, runs our build script, and serves the content of the build directory. If we have a malicious package in our dependencies, it can do whatever it wants to the checked out copy of the repo, including the built files. Netlify supports _headers and _redirects files generated at build time. So along with modifying served content, an attacker could modify those. Netlify also supports cloud functions, but we don't use them. These can be enabled via a netlify.toml configuration in the root of the repo. However, I'm told this is read before npm install, so a malicious package won't be able to enable cloud functions. Phew! However, the header and redirect parts of netlify.toml are read after build, so that's another vector to watch out for. So, what could these attacks look like? Coin mining or a similar abuse of user resources, such as carrying out DDoS attacks. Users tend to notice attacks like this due to excessive CPU usage. Although, that might be harder to spot with Squoosh, since it uses CPU during image compression. Stealing user data. Squoosh does all its work on the client, meaning images you open/generate in the app don't leave your machine. However, a malicious script could send your pictures elsewhere. Users may spot these unexpected network requests in devtools. Content change. The old school. The l33tasaurus and their hacker crew proudly boast of their attack, along with a picture of an excessively-sized arse. Users are likely to spot the giant arse. Subtle content change. Add something like a "donate" link which goes to an account owned by the attacker. Users may think this is intentional, so it might take someone involved in the project to spot it. What can we do about it? Let's start from the worst-case scenario: Recovering after a successful hack Our job is to get everything back to normal as quickly as possible. First step: redeploy without the malicious package. The site is now fixed for everyone who didn't visit while the site was hacked. For everyone else, it depends on how crafty the attacker was. They may have modified headers to give their malicious resources a long cache time. With Squoosh we hash most of our resource URLs, so we can avoid a lot of caching issues easily. That leaves the root HTML, and the service worker script. Without a service worker, the user might continue to get the hacked HTML from their HTTP cache for a long time, but the service worker gives us a bit more control. When the user visits Squoosh, the browser will check for updates to the service worker in the background. Our new, unhacked service worker is in a good position to look at the current state of things and decide if the user is running the hacked version. If that's the case, we need to get rid of anything the hacked version may have compromised. The best way to do that is to burn it all down & start again. The new service worker could dump all caches, unregister itself, and navigate all clients to /emergency. This URL would serve a Clear-Site-Data: * header, deleting everything stored & cached by the origin, then redirect to /. addEventListener('install', (event) => { event.waitUntil(async function() { if (isRunningHackedVersion()) { for (const cacheName of await caches.keys()) { await caches.delete(cacheName); } await registration.unregister(); const allClients = await clients.matchAll({ includeUncontrolled: true }); for (const client of allClients) { client.navigate('/emergency'); } return; } // … }()); }); Unfortunately Safari & Edge don't support Clear-Site-Data. This means the HTTP cache may still be compromised. To work around this, we could redirect the user to /?[random number] rather than /, and force the browser to bypass the cache when storing the page: addEventListener('install', (event) => { event.waitUntil(async function() { if (isRunningHackedVersion()) { // As above } const cache = await caches.open('static-v1'); await cache.addAll([ new Request('/', { cache: 'reload' }), // + CSS & JS ]); }()); }); { cache: 'reload' } tells the browser to bypass the HTTP cache on the way to the network, but it may put the response in the cache. The attacker may have given the service worker script a long cache time, but the spec mandates that the browser caps this at 24hrs. Even if the attacker has been really crafty, they can't lock users into the hacked version longer than that. Limiting attacks What can we do to limit the impact of an attack? Well, a lot of attacks benefit from contacting another server. Sending the results of coin mining, forwarding-on user data, and of course, downloading giant arse imagery. CSP can help a lot here: Content-Security-Policy: default-src 'self' www.google-analytics.com 'sha256-QHnk…' 'sha256-kubd…' With this header we can limit communication to the same origin, Google Analytics, and allow our inline styles and scripts. Communication with other origins is blocked. If we were using cloud functions, the attacker would be able to make the communication same-origin, and make the cloud function proxy the data. However, we aren't using cloud functions, so this isn't a concern. CSP doesn't prevent a coin miner using the user's CPU, it just prevents the attacker profiting from it. However, the attacker can also modify the headers file during build, so it'd be trivial for them to remove or modify the CSP. It feels like we need to split our build process into "trusted" and "untrusted", so the build would do this: Run the "trusted" build script. The "trusted" script runs the "untrusted" script in a sandbox. Once the "untrusted" script has completed, the "trusted" script sets sensitive things like headers, ensuring any headers the "untrusted" script set are discarded. The "trusted" script would only use audited dependencies. Noticing an attack before deploy A malicious script could edit our source in the hope that a contributor wouldn't notice and commit the result. But, we create PRs for all changes, and review them before merging, so this seems unlikely. Let's assume the attacker is making changes at build time to avoid this. Spotting visual changes Netlify automatically builds our PRs & branches, so it's easy to review the result of a change before it goes live. As such, we'd quickly see content changes by the crewasaurus and the hackerl33t. Unless of course, they did this: if (location.origin === 'https://squoosh.app') { document.body.innerHTML = `<h1>LOL</h1>`; } We wouldn't see this during staging because the origin would be something like https://deploy-preview-366--squoosh.netlify.com/, but the same code on the live server would change the content. We could deploy to a different-but-identical server, and use hosts files to point at that server when we access https//squoosh.app. This would catch the above example, but it wouldn't catch attacks that triggered after a future date, for example. Spotting unexpected source changes Looking at the above code example, it's pretty easy to tell it's malicious, but would I spot it amongst 80k of minified code? I've looked at the minified output before to check if tree-shaking is working as expected (it wasn't btw), but it isn't something I do often. Assuming that the attacker can't bring in external resources (due to the CSP mitigation), they're going to have to make it part of the output. I want each PR to automatically include the before/after size of every asset, including removed/added assets. This is for performance reasons, but it also means we'd spot the 265k increase caused by something like Coinhive (a JS coin miner), or the addition of plethora-of-arse.jpg. We'd still miss something small, or something that was able to offset itself by removing code we wouldn't immediately spot. Preview != production These mitigations assume that the preview build is the same as the production build, but this isn't true. When we move our live branch, Netlify rebuilds and deploys, even if it's already built that git commit before. This means a malicious package could look at the environment, plus the state of GitHub, and realise a given build isn't going to be published at squoosh.app. In this case, the malicious package could do nothing, leaving no trace in the staging version of the site. If the deploy is heading for squoosh.app, it could make its changes, and we wouldn't notice until it was live (or sometime much later). If a build has already been completed for a particular commit (for a PR, or another branch), Netlify should reuse it by default. That way we can be sure that the site we're checking in staging is the same as the site that'll be deployed to live. Without this feature, we could use our "trusted" + "untrusted" build system from earlier. The "trusted" part could check the output matches the output from an earlier build that should be identical. In conclusion: uh oh It's been terrifying to think this through, and this is just for a static site. Some of the mitigations I've detailed here are pretty complicated, and partial. For sites with a server component and database, it feels negligent to use packages you haven't audited. With Copay, we've seen that attacks like this aren't theoretical, yet the auditing task feels insurmountable. I don't really have answers, just worries. Anyway, sleep well!

I discovered a browser bug

I accidentally discovered a huge browser bug a few months ago and I'm pretty excited about it. Security engineers always seem like the "cool kids" to me, so I'm hoping that now I can be part of the club, and y'know, get into the special parties or whatever. I've noticed that a lot of these security disclosure things are only available as PDFs. Personally, I prefer the web, but if you're a SecOps PDF addict, check out the PDF version of this post. Oh, I guess the vulnerability needs an extremely tenuous name and logo right? Here goes: Why Wavethrough? Well, it involves wave audio, and data is allowed through that shouldn't be. Tenuous enough? All the browser security bugs I cover in this post have since been fixed. Make sure your browser is up to date. As I said, I stumbled into this whole thing by accident. Here's how it happened from the start: Media via a service worker didn't quite work If you have a service worker like this: addEventListener('fetch', (event) => { event.respondWith(fetch(event.request)); }); …the idea is you shouldn't see any behavioural difference between this and no-service-worker. Unfortunately cross-origin <video> and <audio> doesn't quite behave the same. Seeking doesn't work, and sometimes it fails entirely. <video> and <audio> are different from most web APIs in that they use range requests. Ok, let's push that onto the stack: Range requests Usually when the browser makes a request, it's asking for the whole resource. However, HTTP defines the Range header and partial content responses. For example, the request may have the following header: Range: bytes=50-100 …which is requesting bytes 50-100 (inclusive) of the resource. The server may then respond with a 206 Partial Content, and a header like this: Content-Range: bytes=50-100/5000 …indicating it's returning bytes 50-100 (inclusive) of a 5000 byte resource. Browsers use this for resuming downloads, but it's also used by media elements if the user seeks the media, so it can go straight to that point without downloading everything before it, or to pick up metadata if it's one of those annoying media formats that has important metadata at the end of the file. Unfortunately, via a service worker, that Range header was going missing (dun-dun-dunnnnnnnnn!). This is because media elements make what we call "no-cors" requests. Let's push that onto the stack too: No-cors requests If you fetch() something from another origin, that origin has to give you permission to view the response. By default the request is made without cookies, and if you want cookies to be involved, the origin has to give extra permission for that. If you want to send fancy headers, the browser checks with the origin first, before making the request with the fancy headers. This is known as CORS. However, some APIs couldn't give a shit about all that. They make "no-cors" requests, so the checks above don't happen. If you make a no-cors request to another origin, it's sent with cookies and you get back an "opaque" response. Developers shouldn't be able to access the data of an opaque response, but particular APIs may interpret that data behind the scenes. Take <img> for instance. If you include an <img> that points to another origin, it'll make a no-cors request to that origin using that origin's cookies. If valid image data is returned, it'll display on your site. Although you can't access the pixel data of that image, data is still leaked through the width and height of the image. You also know whether or not you received valid image data. Let's say there's an image that's only accessible if the user is logged into a particular site. An attacker can tell from the load/error event of the <img> whether that user is logged into that site. The user's privacy has been compromised. Yaaaay. Allowing this to happen is a mistake, but we have decades of content depending on this behaviour. We can't simply prevent it, but we can add things to mitigate it in certain situations. If we started the web again, everything would require something like CORS. It isn't just images either. Classic non-module scripts, CSS, and media elements also make no-cors requests by default. No-cors + ranges + service workers So, back to our pass-through service worker: addEventListener('fetch', (event) => { event.respondWith(fetch(event.request)); }); A media element would make a no-cors request with a Range header. When it's passed to fetch() the request object is checked. At this point fetch sees a header (Range) that isn't allowed in no-cors requests, and silently removes it. Therefore the server doesn't see the Range header, so it just responds with a standard 200 response. Why is this header filtered? Well, no one standardised how they were supposed to work. Actually that deserves its own heading: Range requests were never standardised They're standardised in HTTP, but not by HTML. We know what the headers look like, and when they should appear, but there's nothing to say what a browser should actually do with them. Should all media requests be range requests, or just additional requests? What happens if the returned range ends sooner/later than what the browser asked for? What happens if the returned range starts sooner/later than what the browser asked for? What happens if a range is requested but the server returns a normal 200 response? What happens if a range is requested but the server returns a redirect? What happens if the underlying content appears to have changed between requests? What happens if a normal request is made but a 206 partial is returned? None of this is defined, so browsers all kinda do different things. Yay. We couldn't just add the Range header to the safelist, as developers would be able to set it to values the browser would never usually send, and that presents a security risk. Also, with a service worker in the middle, you can respond to a request however you want, even if it's a no-cors request to another origin. For example, you can have an <img> on your page that points to facebook.com, but your service worker could return data from twitter.com. This isn't a problem as you can only lie to yourself. However, media elements piece multiple responses together and treat it as a single resource, and that opens up an interesting attack vector: Can known data be mixed with unknown data to reveal the content of the unknown data? I pretended to be a hacker and wrote down all the attacks I could think of, and Anne van Kesteren pointed out that some of them were possible without a service worker, as you can do similar things with redirects. So, I investigated how browsers currently handle these situations. Mixing known and unknown data Page: Hey, this audio tag needs audio data from "/whatever.wav". 10:24 evil.com: No problem, here's 44 bytes of data. 10:24 Page: Cool, I see this is a PCM WAV header, 1 channel, 44100hz, 8bit, 30mins long. However, that's not enough data, can you send me Range: 44- please? 10:24 evil.com: Oh, get that from facebook.com/ instead. 10:24 Page: Ok facebook.com/, here are your cookies, can I get Range: 44- please? 10:24 facebook.com: Sure, here you go… 10:25 I created a site that does the above. I used a PCM wav header because everything after the header is valid data, and whatever Facebook returned would be treated as uncompressed audio. In my opinion, browsers should reject the response from Facebook, as the media element shouldn't allow mixing visible and opaque data. Nor should it allow opaque data from multiple sources, although that isn't happening here. Chrome and Safari rejected as soon as they saw the redirect. This is safe, although they would need to check the response if a service worker was in the middle too, since that can result in a response from somewhere else without a redirect occurring. However… Firefox security bug Beta and nightly versions of Firefox at the time allowed the redirect, combine the responses together, and expose the duration of the audio through mediaElement.duration. Because I set the frequency, bit depth, and channel count of the audio in the header, I could determine the length of the cross-origin resource from the audio length using ✨basic maths✨. const contentLength = audio.duration * /* WAV frequency */ 44100 + /* WAV header length */ 44; Length of sensitive resource revealed in Firefox 59.0b9 It looks like the size isn't detected exactly, but Google returns a range, so the reported size includes the extra 44 bytes that are missing from the start (the WAV header). And here's a link to the attack, which works in Firefox 59.0b9 at least. Leaking the length of a resource may not sound like a big deal, but consider an endpoint like gender.json. The content length can give a lot away. Also see Timing attacks in the Modern Web (PDF, heh) which demonstrates the amount of information content-length can leak. Firefox handled this brilliantly. Within three hours Paul Adenot replied to the bug report, confirming it, and digged into other potential leaks (there weren't any). I was able to engage with engineers directly on how the issue should be fixed, which was important as I was planning how to standardise the mitigation. Since this was a regression caught in beta, Firefox were able to patch it before it reached stable. Edge security bug Edge suffered from the same kind of bug, but with a huge twist. Firstly, it didn't care if the other server returned a 206 or not. Secondly, and this is the big one, it allowed the resulting audio to pass through the web audio API. The web audio API is like the <canvas> equivalent for audio, meaning I could monitor the samples being played: // Get the audio element. const audio = document.querySelector('audio'); // Create a web audio context. const ac = new AudioContext(); // Connect the two. const source = ac.createMediaElementSource(audio); // Create a script processor. // This lets me transform the audio data. I don't really care // about transforming, I just want to collect the data. const scriptNode = ac.createScriptProcessor(256, 1, 1); const datas = []; scriptNode.onaudioprocess = (event) => { const inputData = event.inputBuffer.getChannelData(0); // Store the audio data if (!audio.paused) datas.push(inputData.slice()); }; // Connect the processor. source.connect(scriptNode); scriptNode.connect(ac.destination); audio.addEventListener('ended', (event) => { source.disconnect(scriptNode); scriptNode.disconnect(ac.destination); // Now I can look at all the data received, and turn it from // audio sample data, back into bytes, then into a string. const str = datas.reduce((str, data) => { // Each sample is -1 to 1. // In the original wav it was 16-bits per sample, // so I map each value to a signed 16-bit value. const ints = Array.from(data).map((num) => Math.round(num * 32768)); // Then put that into a typed array. const int16 = new Int16Array(ints); // But, assuming utf-8, I need unsigned 8-bit chunks: const bytes = new Uint8Array(int16.buffer); // Now I can create a string from that. return ( str + Array.from(bytes) .map((b) => String.fromCharCode(b)) .join('') ); }, ''); // Output the data. document.body.appendChild(document.createTextNode(str)); }); And here's what that looks like: Reading cross-origin content in Edge The text you see is the content of BBC News. Since the request is made with cookies, the content is the "logged in" view, although I wasn't logged in for the demo. It's kinda pathetic how excited I got about this, but this is a huge bug. It means you could visit my site in Edge, and I could read your emails, I could read your Facebook feed, all without you knowing. And here's a link to the attack. If this works in your version of Edge, update your browser immediately. Reporting the bug to Microsoft You're about to witness a boy in his mid-30s having a massive entitled whinge. If you want to avoid that, skip this section, but I really need to get it off my chest. The experience I had with Microsoft was very different to Firefox. I filed the issue in Edge's bug tracker on March 1st and notified secure@microsoft.com. I got an email from Microsoft security later that day saying that they don't have access to Edge's bug tracker, and asked if I could paste the details into an email for them. So yeah, Microsoft's security team don't have visibility into Edge security issues. Anyway, I sent them the details of the exploit over plain email. Update: Turns out when you file a security bug with Edge, you get a special URL only the reporter can access. I didn't know this was the case, and it didn't seem like the security contact at MS knew either. The next day they said they couldn't investigate the issue unless I provided the source code. C'mon folks, the "view source" button is right there. Anyway, I sent them the source. Then there was 20 days of silence. At this point I had no idea if they were able to understand the issue, or if they knew how serious it was. I pointed out that the attack could be used to read people's private messages, but received no response. Update: 16 days into the silence I sent a further email "Is it ok if I present this exploit at a conference next week?". I wasn't booked to speak at any conference, I was just trying to elicit a response, to get some indication that the lights were on. It didn't work. I recently found out Microsoft characterised this as a threat. I asked Jacob Rossi and Patrick Kettner (awesome folks who work on the Edge team) if they could chase it internally. After they did, I finally got a reply from Microsoft security saying they were "developing a fix", with no further detail. If you find a bug like this, you're eligible for a bounty. I asked if I could nominate a charity or two to receive the bounty. There was no response. 14 days of silence. I asked Patrick to chase them again (thanks Patrick!), and they replied saying they wouldn't be able to give the bounty to charity, despite their public docs saying otherwise. Apparently the rules changed at some point, and I was looking at old docs. Whatever. Thankfully Google are ok with me taking the money directly, and will match what I donate (I found the bug while at work, so I was worried about the legal implications of taking the money. I'm sure there'll be some tax complications too, ugh). I wasn't getting any progress update, or any details on how they planned to fix it (which would have been useful from a standards perspective). So, I shitposted on Twitter, and Jun Kokatsu kinda sniped back. Jun is a security engineer at Edge, and we got chatting over DMs. And holy shit, this is who I should have been talking to all along. Jun told me there had been a lot of activity around the bug internally, and they're looking to improve visibility of this kind of stuff to the reporter. We were able to discuss what the fix would look like, and how that would work with a service worker in the middle. I really can't stress enough how helpful Jun has been. Microsoft released a patch for the bug, and published CVE-2018-8235. I found out about this through Jun. I haven't heard anything through the official channel. On June 7th I asked the official contact for an update on the bug bounty, since they haven't confirmed any of that yet. I've yet to receive a reply. Update: Shortly after publishing this they contacted me to say I qualify for the bounty. Ok, that was a lot of complaining, but I really want Microsoft to look at the experience I had with Firefox and learn from it. Security issues like this put their users at huge risk, and they need to ensure reporting these things isn't more effort than it's worth. Standards are important I've covered two browser security issues here, but these bugs started when browsers implemented range requests for media elements, which wasn't covered by the standard. These range requests were genuinely useful, so all browsers did it by copying each others behaviour, but no one integrated it into the standard. The result is the browsers all behave slightly differently, and some ended up with security issues. This is why standards are important. Chrome had a similar security issue a few years ago, but instead of just fixing it in Chrome, the fix should have been written into a standard, and tests should have been written for other browsers to check against. I've been working to improve standards here. Range requests are now able to pass through a service worker safely according to the spec. The next step is to specify the request and response handling for media elements. Also, CORB has been added to fetch. The aim here is to reduce the capabilities of no-cors while retaining compatibility with the web. For instance: <img src="https://facebook.com/secret-data.json" /> Previously, the above would fail to load, but the response would be in the same process as the rest of the page. This is really bad thing given Spectre and Meltdown. But CORB will prevent that resource entering the page process, since its content (JSON) isn't something that can be loaded by any no-cors API. CORB also prevents the attack outlined in this post, as it wouldn't allow text/html content to enter the process as the result of a no-cors request. And that's it! I now have a CVE number I can have etched on my grave. And I'm going to sit here and patiently await my invite to all the cool security parties. Thanks to Sandra and Monica Stromann, whose icons I butchered to create the Wavethrough logo. Also thanks to Mathias Bynens, Jun Kokatsu, and Paul Lewis for proofreading & corrections.

Third party CSS is not safe

A few days ago there was a lot of chatter about a 'keylogger' built in CSS. Some folks called for browsers to 'fix' it. Some folks dug a bit deeper and saw that it only affected sites built in React-like frameworks, and pointed the finger at React. But the real problem is thinking that third party content is 'safe'. Third party images <img src="https://example.com/kitten.jpg"> If I include the above, I'm trusting example.com. They may betray that trust by deleting the resource, giving me a 404, making my site look broken. Or, they might replace the kitten data with something a lot less pleasant. However, the impact of an image is limited to the content box of the element itself. I can try and explain to users "Here's some content from example.com, if it goes gross it's their fault, not mine", and hope they believe me. But it certainly can't impact things like password fields. Third party script <script src="https://example.com/script.js"></script> Compared to images, third party script has way more control. If I include the above, I'm giving example.com full control of my site. They can: Read/change page content. Monitor every bit of user interaction. Run computationally heavy code (eg, cryptocoin miner). Make requests to my origin with the user's cookies, and forward the response. Read/change origin storage. …they can do pretty much whatever they want. The 'origin storage' bit is important. If the script interferes with IndexedDB or the cache storage API, the attack may continue across the whole origin, even after you've removed the script. If you're including script from another origin, you must absolutely trust them, and their security. If you get hit by a bad script, you should purge all site data using the Clear-Site-Data header. Third party CSS <link rel="stylesheet" href="https://example.com/style.css"> CSS is much closer in power to a script than an image. Like a script, it applies to the whole page. It can: Remove/add/modify page content. Make requests based on page content. Respond to many user interactions. CSS can't modify origin storage, and you can't build a cryptocoin miner in CSS (probably, maybe, I don't know), but malicious CSS can still do a lot of damage. The keylogger Let's start with the one that's getting a lot of attention: input[type="password"][value$="p"] { background: url('/password?p'); } The above will trigger a request to /password?p if the input's value attribute ends with p. Do this with every character, and you're capturing a lot of data. Browsers don't store the user-inputted value in the value attribute by default, so the attack depends on something that synchronises those values, such as React. To mitigate this, React could look for another way to synchronise password fields, or browsers could limit selectors that match on the value attribute of password fields. However, this would create a false sense of security. You'd be solving things for one particular case, but leaving everything else open. If React switched to using the data-value attribute, the mitigation fails. If the site changes the input to type="text", so the user can see what they're typing, the mitigation fails. If the site creates <better-password-input> and exposes the value as an attribute there, the mitigation fails. Besides, there are many other CSS-based attacks: Disappearing content body { display: none; } html::after { content: 'HTTP 500 Server Error'; } The above is an extreme example, but imagine if the third party was doing that for some small percent of your users. It'd be difficult for you to debug, and erode user trust. More subtle hacks could just remove the 'buy' button occasionally, or rearrange the paragraphs in your content. Adding content .price-value::before { content: '1'; } Oh shit your prices just went up. Moving content .delete-everything-button { opacity: 0; position: absolute; top: 500px; left: 300px; } Take that button that does something severe, make it invisible, and place it over something the user is likely to click. Thankfully, if the button does something really severe, the site is likely to show a confirmation dialogue first. That's ok, just use more CSS to trick the user into clicking the "yes I'm sure" button instead of the "oh god no" button. Imagine if browsers did try to mitigate the 'keylogger' trick. Attackers could just take a non-password text input on the page (a search field, perhaps) and place it over the password input. Now they're back in business. Reading attributes It isn't just passwords you have to worry about. You probably have other private content in attributes: <input type="hidden" name="csrf" value="1687594325"> <img src="/avatars/samanthasmith83.jpg"> <iframe src="//cool-maps-service/show?st-pancras-london"></iframe> <img src="/gender-icons/female.png"> <div class="banner users-birthday-today"></div> All of these can be targeted by CSS selectors and a request can be made as a result. Monitoring interactions .login-button:hover { background: url('/login-button-hover'); } .login-button:active { background: url('/login-button-active'); } Hovers and activations can be sent back to a server. With a moderate amount of CSS you can build up a pretty good picture of what the user's up to. Reading text @font-face { font-family: blah; src: url('/page-contains-q') format('woff'); unicode-range: U+71; } html { font-family: blah, sans-serif; } In this case, a request will be sent if the page contains q. You can create lots of these for different letters, and target particular elements. Fonts can also contain ligatures, so you can start detecting sequences of characters. You can even combine font tricks with scrollbar detection to infer even more about the content. Third party content is not safe These are just a few tricks I'm aware of, I'm sure there are many more. Third party content has a high impact within its sandbox. An image or a sandboxed iframe has a pretty small sandbox, but script & style are scoped to your page, or even the whole origin. If you're worried about users tricking your site into loading third party resources, you can use CSP as a safety net, to limit where images, scripts and styles can be fetched from. You can also use Subresource Integrity to ensure the content of a script/style matches a particular hash, otherwise it won't execute. Thanks to Piskvorrr on Hacker News for reminding me! If you're interested in more hacks like this, including more details on the scrollbar tricks, check out Mathias Bynens' talk from 2014, Mike West's talk from 2013, or Mario Heiderich et al.'s paper from 2012 (PDF). Yeah, this stuff isn't new.

Arrays, symbols, and realms

On Twitter, Allen Wirfs-Brock asked folks if they knew what Array.isArray(obj) did, and the results suggested… no they don't. For what it's worth, I also got the answer wrong. Type-checking arrays function foo(obj) { // … } Let's say we wanted to do something specific if obj is an array. JSON.stringify is an example of this, it outputs arrays differently to other objects. We could do: if (obj.constructor == Array) // … But that's false for things that extend arrays: class SpecialArray extends Array {} const specialArray = new SpecialArray(); console.log(specialArray.constructor === Array); // false console.log(specialArray.constructor === SpecialArray); // true If you want to catch subclasses, there's instanceof: console.log(specialArray instanceof Array); // true console.log(specialArray instanceof SpecialArray); // true But things get more complicated when you introduce multiple realms: Multiple realms A realm contains the JavaScript global object, which self refers to. So, it can be said that code running in a worker is in a different realm to code running in the page. The same is true between iframes, but same-origin iframes also share an ECMAScript 'agent', meaning objects can… (and please read the next bit in a 70s sci-fi voiceover) travel across realms. Seriously, look: <iframe srcdoc="<script>var arr = [];</script>"></iframe> <script> const iframe = document.querySelector('iframe'); const arr = iframe.contentWindow.arr; console.log(arr.constructor === Array); // false console.log(arr.constructor instanceof Array); // false </script> Both of those are false because: console.log(Array === iframe.contentWindow.Array); // false …the iframe has its own array constructor, which is different to the one in the parent page. Enter Array.isArray console.log(Array.isArray(arr)); // true Array.isArray will return true for arrays, even if they were created in another realm. (You're still reading that in the 70s voice over right?) It'll also return true for subclasses of Array, from any realm. This is what JSON.stringify uses internally. But, as Allen revealed, that doesn't mean arr has array methods. Some, or even all of the methods would have been set to undefined, or the array could have had its entire prototype ripped out: const noProtoArray = []; Object.setPrototypeOf(noProtoArray, null); console.log(noProtoArray.map); // undefined console.log(noProtoArray instanceof Array); // false console.log(Array.isArray(noProtoArray)); // true That's what I got wrong in Allen's poll, I picked "it has Array methods", the least-picked answer. So, yeah, feeling pretty hipster right now. Anyway, if you really want to defend against the above, you can apply array methods from the array prototype: if (Array.isArray(noProtoArray)) { const mappedArray = Array.prototype.map.call(noProtoArray, callback); // … } Symbols and realms Take a look at this: <iframe srcdoc="<script>var arr = [1, 2, 3];</script>"></iframe> <script> const iframe = document.querySelector('iframe'); const arr = iframe.contentWindow.arr; for (const item of arr) { console.log(item); } </script> The above logs 1, 2, 3. Pretty unspectacular, but for-of loops work by calling arr[Symbol.iterator], and this is somehow working across realms. Here's how: const iframe = document.querySelector('iframe'); const iframeWindow = iframe.contentWindow; console.log(Symbol === iframeWindow.Symbol); // false console.log(Symbol.iterator === iframeWindow.Symbol.iterator); // true While each realm has its own instance of Symbol, Symbol.iterator is the same across realms. To steal a line from Keith Cirkel, symbols are simultaneously the most unique and least unique thing in JavaScript. The most unique const symbolOne = Symbol('foo'); const symbolTwo = Symbol('foo'); console.log(symbolOne === symbolTwo); // false const obj = {}; obj[symbolOne] = 'hello'; console.log(obj[symbolTwo]); // undefined console.log(obj[symbolOne]); // 'hello' The string you pass to the Symbol function is just a description. The symbols are unique, even within the same realm. The least unique const symbolOne = Symbol.for('foo'); const symbolTwo = Symbol.for('foo'); console.log(symbolOne === symbolTwo); // true const obj = {}; obj[symbolOne] = 'hello'; console.log(obj[symbolTwo]); // 'hello' Symbol.for(str) creates a symbol that's as unique as the string you pass it. The interesting bit is it's the same across realms: const iframe = document.querySelector('iframe'); const iframeWindow = iframe.contentWindow; console.log(Symbol.for('foo') === iframeWindow.Symbol.for('foo')); // true And this is roughly how Symbol.iterator works. Creating our own 'is' function What if we wanted to create our own 'is' function that worked across realms? Well, symbols allow us to do this: const typeSymbol = Symbol.for('whatever-type-symbol'); class Whatever { static isWhatever(obj) { return obj && Boolean(obj[typeSymbol]); } constructor() { this[typeSymbol] = true; } } const whatever = new Whatever(); Whatever.isWhatever(whatever); // true This works, even if the instance is from another realm, even if it's a subclass, and even if it has its prototype removed. The only slight issue, is you need to cross your fingers and hope your symbol name is unique across all the code. If someone else creates their own Symbol.for('whatever-type-symbol') and uses it to mean something else, isWhatever could return false positives. Further reading Iterators Async iterators Keith Cirkel's deep dive into symbols

await vs return vs return await

When writing async functions, there are differences between await vs return vs return await, and picking the right one is important. Let's start with this async function: async function waitAndMaybeReject() { // Wait one second await new Promise(r => setTimeout(r, 1000)); // Toss a coin const isHeads = Boolean(Math.round(Math.random())); if (isHeads) return 'yay'; throw Error('Boo!'); } This returns a promise that waits a second, then has a 50/50 chance of fulfilling with "yay" or rejecting with an error. Let's use it in a few subtlety different ways: Just calling async function foo() { try { waitAndMaybeReject(); } catch (e) { return 'caught'; } } Here, if you call foo, the returned promise will always fulfill with undefined, without waiting. Since we don't await or return the result of waitAndMaybeReject(), we don't react to it in any way. Code like this is usually a mistake. Awaiting async function foo() { try { await waitAndMaybeReject(); } catch (e) { return 'caught'; } } Here, if you call foo, the returned promise will always wait one second, then either fulfill with undefined, or fulfill with "caught". Because we await the result of waitAndMaybeReject(), its rejection will be turned into a throw, and our catch block will execute. However, if waitAndMaybeReject() fulfills, we don't do anything with the value. Returning async function foo() { try { return waitAndMaybeReject(); } catch (e) { return 'caught'; } } Here, if you call foo, the returned promise will always wait one second, then either fulfill with "yay", or reject with Error('Boo!'). By returning waitAndMaybeReject(), we're deferring to its result, so our catch block never runs. Return-awaiting The thing you want in try/catch blocks, is return await: async function foo() { try { return await waitAndMaybeReject(); } catch (e) { return 'caught'; } } Here, if you call foo, the returned promise will always wait one second, then either fulfill with "yay", or fulfill with "caught". Because we await the result of waitAndMaybeReject(), its rejection will be turned into a throw, and our catch block will execute. If waitAndMaybeReject() fulfills, we return its result. If the above seems confusing, it might be easier to think of it as two separate steps: async function foo() { try { // Wait for the result of waitAndMaybeReject() to settle, // and assign the fulfilled value to fulfilledValue: const fulfilledValue = await waitAndMaybeReject(); // If the result of waitAndMaybeReject() rejects, our code // throws, and we jump to the catch block. // Otherwise, this block continues to run: return fulfilledValue; } catch (e) { return 'caught'; } } Note: Outside of try/catch blocks, return await is redundant. There's even an ESLint rule to detect it, but it allows it in try/catch.

Netflix functions without client-side React, and it's a good thing

A few days ago Netflix tweeted that they'd removed client-side React.js from their landing page and they saw a 50% performance improvement. It caused a bit of a stir. This shouldn't be a surprise The following: Download HTML & CSS in parallel. Wait for CSS to finish downloading & execute it. Render, and continue rendering as HTML downloads. …is always going to be faster than: Download HTML (it's tiny). Download CSS & JS in parallel. Wait for CSS to finish downloading & execute it. Wait for JS to finish downloading & execute it. (In many cases, SPAs wait until this point to start downloading data). Update the DOM & render. …to achieve the same result. Netflix switched from the second pattern to the first, so the headline boils down to: less code was executed and stuff got faster. When the PS4 was released in 2013, one of its advertised features was progressive downloading – allowing gamers to start playing a game while it's downloading. Although this was a breakthrough for consoles, the web has been doing this for 20 years. The HTML spec (warning: 8mb document), despite its size, starts rendering once ~20k is fetched. Unfortunately, it's a feature we often engineer-away with single page apps, by channelling everything through a medium that isn't streaming-friendly, such as a large JS bundle. Do a little with a little I used to joke about Photoshop, where you'd be shown this: Once this appeared, it was time to go for a coffee, go for a run, practice juggling, perhaps even acknowledge the humans around you, because Photoshop was busy doing some serious behind-the-scenes work and there was nothing you could do about it. A splash screen is a gravestone commemorating the death of an app's performance. Adobe even used that space to list all of those responsible. But what did you get once all the loading completed? That. It looks like the whole app, but I can't use the whole app right now. My first interaction here is pretty limited. I can create a blank canvas, or open an existing image. Those two interactions don't justify a lot of up-front loading. Rather than copying bad examples from the history of native apps, where everything is delivered in one big lump, we should be doing a little with a little, then getting a little more and doing a little more, repeating until complete. Think about the things users are going to do when they first arrive, and deliver that. Especially consider those most-likely to arrive with empty caches. Webpack's super-smart code-splitting allows you to throw more engineering at the problem by splitting out code that isn't needed for the most-likely first interaction, but sometimes there's a simpler opportunity. If your first interaction is visual, such as reading an article or looking at an image, serve HTML for those things. Most frameworks now offer some sort of server-rendering feature – just ensure you're not serving up a load of buttons that don't work while the client-side JS is loading. If the majority of your functionality is behind a log-in, take advantage of it. Most login systems can be implemented without JavaScript, such as a simple form or a link. While the user is distracted with this important and useful functionality, you can start fetching and preparing what they need next. This is what Netflix did. It shouldn't end with the first interaction either. You can lazily-load and execute code for discrete interactions, loading them in the order the user is most-likely to need them. <link rel=preload> can be used to lazily-load without executing, and the new import() function can be used to execute the code when needed. Again, webpack can help with the splitting if you're currently bundling. If you're loading a JSON object containing 100 pieces of data, could that be streamed in a way that lets you display results as they arrived? Something like newline-delimited JSON can help here. Once you're prepared for logged-in interactions, you can cache all that, and serve it from a service worker for future visits. If you update your code, that can be downloaded in parallel with the user using the current version, and you can transition the user to the new version in the least-disruptive way you can manage. I'm a fan of progressive enhancement as it puts you in this mindset. Continually do as much as you can with what you've got. This is good news for React Some folks in the React community were pretty angry about Netflix's tweet, but I struggle to read it as a bad story for React. Frameworks are an abstraction. They make a subset of tasks easier to achieve, in a way that's familiar to other users of the framework. The cost is performance – the overhead of the framework. The key is making the cost worth it. I have a reputation for being against frameworks, but I'm only against unnecessary usage, where the continual cost to users outweighs the benefit to developers. But "only use a framework if you need to" is easier said than done. You need to decide which framework to use, if any, at the start of a project, when you're least-equipped to make that decision. If you start without a framework, then realise that was a mistake, fixing it is hard. Unless you catch it early enough, you can end up with lots of independent bits of script of varying quality, with no control over their scheduling, and duplication throughout. Untangling all that is painful. Similarly, it's often difficult to remove a framework it turns out you didn't need. The framework 'owns' the project end-to-end. Framework buy-in can leave you with a lot of simple buttons and links that now have a framework dependency, so undoing all that can involve a rewrite. However, this wasn't the case for Netflix. Netflix uses React on the client and server, but they identified that the client-side portion wasn't needed for the first interaction, so they leaned on what the browser can already do, and deferred client-side React. The story isn't that they're abandoning React, it's that they're able to defer it on the client until it's was needed. React folks should be championing this as a feature. Netflix has shown you could start with React on the server, then activate the client side parts if you need them, when you need them, and where you need them. It's kinda the best of both worlds.

Lazy async SVG rasterisation

Phwoar I love a good sciency-sounding title. SVG can be slow When transforming an SVG image, browsers try to render on every frame to keep the image as sharp as possible. Unfortunately SVG rendering can be slow, especially for non-trivial images. Here's a demo, press "Scale SVG". Devtools timeline for SVG animation Sure, this is a pretty complex SVG, but we're 10x over our frame budget on some frames, and as a result the animation looks awful. And this is on a powerful MacBook. Things are less bad with a simpler SVG. Here's an example with the Firefox logo, but it's still pretty janky. However, a new API gives us more control: Rasterising SVG lazily createImageBitmap(imgElement).then((imageBitmap) => { // … }); createImageBitmap can rasterise many different image into bitmap data, which can be drawn onto a canvas element. However, in Chrome 61+, with chrome://flags/#enable-experimental-canvas-features enabled, it can take an HTML image element for an SVG image, and rasterise it asynchronously, off the main thread, so it doesn't disrupt animations. You can also render part of the SVG, and output it at a particular size: createImageBitmap( imgElement, sourceCropX, sourceCropY, sourceCropWidth, sourceCropHeight, {resizeWidth, resizeHeight} ).then(imageBitmap => …); The allows me to perform a really cheap bitmap zoom of the SVG using a canvas, while rendering a cropped-but-sharp version in parallel. Once the sharp version is ready, I can include it in the animation. Here's a demo, press "Scale canvas". Requires Chrome 61+ with chrome://flags/#enable-experimental-canvas-features enabled. Devtools timeline for canvas animation This method is much friendlier to the CPU, and the animation is fluid: SVG animation vs SVG-in-canvas For the complex car SVG, the sharp image appears towards the end of the image. With the Firefox logo, the sharp version appears much earlier because it take less time to render. Here's the code for the demo. Chunking up rasterisation As you can see from the timeline above, Chrome still skips a frame as it uploads the sharper texture to the GPU. This could be solved by chunking the work into smaller tiles, so the GPU-upload doesn't blow the frame budget. OpenSeadragon is designed to dynamically load image tiles to create a zoomable image. It's very much geared towards getting bitmap data from the network, but with a bit of hacking… Zoomable lazy-rendered tiled SVG. Requires Chrome 61+ with chrome://flags/#enable-experimental-canvas-features enabled. Yeah, it's a little rough around the edges. Like I said, it's a hack. But I'm really excited that this level of control over SVG painting is coming to the web.

HTTP/2 push is tougher than I thought

"HTTP/2 push will solve that" is something I've heard a lot when it comes to page load performance problems, but I didn't know much about it, so I decided to dig in. HTTP/2 push is more complicated and low-level than I initially thought, but what really caught me off-guard is how inconsistent it is between browsers – I'd assumed it was a done deal & totally ready for production. This isn't an "HTTP/2 push is a douchebag" hatchet job – I think HTTP/2 push is really powerful and will improve over time, but I no longer think it's a silver bullet from a golden gun. Map of fetching Between your page and the destination server there's a series of caches & things that can intercept the request: Service workerHTTP cacheServerPageImage cachePreload cache PageImage cachePreload cache Push cache HTTP/2 connection Push cache HTTP/2 connection The above is probably like those flow diagrams people use to try and explain Git or observables – they're reassuring to someone who already knows the thing, but terrifying to others. If that's the case, sorry! Hopefully the next few sections will help. How HTTP/2 push works Page: Hey example.com, can I have your homepage please? 10:24 Server: Sure thing! Oh, but while I'm sending you that, here's a stylesheet, some images, some JavaScript, and some JSON. 10:24 Page: Uh, sure. 10:24 Page: I'm just reading the HTML here, and it looks like I'm going to need a stylesh… oh it's the one you're already sending me, cool! 10:25 When the server responds to a request it can include additional resources. This includes a set of request headers, so the browser knows how to match it up later. They sit in a cache until the browser asks for a resource that matches its description. You get a performance boost because you start sending the resources without waiting for the browser to ask for them. In theory, this means the page loads faster. This is pretty much all I knew about HTTP/2 push for years, and it sounded relatively simple, but the devil is in the details… Anything can use the push cache HTTP/2 push is a low-level networking feature – anything that uses the networking stack can make use of it. The key to it being useful is consistency and predictability. I gave this a spin by pushing resources and trying to collect them with: fetch() XMLHttpRequest <link rel="stylesheet" href="…"> <script src="…"> <iframe src="…"> I also slowed the delivery of the body of the pushed resources to see if browsers would match items that were still being pushed. The fairly scrappy test suite is on github. Chrome - good support Safari - bad support Firefox - good support Edge - some support Edge didn't retrieve the item from the push cache when using fetch(), XMLHttpRequest, or <iframe> (issue, including video). Safari is a weird one. When it will/won't use the push cache seems like a flip of a coin. Safari defers to OSX's network stack, which is closed-source, but I think some of the bugs are in Safari-land. It seems like it opens too many connections and pushed items end up being distributed between them. This means you only get a cache hit if the request is lucky enough to use same connection – but it's really above my brain-grade (issue, including video). All browsers (except Safari when it's being weird) will use matching pushed items even if they're still in-progress of being pushed. That's pretty good. Unfortunately, Chrome is the only browser with devtools support. The network panel will tell you which items have been fetched from the push cache. Recommendations If the browser won't retrieve the item from the push cache, you'll end up being slower than if you hadn't pushed it at all. Edge's support is poor, but at least it's consistently poor. You could use user-agent sniffing to ensure you only push resources you know it'll use. If that isn't possible for whatever reason, it's probably safer to avoiding pushing anything to Edge users. Safari's behaviour doesn't appear to be deterministic, so it isn't something you can hack around. Use user-agent sniffing to avoid pushing resources to Safari users. You can push no-cache and no-store resources With the HTTP cache, an item must have something like a max-age to allow the browser to use it without server revalidation (here's a post on caching headers). HTTP/2 push is different – an item's "freshness" isn't checked when matching items. Chrome - good support Safari - good support Firefox - good support Edge - good support All browsers behave this way. Recommendations Some single-page apps suffer in performance because they're not only render-blocked by JS, but also by some data (JSON or whatever) the JS starts fetching once it executes. Server rendering is the best solution here, but if that isn't possible you can push the JS and the JSON along with the page. However, given the Edge/Safari issues mentioned earlier, inlining the JSON is more reliable. The HTTP/2 push cache is the last cache the browser checks Pushed items sit with the HTTP/2 connection meaning the browser will only use pushed items if nothing before it provides a response. This includes the image cache, the preload cache, the service worker, and the HTTP cache. Chrome - good support Safari - good support Firefox - good support Edge - good support All browsers behave this way. Recommendations Just be aware of it. For instance, if you have a matching item in the HTTP cache that's fresh according to its max-age, and you push an item that's fresher, the pushed item will be ignored in favour of the older item in the HTTP cache (unless the API bypasses the HTTP cache for whatever reason). Being last in the chain isn't really a problem, but knowing cached items sit with the connection helped me understand a lot of other behaviours I saw. For instance… If the connection closes, bye bye push cache The push cache sits with the HTTP/2 connection, so you lose it if the connection closes. This happens even if a pushed resource is highly cacheable. The push cache sits beyond the HTTP cache, so items don't enter the HTTP cache until the browser requests them. At that point they're pulled out of the push cache, through the HTTP cache, the service worker etc etc, and into the page. If the user is on a flaky connection, you may successfully push something, but lose the connection before the page manages to get it. This means they'll have to set up a new connection and re-download the resource. Chrome - good support Safari - good support Firefox - good support Edge - good support All browsers behave this way. Recommendations Don't rely on items hanging around in the push cache for a long time. Push is best-used for urgent resources, so there shouldn't be much time between pushing a resource and the page picking it up. Multiple pages can use the same HTTP/2 connection Each connection has its own push cache, but multiple pages can use a single connection, meaning multiple pages may share a push cache. In practice, this means if you push a resource along with a navigation response (eg an HTML page), it isn't exclusively available to that page (I'm going to use "pages" throughout the rest of this post, but in reality this includes other contexts that can fetch resources, such as workers). Chrome - good support Safari - bad support Firefox - good support Edge - some support Edge seems to use a new connection per tab (issue, including video). Safari creates multiple connections to the same origin unnecessarily. I'm pretty sure this is the root its weirdness (issue, including video). Recommendations Watch out for this when you're pushing stuff like JSON data along with a page – you can't rely on the same page picking it up. This behaviour can become an advantage, as resources you push along with a page can be picked up by requests made from an installing service worker. Edge's behaviour isn't optimal, but it isn't anything to worry about right now. Once Edge has service worker support, it could become an issue. Again, I would avoid pushing resources for Safari users. Requests without credentials use a separate connection "Credentials" are going to pop up a few times in this article. Credentials are things the browser sends that identify a particular user. This generally means cookies, but can also mean HTTP basic auth and connection-level identifiers like client certificates. If you think of an HTTP/2 connection like a single phone call, once you introduce yourself the call is no longer anonymous, and that includes anything you said prior to introducing yourself. For privacy reasons, the browser sets up a separate 'call' for "anonymous" requests. However, because the push cache sits with connection, you can end up missing cached items by making non-credentialed requests. For instance, if you push a resource along with a page (a credentialed request), then fetch() it (non-credentialed), it will set up a new connection and miss the pushed item. If a cross-origin stylesheet (credentialed) pushes a font, the browser's font request (non-credentialed) will miss the font in the push cache. Recommendations Ensure your requests use the same credentials mode. In most cases this means ensuring your requests include credentials, as your page request is always made with credentials. To fetch with credentials, use: fetch(url, { credentials: 'include' }); You can't add credentials to a cross-origin font request, but you can remove them from the stylesheet: <link rel="stylesheet" href="…" crossorigin /> …this means both the stylesheet & font request will go down the same connection. However, if that stylesheet also applies background images, those requests are always credentialed, so you'll end up with another connection again. The only solution here is a service worker, which can change how the fetch is performed per request. I've heard developers say that non-credentialed requests are better for performance as they don't need to send cookies, but you have to weigh this against the much greater cost of setting up a new connection. Also, HTTP/2 can compress-away headers repeated between requests, so cookies aren't really an issue. Maybe we should change the rules Edge is the only browser that doesn't follow the rules here. It allows credentialed & non-credentialed requests to share a connection. However, I've skipped the usual row of browser icons as I'd like to see the spec changed here. If a page makes a non-credentialed request to its origin, there's little point setting up a separate connection. A credentialed resource initiated the request, so it could add its credentials to the "anonymous" request through the URL. I'm less sure about other cases, but due to browser fingerprinting there isn't much in the way of anonymity if you're making credentialed & non-credentialed requests to the same server. If you want to dig deeper on this there's discussion on GitHub, a Mozilla mailing list, and Firefox's bug tracker. Phew. That was a bit jargon-heavy. Sorry about that. Items in the push cache can only be used once Once the browser uses something in the push cache, it's removed. It may end up in the HTTP cache (depending on caching headers), but it's no longer in the push cache. Chrome - good support Safari - bad support Firefox - good support Edge - good support Safari suffers from race conditions here. If a resource is fetched multiple times while it's pushing, it'll get the pushed item multiple times (issue, including video). If it's fetched twice after the item has finished pushing, it behaves correctly – the first will return from the push cache, whereas the second won't. Recommendations If you decide to push stuff to Safari users, be aware of this bug when you're pushing no-cache resources (eg, JSON data). Maybe pass a random ID along with the response, and if you get the same ID back twice, you know you've hit the bug. In that case, wait a second and try again. In general, use caching headers or a service worker to cache your pushed resources once they're fetched, unless caching isn't desirable (such as one-off JSON fetches). The browser can abort pushed items if it already has them When you push content, you do it without much negotiation with the client. This means you can be pushing something the browser already has in one of its caches. The HTTP/2 spec allows the browser to abort the incoming stream using the CANCEL or REFUSED_STREAM code in this case, to avoid wasting bandwidth. Chrome - some support Safari - some support Firefox - poor support Edge - good support The spec isn't strict here, so my judgements here are based on what's useful to developers. Chrome will reject pushes if it already has the item in the push cache. It rejects with PROTOCOL_ERROR rather than CANCEL or REFUSED_STREAM, but that's a minor thing (issue). Unfortunately it doesn't reject items it already has in the HTTP cache. It sounds like this is almost fixed, but I haven't been able to test it (issue). Safari will reject pushes if it already has the item in the push cache, but only if the item in the push cache is 'fresh' according to cache headers (eg max-age), unless the user hit refresh. This is different to Chrome, but I don't think it's 'wrong'. Unfortunately, like Chrome, it doesn't reject items it already has in the HTTP cache (issue). Firefox will reject pushes if it already has the item in the push cache, but then it also drops the item it already had in the push cache, leaving it with nothing! This makes it pretty unreliable, and difficult to defend against (issue, including video). Firefox also doesn't reject items it already has in the HTTP cache (issue). Edge doesn't reject pushes for items already in the push cache, but it does reject if the item is in the HTTP cache. Recommendations Unfortunately, even with perfect browser support you'll have wasted bandwidth and server I/O before you get the cancel message. Cache digests aim to solve this, by telling the server in-advance what it has cached. In the meantime, you may want to use cookies to track if you've already pushed cachable assets to the user. However, items can disappear from the HTTP cache at the browser's whim, whereas cookies persist, so the presence of the cookies doesn't mean the user still has the items in their cache. Items in the push cache should be matched using HTTP semantics, aside from freshness We've already seen that freshness is ignored when it comes to matching items in the push cache (that's how no-store and no-cache items are matched), but other matching mechanisms should be used. I tested POST requests, and Vary: Cookie. Update: The spec says pushed requests "MUST be cacheable, MUST be safe, and MUST NOT include a request body" – I missed these definitions at first. POST requests don't fall into the definition of "safe", so browsers should reject POSTs. Chrome - poor support Safari - some support Firefox - poor support Edge - poor support Chrome accepts POST push streams, but doesn't appear to use them (issue). Chrome also ignores the Vary header when matching pushed items (issue), although the issue suggests it works when using QUIC. Firefox rejects the pushed POST stream. However, Firefox ignores the Vary header when matching pushed items (issue). Edge also rejects the pushed POST stream. But also ignores the Vary header (issue). Safari, like Chrome, accepts POST push streams, but doesn't appear to use them (issue). It does obey the Vary header though, and it's the only browser to do so. Recommendations I'm kinda sad that no one but Safari observes the Vary header for pushed items. This means that you could push some JSON intended for one user, then that user logs out & another logs in, but you still get the pushed JSON for the previous user if it wasn't already collected. If you're pushing data intended for one user, also respond with the expected user ID. If it's different to what you're expecting, make the request again (as the pushed item will have gone). In Chrome, you can use the clear site data header when a user logs out. This also clears items in the push cache by terminating the HTTP/2 connections. You can push items for other origins As the owners of developers.google.com/web, we could get our server to push a response containing whatever we wanted for android.com, and set it to cache for a year. A simple fetch would be enough to drag that in the HTTP cache. Then, if our visitors went to android.com, they'd see "NATIVE SUX – PWA RULEZ" in large pink comic sans, or whatever we wanted. Of course, we wouldn't do that, we love Android. I'm just saying… Android: if you mess with the web, we'll fuck you up. Ok ok, I jest, but the above actually works. You can't push assets for any origin, but you can push assets for origins which your connection is "authoritative" for. If you look at the certificate for developers.google.com, you can see it's authoritative for all sorts of Google origins, including android.com. Viewing certificate information in Chrome Now, I lied a little, because when we fetch android.com it'll perform a DNS lookup and see that it terminates at a different IP to developers.google.com, so it'll set up a new connection and miss our item in the push cache. We could work around this using an ORIGIN frame. This lets the connection say "Hey, if you need anything from android.com, just ask me. No need to do any of that DNS stuff", as long as it's authoritative. This is useful for general connection coalescing, but it's pretty new and only supported in Firefox Nightly. If you're using a CDN or some kind of shared host, take a look at the certificate, see which origins could start pushing content for your site. It's kinda terrifying. Thankfully, no host (that I'm aware of) offers full control over HTTP/2 push, and is unlikely to thanks to this little note in the spec: Where multiple tenants share space on the same server, that server MUST ensure that tenants are not able to push representations of resources that they do not have authority over. — HTTP/2 spec That ought to do it. Chrome - good support Safari - some support Firefox - unknown support Edge - unknown support Chrome allows sites to push resources for origins it has authority over. It will reuse the connection if the other origin terminates at the same IP, so those pushed items are used. Chrome doesn't support the ORIGIN frame yet. Safari allows sites to push resources for origins it has authority over, but it sets up a new connection for other origins, so these pushed items are never used. Safari doesn't support the ORIGIN frame. Firefox rejects other-origin pushes. Like Safari, it sets up a new connection for other origins. However, I'm bypassing certificate warnings in Firefox, so I'm not confident in my results. Firefox Nightly supports the ORIGIN frame. Edge also rejects other-origins pushes. Again, I'm bypassing certificate warnings, so these results may be different with a proper certificate. Edge doesn't support the ORIGIN frame. Recommendations If you make use of multiple origins on the same page that end up using the same server, start looking into the ORIGIN frame. Once it's supported it removes the need for a DNS lookup, improving performance. If you think you'd benefit from cross-origin pushes, write some better tests than I did & ensure browsers will actually use what you pushed. Otherwise, use user-agent sniffing to push to particular browsers. Push vs preload Instead of pushing resources, you can ask the browser to preload them using HTML: <link rel="preload" href="https://fonts.example.com/font.woff2" as="font" crossorigin type="font/woff2" /> Or a page header: Link: <https://fonts.example.com/font.woff2>; rel=preload; as=font; crossorigin; type='font/woff2' href – the URL to preload as – the destination of the response. This means the browser can set the right headers and apply the correct CSP policies. crossorigin – Optional. Indicates that the request should be a CORS request. The CORS request will be sent without credentials unless crossorigin="use-credentials". type – Optional. Allows the browser to ignore the preload if the provided MIME type is unsupported. Once the browser sees a preload link, it fetches it. The functionality is similar to HTTP/2 push, in that: Anything can be preloaded. no-cache & no-store items can be preloaded. Your request will only match a preloaded item if its credentials mode is the same. Cached items can only be used once, although they may be in the HTTP cache for future fetches. Items should be matched using HTTP semantics, aside from freshness. You can preload items from other origins. But also different: The browser fetches the resource, meaning it will look for responses from the service worker, HTTP cache, HTTP/2 cache, or destination server – in that order. Preloaded resources are stored alongside the page (or worker). This makes it one of the first caches the browser will check (before the service worker and HTTP cache), and losing a connection doesn't lose you preloaded items. The direct link to the page also means devtools can show a useful warning if preloaded items aren't used. Each page has its own preload cache, so it's kinda pointless to preload things intended for another page. As in, you can't preload items intended for use after a page load. It's also pointless to preload stuff from a page for use in a service worker install – the service worker won't check the page's preload cache. Chrome - some support Safari - some support Firefox - no support Edge - no support Chrome doesn't support preloading with all APIs. For instance, fetch() doesn't use the preload cache. XHR will, but only if it's sent with credentials (issue). Safari only supports preloading in its latest technology preview. fetch() doesn't use the preload cache, and neither does XHR (issue). Firefox doesn't support preloading, but their implementation is in progress (issue). Edge doesn't support preloading. Give it an upvote if you want. Recommendations The perfect preload will always be slightly slower than the perfect HTTP/2 push, since it doesn't need to wait for the browser to make the request. However, preloading is drastically simpler and easier to debug. I recommend using it today, as browser support is only going to get better – but do keep an eye on devtools to ensure your pushed items are being used. Some services will turn preload headers into HTTP/2 pushes. I think this is a mistake given how subtly different the behaviour is between the two, but it's probably something we're going to have to live with for a while. However, you should ensure these services strip the header from the final response, else you may end up with a race conditions where the preload happens before the push, resulting in double the bandwidth usage. The future of push There are some pretty gnarly bugs around HTTP/2 push right now, but once those are fixed I think it becomes ideal for the kinds of assets we currently inline, especially render-critical CSS. Once cache digests land, we'll hopefully get the benefit of inlining, but also the benefit of caching. Getting this right will depend on smarter servers which allow us to correctly prioritise the streaming of content. Eg, I want to be able to stream my critical CSS in parallel with the head of the page, but then give full priority to the CSS, as it's a waste spending bandwidth on body content that the user can't yet render. If your server is a little slow to respond (due to expensive database lookups or whatever), you could also fill that time pushing assets the page will likely need, then change the priorities once the page is available. Like I said, this post isn't a hatchet job and I hope it doesn't come across as one. HTTP/2 push can improve performance, just don't use it without careful testing, else you might be making things slower. Thanks to Gray Norton, Addy Osmani, and Surma for proofreading and giving feedback.

ECMAScript modules in browsers

ES modules are now available in browsers! They're in… Safari 10.1. Chrome 61. Firefox 60. Edge 16. <script type="module"> import {addTextToBody} from './utils.mjs'; addTextToBody('Modules are pretty cool.'); </script> // utils.mjs export function addTextToBody(text) { const div = document.createElement('div'); div.textContent = text; document.body.appendChild(div); } Live demo. All you need is type=module on the script element, and the browser will treat the inline or external script as an ECMAScript module. There are already some great articles on modules, but I wanted to share a few browser-specific things I'd learned while testing & reading the spec: "Bare" import specifiers aren't currently supported // Supported: import {foo} from 'https://jakearchibald.com/utils/bar.mjs'; import {foo} from '/utils/bar.mjs'; import {foo} from './bar.mjs'; import {foo} from '../bar.mjs'; // Not supported: import {foo} from 'bar.mjs'; import {foo} from 'utils/bar.mjs'; Valid module specifiers must match one of the following: A full non-relative URL. As in, it doesn't throw an error when put through new URL(moduleSpecifier). Starts with /. Starts with ./. Starts with ../. Other specifiers are reserved for future-use, such as importing built-in modules. nomodule for backwards compatibility <script type="module" src="module.mjs"></script> <script nomodule src="fallback.js"></script> Live demo. Browsers that understand type=module should ignore scripts with a nomodule attribute. This means you can serve a module tree to module-supporting browsers while providing a fall-back to other browsers. Browser issues Firefox doesn't support nomodule (issue). Fixed in Firefox nightly! Edge doesn't support nomodule (issue). Fixed in Edge 16! Safari 10.1 doesn't support nomodule. Fixed in Safari 11! For 10.1, there's a pretty smart workaround. Defer by default <!-- This script will execute after… --> <script type="module" src="1.mjs"></script> <!-- …this script… --> <script src="2.js"></script> <!-- …but before this script. --> <script defer src="3.js"></script> Live demo. The order should be 2.js, 1.mjs, 3.js. The way scripts block the HTML parser during fetching is baaaad. With regular scripts you can use defer to prevent blocking, which also delays script execution until the document has finished parsing, and maintains execution order with other deferred scripts. Module scripts behave like defer by default – there's no way to make a module script block the HTML parser while it fetches. Module scripts use the same execution queue as regular scripts using defer. Inline scripts are also deferred <!-- This script will execute after… --> <script type="module"> addTextToBody("Inline module executed"); </script> <!-- …this script… --> <script src="1.js"></script> <!-- …and this script… --> <script defer> addTextToBody("Inline script executed"); </script> <!-- …but before this script. --> <script defer src="2.js"></script> Live demo. The order should be 1.js, inline script, inline module, 2.js. Regular inline scripts ignore defer whereas inline module scripts are always deferred, whether they import anything or not. Async works on external & inline modules <!-- This executes as soon as its imports have fetched --> <script async type="module"> import {addTextToBody} from './utils.mjs'; addTextToBody('Inline module executed.'); </script> <!-- This executes as soon as it & its imports have fetched --> <script async type="module" src="1.mjs"></script> Live demo. The fast-downloading scripts should execute before the slow ones. As with regular scripts, async causes the script to download without blocking the HTML parser and executes as soon as possible. Unlike regular scripts, async also works on inline modules. As always with async, scripts may not execute in the order they appear in the DOM. Browser issues Firefox doesn't support async on inline module scripts (issue). Fixed in Firefox 59! Modules only execute once <!-- 1.mjs only executes once --> <script type="module" src="1.mjs"></script> <script type="module" src="1.mjs"></script> <script type="module"> import "./1.mjs"; </script> <!-- Whereas classic scripts execute multiple times --> <script src="2.js"></script> <script src="2.js"></script> Live demo. If you understand ES modules, you'll know you can import them multiple times but they'll only execute once. Well, the same applies to script modules in HTML – a module script of a particular URL will only execute once per page. Browser issues Edge executes modules multiple times (issue). Fixed in Edge 17! Always CORS <!-- This will not execute, as it fails a CORS check --> <script type="module" src="https://….now.sh/no-cors"></script> <!-- This will not execute, as one of its imports fails a CORS check --> <script type="module"> import 'https://….now.sh/no-cors'; addTextToBody("This will not execute."); </script> <!-- This will execute as it passes CORS checks --> <script type="module" src="https://….now.sh/cors"></script> Live demo. Unlike regular scripts, module scripts (and their imports) are fetched with CORS. This means cross-origin module scripts must return valid CORS headers such as Access-Control-Allow-Origin: *. Browser issues Firefox fails to load the demo page (issue). Fixed in Firefox 59! Edge loads module scripts without CORS headers (issue). Fixed in Edge 16! Credentials by default Live demo. Most CORS-based APIs will send credentials (cookies etc) if the request is to the same origin, but for a while fetch() and module scripts were exceptions. However, that all changed, and now fetch() and module scripts behave the same as other CORS-based APIs. However, that means you'll encounter three exciting varieties of browser support: Old versions of browsers that, against the spec at the time, sent credentials by to same-origin URLs by default. Browsers that followed the spec at the time, and did not send credentials to same-origin URLs by default. New browsers that follow the new spec, and send credentials to same-origin URLs by default. If you hit this issue, you can add the crossorigin attribute, which will add credentials to same-origin requests, but not cross-origin request, in any browser that follows the old spec. It doesn't do anything if the browser follows the new spec, so it's safe to use. <!-- Fetched with credentials (cookies etc) --> <script src="1.js"></script> <!-- Fetched with credentials, except in old browsers that follow the old spec --> <script type="module" src="1.mjs"></script> <!-- Fetched with credentials, in browsers that follow the old & new spec --> <script type="module" crossorigin src="1.mjs"></script> <!-- Fetched without credentials --> <script type="module" crossorigin src="https://other-origin/1.mjs"></script> <!-- Fetched with credentials--> <script type="module" crossorigin="use-credentials" src="https://other-origin/1.mjs"></script> Mime-types Unlike regular scripts, modules scripts must be served with one of the valid JavaScript MIME types else they won't execute. The HTML Standard recommends text/javascript. Live demo. Browser issues Edge executes scripts with invalid MIME types (issue). And that's what I've learned so far. Needless to say I'm really excited about ES modules landing in browsers! Performance recommendations, dynamic import & more! Check out the article on Web Fundamentals for a deep-dive into module usage.

Combining fonts

@font-face { font-family: 'Just Another Hand'; font-style: normal; font-weight: 400; src: local('Just Another Hand'), local('JustAnotherHand-Regular'), url(https://fonts.gstatic.com/s/justanotherhand/v7/fKV8XYuRNNagXr38eqbRf2bHIGFY9zRy9KAPVD43QdU.woff2) format('woff2'), url(https://fonts.gstatic.com/s/justanotherhand/v7/fKV8XYuRNNagXr38eqbRf8-ortBJrX8dG4H9Ox7zsWc.woff) format('woff'); } @font-face { font-family: 'Just Another Hand Fixed'; font-style: normal; font-weight: 400; src: local('Just Another Hand'), local('JustAnotherHand-Regular'), url(https://fonts.gstatic.com/s/justanotherhand/v7/fKV8XYuRNNagXr38eqbRf2bHIGFY9zRy9KAPVD43QdU.woff2) format('woff2'), url(https://fonts.gstatic.com/s/justanotherhand/v7/fKV8XYuRNNagXr38eqbRf8-ortBJrX8dG4H9Ox7zsWc.woff) format('woff'); } @font-face { font-family: "Just Another Hand Fixed"; src: local('Architects Daughter'), local('ArchitectsDaughter'), url(https://fonts.gstatic.com/l/font?kit=RXTgOOQ9AAtaVOHxx0IUBDrRnkt4rmSI6qS07ibfugwWYsZpeeE_lScv_WSQELyh&skey=d34ee9a1a308e98b&v=v6) format('woff2'), url(https://fonts.gstatic.com/l/font?kit=RXTgOOQ9AAtaVOHxx0IUBAgVrDCMKncvdxTxFP2hhz8WYsZpeeE_lScv_WSQELyh&skey=d34ee9a1a308e98b&v=v6) format('woff'); unicode-range: U+2d, U+3d; } .jah-demo, .jah-fixed-demo { font: normal 2.5rem/1 "Just Another Hand"; margin: 1.3rem 0 0.9rem; text-align: center; } @media (min-width: 435px) { .jah-demo, .jah-fixed-demo { font: normal 3.5rem/1 "Just Another Hand"; margin: 1.5rem 0 0.9rem; } } .jah-fixed-demo { font-family: "Just Another Hand Fixed"; } I love the font Just Another Hand, I use it a lot in diagrams during my talks: Here it is! Yay! The thing is, I don't like the positioning of the hyphen & equals glyphs… Cache-Control: max-age=3600 They look awkwardly positioned – they sit too high. Thankfully CSS lets you merge fonts together, so I can create a single font family that's like Just Another Hand, except it takes the hyphen & equals glyphs from a different font, Architects Daughter: Cache-Control: max-age=3600 How it works The @font-face is defined as usual: @font-face { font-family: 'Just Another Hand Fixed'; font-style: normal; font-weight: 400; src: local('Just Another Hand'), local('JustAnotherHand-Regular'), url('https://fonts.gstatic.com/…woff2') format('woff2'), url('https://fonts.gstatic.com/…woff') format('woff'); } But I added another @font-face of the same name for the hyphen & equals glyphs: @font-face { font-family: 'Just Another Hand Fixed'; src: local('Architects Daughter'), local('ArchitectsDaughter'), url('https://fonts.gstatic.com/l/…') format('woff2'), url('https://fonts.gstatic.com/l/…') format('woff'); unicode-range: U+2d, U+3d; } The trick is in the unicode-range descriptor. It indicates that the src should only be used for the hyphen (U+2d) and equals (U+3d) code points. You can turn a unicode character into a code point using this snippet: '='.codePointAt(0).toString(16); // "3d" As an optimisation, I used Google fonts' text parameter to subset the "Architects Daughter" font, so it only contains the hyphen & equals glyphs. The woff2 version is 500 bytes – not bad! And that's it. Now when I use: .whatever { font-family: 'Just Another Hand Fixed'; } …it uses a combination of both fonts! Update: Do you need unicode-range? A couple of people on Twitter and in the comments have suggested you don't need unicode-range, and you can just do: /* Subsetted font */ @font-face { font-family: 'Just Another Hand Fixed'; src: url('https://fonts.gstatic.com/l/…') format('woff2') …; } /* Main font */ @font-face { font-family: 'Just Another Hand Fixed'; src: url('https://fonts.gstatic.com/…woff2') format('woff2') …; } This works visually, but it's worse in terms of performance. In this case the browser downloads the subsetted font first, then it realises it doesn't have all the glyphs it needs, so it starts downloading the main font. The fonts download one after the other. Live demo. Network waterfall. Whereas with the unicode-range solution, the browser knows what it needs in advance, so it can download the fonts in parallel. Live demo. Network waterfall. Also, if you don't use one of the subsetted characters, the browser knows it doesn't need to download the font at all! Live demo. Network waterfall.

Async iterators and generators

Streaming fetches are supported in Chrome, Edge, and Safari, and they look a little like this: async function getResponseSize(url) { const response = await fetch(url); const reader = response.body.getReader(); let total = 0; while (true) { const {done, value} = await reader.read(); if (done) return total; total += value.length; } } This code is pretty readable thanks to async functions (here's a tutorial if you're unfamiliar with those), but it's still a little clumsy. Thankfully, async iterators are arriving soon, which makes it much neater: async function getResponseSize(url) { const response = await fetch(url); let total = 0; for await (const chunk of response.body) { total += chunk.length; } return total; } Async iterators are available in Chrome Canary if you launch it with the flag --js-flags=--harmony-async-iteration. Here's how they work, and how we can use them to make streams iterate… Async iterators Async iterators work pretty much the same as regular iterators, but they involve promises: async function example() { // Regular iterator: const iterator = createNumberIterator(); iterator.next(); // Object {value: 1, done: false} iterator.next(); // Object {value: 2, done: false} iterator.next(); // Object {value: 3, done: false} iterator.next(); // Object {value: undefined, done: true} // Async iterator: const asyncIterator = createAsyncNumberIterator(); const p = asyncIterator.next(); // Promise await p; // Object {value: 1, done: false} await asyncIterator.next(); // Object {value: 2, done: false} await asyncIterator.next(); // Object {value: 3, done: false} await asyncIterator.next(); // Object {value: undefined, done: true} } Both types of iterator have a .return() method, which tells the iterator to end early, and do any clean-up it needs to do. Iterators & loops It's fairly uncommon to use iterator objects directly, instead we use the appropriate for loop, which uses the iterator object behind-the-scenes: async function example() { // Regular iterator: for (const item of thing) { // … } // Async iterator: for await (const item of asyncThing) { // … } } The for-of loop will get its iterator by calling thing[Symbol.iterator]. Whereas the for-await loop will get its iterator by calling asyncThing[Symbol.asyncIterator] if it's defined, otherwise it will fall back to asyncThing[Symbol.iterator]. For-await will give you each value once asyncIterator.next() resolves. Because this involves awaiting promises, other things can happen on the main thread during iteration. asyncIterator.next() isn't called for the next item until your current iteration is complete. This means you'll always get the items in order, and iterations of your loop won't overlap. It's pretty cool that for-await falls back to Symbol.iterator. It means you can use it with regular iterables like arrays: async function example() { const arrayOfFetchPromises = [ fetch('1.txt'), fetch('2.txt'), fetch('3.txt') ]; // Regular iterator: for (const item of arrayOfFetchPromises) { console.log(item); // Logs a promise } // Async iterator: for await (const item of arrayOfFetchPromises) { console.log(item); // Logs a response } } In this case, for-await takes each item from the array and waits for it to resolve. You'll get the first response even if the second response isn't ready yet, but you'll always get the responses in the correct order. Async generators: Creating your own async iterator Just as you can use generators to create iterator factories, you can use async generators to create async iterator factories. Async generators a mixture of async functions and generators. Let's say we wanted to create an iterator that returned random numbers, but those random numbers came from a web service: // Note the * after "function" async function* asyncRandomNumbers() { // This is a web service that returns a random number const url = 'https://www.random.org/decimal-fractions/?num=1&dec=10&col=1&format=plain&rnd=new'; while (true) { const response = await fetch(url); const text = await response.text(); yield Number(text); } } This iterator doesn't have a natural end – it'll just keep fetching numbers. Thankfully, you can use break to stop it: async function example() { for await (const number of asyncRandomNumbers()) { console.log(number); if (number > 0.95) break; } } Live demo Like regular generators, you yield values, but unlike regular generators you can await promises. Like all for-loops, you can break whenever you want. This results in the loop calling iterator.return(), which causes the generator to act as if there was a return statement after the current (or next) yield. Using a web service to get random numbers is a bit of a silly example, so let's look at something more practical… Making streams iterate Like I mentioned at the start of the article, soon you'll be able to do: async function example() { const response = await fetch(url); for await (const chunk of response.body) { // … } } …but it hasn't been spec'd yet. So, let's write our own async generator that lets us iterate over a stream! We want to: Get a lock on the stream, so nothing else can use it while we're iterating. Yield the values of the stream. Release the lock when we're done. Releasing the lock is important. If the developer breaks the loop, we want them to be able to continue to use the stream from wherever they left off. So: async function* streamAsyncIterator(stream) { // Get a lock on the stream const reader = stream.getReader(); try { while (true) { // Read from the stream const {done, value} = await reader.read(); // Exit if we're done if (done) return; // Else yield the chunk yield value; } } finally { reader.releaseLock(); } } The finally clause there is pretty important. If the user breaks out of the loop it'll cause our async generator to return after the current (or next) yield point. If this happens, we still want to release the lock on the reader, and a finally is the only thing that can execute after a return. And that's it! Now you can do: async function example() { const response = await fetch(url); for await (const chunk of streamAsyncIterator(response.body)) { // … } } Live demo Releasing the lock means you can still control the stream after the loop. Say we wanted to find the byte-position of the first J in the HTML spec… async function example() { const find = 'J'; const findCode = find.codePointAt(0); const response = await fetch('https://html.spec.whatwg.org'); let bytes = 0; for await (const chunk of streamAsyncIterator(response.body)) { const index = chunk.indexOf(findCode); if (index != -1) { bytes += index; console.log(`Found ${find} at byte ${bytes}.`); break; } bytes += chunk.length; } response.body.cancel(); } Live demo Here we break out of the loop when we find a match. Since streamAsyncIterator releases its lock on the stream, we can cancel the rest of it & save bandwidth. Note that we don't assign streamAsyncIterator to ReadableStream.prototype[Symbol.asyncIterator]. This would work – allowing us to iterate over streams directly, but it's also messing with objects we don't own. If streams become proper async iterators, we could end up with weird bugs if the spec'd behaviour is different to ours. A shorter implementation You don't need to use async generators to create async iterables, you could create the iterator object yourself. And that's what Domenic Denicola did. Here's his implementation: function streamAsyncIterator(stream) { // Get a lock on the stream: const reader = stream.getReader(); return { next() { // Stream reads already resolve with {done, value}, so // we can just call read: return reader.read(); }, return() { // Release the lock if the iterator terminates. reader.releaseLock(); return {}; }, // for-await calls this on whatever it's passed, so // iterators tend to return themselves. [Symbol.asyncIterator]() { return this; } }; } You can play with all of the above in Chrome Canary today by launching it with the flag --js-flags=--harmony-async-iteration. If you want to use them in production today, Babel can transpile them.

Do we need a new heading element? We don't know

There's a proposal to add a new <h> element to the HTML spec. It solves a fairly common use-case. Take this HTML snippet: <div class="promo"> <h2>Do you find the "plot" a distraction in movies?</h2> <p>If so, you should check out "John Wick" - satisfaction guaranteed!</p> </div> This could be a web component, or a simple include. The problem is, by using <h2>, we've assumed the 'parent' heading is an <h1>. If this snippet is moved elsewhere in the DOM, this assumption may break the heading outline. What if, instead, we could write: <section class="promo"> <h>Do you find the "plot" a distraction in movies?</h> <p>If so, you should check out "John Wick" - satisfaction guaranteed!</p> </section> …where the <h> element is contextual to its parent <section>. Now this snippet can be moved around without breaking things - the heading always represents a subsection within its parent. The structure of a document should be marked up in a nested manner, and this is mostly how HTML works: You put an <ol> within an <li> to express a list within a list. Sections & headings should work the same. This is old news The <h> idea is at least 26 years old. It can be found in an old www-talk email from 1991 (thanks to Jeremy Keith for pointing that out). It made it into the XHTML2 spec in 2004. It was also rolled into the (then-named) HTML5 spec, but applied to existing headings to maintain some backwards compatibility. <h1>Level 1 heading</h1> <section> <h1>Level 2 heading</h1> <h2>Level 3 heading</h2> <section> <h6>Level 3 heading</h6> </section> </section> The outline algorithm as defined by the HTML spec allows both the old numbered heading system to coexist with a contextual section-based system, which is a real bonus when working with existing content. For example, the posts in this blog use markdown, which uses a flat heading structure. The HTML outline meaning I can put this flat structure inside a section, and it becomes contextual to the section. Also, for user agents that don't understand sectioned headings, at least they see headings. Browsers implemented some of this. <section> is a recognised element, and browsers give <h1>s within sections a smaller font size. Unfortunately, no browser implements the outline when it comes to the accessibility tree, meaning screen readers still see an <h1> as a level 1 heading no matter how many sections it's within. This sucks. The outline was kinda the whole point. <h> to the rescue? The suggestion is that <h> would solve this, as browsers would implement it & do the right thing in terms of the accessibility tree. This is a common mistake in standards discussion - a mistake I've made many times before. You cannot compare the current state of things, beholden to reality, with a utopian implementation of some currently non-existent thing. If you're proposing something almost identical to something that failed, you better know why your proposal will succeed where the other didn't. We need evidence. And the first step is understanding what went wrong with the previous proposal. Why haven't browsers implemented the outline? I don't know. I'm trying to find out, as are people at other browsers. But here are a few guesses: The accessibility part was given low priority & no one got round to it. The outline algorithm significantly impacts performance. By the time browsers got round to it, developers were using sections incorrectly, and adding the outline would have a negative impact on users. If the reason is apathy or performance, the same applies to <h>, meaning <h> is likely to fail as hard, and in the same places. But I stress, I don't know. We have a bit of a global problem right now: confident assertion without evidence (or even despite evidence to the contrary) is valued higher than qualified uncertainty. We must rise above this in the web community. The first step is admitting what we don't know, then figuring that out before proceeding. Fixing the existing web In terms of browser & standards work, making <h> a thing involves: Spec the new element. Update the outline algorithm. Implement the new element. Implement the outline algorithm. To make <h1> perform the same function, all we need to do is: Implement the outline algorithm. And that's it - the spec & partial implementation exists already. The work needed to fix the existing web is a subset of creating a new element that does the same thing, but doesn't fix the existing web. It's possible that implementing the outline for existing heading elements will negatively impact accessibility, and there are anecdotes that point to this. However, it's also possible that, on the whole, it'd improve accessibility, as it'd make correctly-sectioned content work as the author intended. I just don't know. We need evidence. If <h> becomes a standard, there'll be a period of time where it's used, but it's unsupported in user agents. Unless it's polyfilled, this element is no better than a <span> to these users. Given that most screen reader users use headings to navigate pages, sticking with existing heading elements is likely to be less disruptive. If possible, fixing the existing web is preferable. If implementing the outline breaks more sites than it fixes, to the point where it becomes a blocker, can we fix the outline algorithm with a few tweaks? If not, can we make it opt in & switchable? <body outline="sectioned"> <h1>Level 1 heading</h1> <section> <h1>Level 2 heading</h1> <section outline="flat"> <h4>Level 4 heading</h4> </section> </section> </body> …where outline can appear on any sectioning root or sectioning content element. This is preferable to a new element, as it has some meaning to existing user agents, and plays well with existing content. If the problem is simply an apathy towards accessibility, we could expose the computed heading level in the DOM, or CSS (as proposed by Amelia BR): :heading-level(1) { /* styles */ } :heading-level(2) { /* styles */ } This would be generally useful, and may encourage browsers to implement the outline. But the important thing to admit is, we don't know. This, and most of the assertions in the Github thread, are just guesswork. We need to be better. Moving forward Before we throw a new element at the platform that may solve nothing, we need to answer the following: Why haven't browsers implemented the outline for sectioned headings? What proportion of site would get worse / improve / remain unchanged if we implement the HTML outline as-is? Can we fix any breakages with tweaks to the outline algorithm? Are there significant users of sectioned headings that would benefit from an opt-in? And we must measure the above against the likely breakages and potential failures of adding a new element. Measuring the impact of the outline algorithm isn't easy, and I don't see how it can be automated given how subjective it is. We may need to organise some kind of test where users can be presented with two heading outlines, one flat & one sectioned, and assess the quality of each for a representative set of pages. If you want to look at outlines for current pages, the W3C validator will output the heading outline both flat & sectioned. Here are the results for this page. Given that the original problem is worth solving, I really hope we can fix this. Many thanks to Steve Faulkner for a couple of valuable additions to this post, and in general for his years of work with HTML outlines.

Events and disabled form fields

I've been working on the web since I was a small child all the way through to the haggard old man I am to day. However, the web still continues to surprise me. Turns out, mouse events don't fire when the pointer is over disabled form elements, except in Firefox. Serious? Serious. Give it a go. Move the mouse from the blue area below into the disabled button: This calls setPointerCapture Disabled button Misc Mouse Touch Pointer (function() { const demoArea = document.querySelector('.demo-area'); const captureEl = document.querySelector('.capture-el'); const mouseEvents = document.querySelector('.mouse-events'); const touchEvents = document.querySelector('.touch-events'); const pointerEvents = document.querySelector('.pointer-events'); const miscEvents = document.querySelector('.misc-events'); function logEvent(event) { let col; if (event.type == 'click') { col = miscEvents; } else if (event.type.startsWith('mouse')) { col = mouseEvents; } else if (event.type.startsWith('touch')) { col = touchEvents; } else if (event.type.startsWith('pointer')) { col = pointerEvents; } log(col, event.type); } function log(col, val) { const lastLog = col.lastElementChild; if (lastLog && lastLog.querySelector('.name').textContent == val) { lastLog.querySelector('.count').textContent = Number(lastLog.querySelector('.count').textContent) + 1; return; } const div = document.createElement('div'); div.innerHTML = `${val} (1)`; col.appendChild(div); } const events = [ 'click', 'mousedown', 'mousemove', 'mouseup', 'touchstart', 'touchmove', 'touchend', 'pointerdown', 'pointermove', 'pointerup' ]; for (const type of events) { demoArea.addEventListener(type, logEvent, true); } for (const type of ['pointerdown', 'mousedown']) { captureEl.addEventListener(type, event => { const el = event.target; if (el.setPointerCapture && event.pointerId) { log(miscEvents, 'setPointerCapture'); el.setPointerCapture(event.pointerId); } if (el.setCapture) { log(miscEvents, 'setCapture'); el.setCapture(true); } }); } for (const type of ['pointerup', 'mouseup']) { captureEl.addEventListener(type, event => { const el = event.target; if (el.releasePointerCapture && event.pointerId) { log(miscEvents, 'releasePointerCapture'); el.releasePointerCapture(event.pointerId); } if (el.releaseCapture) { log(miscEvents, 'releaseCapture'); el.releaseCapture(true); } }); } }()) It's not like the disabled button element is stopping the event propagating either, it prevents capturing listeners on parent elements too. The spec says: A form control that is disabled must prevent any click events that are queued on the user interaction task source from being dispatched on the element. The HTML Spec …but it seems like most browsers are applying this behaviour to all mouse events. I imagine this is an ancient oddity, but given that Firefox doesn't do it, I hope other browsers can drop this weird behaviour. If not, it should be added to the spec (issue). This kind of thing is especially painful when implementing drag & drop, as you suddenly lose the ability to track the pointer. Touch events vs pointer events The weird disabled-element behaviour doesn't happen with touch events. I guess this is because they're a new set of events, so they were able to break away from the legacy of mouse events. Unfortunately, the weird behaviour is duplicated in pointer events. It's a little sad that this new set of events is taking on legacy behaviour from day one. However, it isn't explicitly part of the spec, so maybe it can change. I've filed an issue to see what can be done about it. Capturing pointer events With touch events, all of the touchmove and touchend events fire on the same element that received the corresponding touchstart event. Whereas with mouse/pointer events, events fire on the element under the pointer. However, pointer events allow you to switch to the touch events model: element.addEventListener('pointerdown', event => { element.setPointerCapture(event.pointerId); }); element.addEventListener('pointerup', event => { element.releasePointerCapture(event.pointerId); }); Once you do this, events will continue to fire on the captured element, even if you move across a disabled form element. This works in Edge & Chrome, although Chrome stops firing events when you move across an iframe (issue). Firefox supports a similar method, setCapture, which is an old non-standard IE API, but achieves roughly the same thing. Unfortunately these methods don't fix the problem entirely. You still lose pointerdown/pointerup events that start on a disabled form element. Workaround input[disabled], button[disabled] { pointer-events: none; } This means disabled form elements cannot be interacted with, but it also solves the event issue. Additionally, for drag & drop interactions, you may want to set pointer-events: none on all iframes during the drag interaction. This means you'll continue to get move events across the whole page.

Fun hacks for faster content

A few weeks ago I was at Heathrow airport getting a bit of work done before a flight, and I noticed something odd about the performance of GitHub: It was quicker to open links in a new window than simply click them. Here's a video I took at the time: GitHub link click vs new tab Here I click a link, then paste the same link into a fresh tab. The page in the fresh tab renders way sooner, even though it's started later. Show them what you got When you load a page, the browser takes a network stream and pipes it to the HTML parser, and the HTML parser is piped to the document. This means the page can render progressively as it's downloading. The page may be 100k, but it can render useful content after only 20k is received. This is a great, ancient browser feature, but as developers we often engineer it away. Most load-time performance advice boils down to "show them what you got" - don't hold back, don't wait until you have everything before showing the user anything. GitHub cares about performance so they server-render their pages. However, when navigating within the same tab navigation is entirely reimplemented using JavaScript. Something like… // …lots of code to reimplement browser navigation… const response = await fetch('page-data.inc'); const html = await response.text(); document.querySelector('.content').innerHTML = html; // …loads more code to reimplement browser navigation… This breaks the rule, as all of page-data.inc is downloaded before anything is done with it. The server-rendered version doesn't hoard content this way, it streams, making it faster. For GitHub's client-side render, a lot of JavaScript was written to make this slow. I'm just using GitHub as an example here - this anti-pattern is used by almost every single-page-app. Switching content in the page can have some benefits, especially if you have some heavy scripts, as you can update content without re-evaluating all that JS. But can we do that without losing streaming? I've often said that JavaScript has no access to the streaming parser, but it kinda does… Using iframes and document.write to improve performance The worst hacks involve <iframe>s, and this one uses <iframe>s and document.write(), but it does allow you to stream content to the page. It goes like this: // Create an iframe: const iframe = document.createElement('iframe'); // Put it in the document (but hidden): iframe.style.display = 'none'; document.body.appendChild(iframe); // Wait for the iframe to be ready: iframe.onload = () => { // Ignore further load events: iframe.onload = null; // Write a dummy tag: iframe.contentDocument.write('<streaming-element>'); // Get a reference to that element: const streamingElement = iframe.contentDocument.querySelector( 'streaming-element', ); // Pull it out of the iframe & into the parent document: document.body.appendChild(streamingElement); // Write some more content - this should be done async: iframe.contentDocument.write('<p>Hello!</p>'); // Keep writing content like above, and then when we're done: iframe.contentDocument.write('</streaming-element>'); iframe.contentDocument.close(); }; // Initialise the iframe iframe.src = ''; Although <p>Hello!</p> is written to the iframe, it appears in the parent document! This is because the parser maintains a stack of open elements, which newly created elements are inserted into. It doesn't matter that we moved <streaming-element>, it just works. Also, this technique processes HTML much closer to the standard page-loading parser than innerHTML. Notably, scripts will download and execute in the context of the parent document, except in Firefox where script doesn't execute at all, but I think that's a bug update: turns out scripts shouldn't be executed (thanks to Simon Pieters for pointing this out), but Edge, Safari & Chrome all do. Now we just have to stream HTML content from the server and call iframe.contentDocument.write() as each part arrives. Streaming is really efficient with fetch(), but for a sake of Safari support we'll hack it with XHR. I've built a little demo where you can compare this to what GitHub does today, and here are the results based on a 3g connection: div { position: absolute; bottom: 0; left: 0; } .timing-graph .scale > div::after { display: block; content: ''; width: 1px; height: 10px; background: #000; } .timing-graph .scale > div:last-child::after { margin-left: -1px; } .timing-graph .scale .label { position: absolute; left: 0; top: -27px; transform: translateX(-50%); } .timing-graph .result { height: 2.4rem; position: relative; display: flex; margin: 6px 0; color: #fff; } .timing-graph .result .title { position: absolute; top: 0; left: 0; bottom: 0; right: 0; display: flex; align-items: center; font: normal 1.2rem/1 sans-serif; margin: 0 10px; text-shadow: 0 1.3px 1.4px rgba(0,0,0,0.6); } .timing-graph .results { margin: 0; padding: 0; } .timing-graph .result .white-time, .timing-graph .result .shell-time { height: 100%; } .timing-graph .result, .results-key .content::before { background: #21AF63; } .timing-graph .result .white-time, .results-key .nothing::before { background: #DB4437; } .timing-graph .result .shell-time, .results-key .header::before { background: #F4B401; } .timing-graph .result::after { content: ''; position: absolute; left: 85%; top: 0; bottom: 0; background: linear-gradient(to right, #21AF63, #fff); } .timing-graph .non-visual { position: absolute; width: 0; height: 0; opacity: 0; overflow: hidden; } .results-key { display: flex; flex-flow: row wrap; justify-content: center; } .results-key > div { display: flex; align-items: center; margin: 0 0.6rem; } .results-key > div::before { content: ''; display: block; width: 1rem; height: 1rem; margin-right: 0.3rem; } .timing-graph .scale::after, .timing-graph .result::after { right: -20px; right: -20px; } @media (min-width: 530px) { .timing-graph .scale::after, .timing-graph .result::after { right: -32px; right: -32px; } } Waiting Some content rendered All avatars loaded function TimingGraph(size, majorTick, minorTick) { this.container = document.createElement('div'); this.container.className = 'timing-graph'; this.container.innerHTML = ' '; this.size = size; this.results = this.container.querySelector('.results'); var scale = this.container.querySelector('.scale'); for (var i = 0; i ' + '' + ''; var titleEl = result.querySelector('.title'); var whiteTimeEl = result.querySelector('.white-time'); var shellTimeEl = result.querySelector('.shell-time'); titleEl.innerHTML = title + ': ' + (shellTime / 1000) + ' seconds until some content rendered, ' + (contentTime / 1000) + ' seconds until all avatars have loaded'; whiteTimeEl.style.width = (shellTime/this.size) * 100 + '%'; shellTimeEl.style.width = ((contentTime - shellTime)/this.size) * 100 + '%'; this.results.appendChild(result); }; (function() { var graph = new TimingGraph(3500, 1000, 500); document.querySelector('.results-streaming-iframe').appendChild(graph.container); graph.addResult('XHR + innerHTML', 2000, 3200); graph.addResult('Streaming iframe hack', 500, 2500); }()); Raw test data. By streaming the content via the iframe, content appears 1.5 seconds sooner. The avatars also finish loading half a second sooner - streaming means the browser finds out about them earlier, so it can download them in parallel with the content. The above would work for GitHub since the server delivers HTML, but if you're using a framework that wants to manage its own representation of the DOM you'll probably run into difficulties. For that case, here's a less-good alternative: Newline-delimited JSON A lot of sites deliver their dynamic updates as JSON. Unfortunately JSON isn't a streaming-friendly format. There are streaming JSON parsers out there, but they aren't easy to use. So instead of delivering a chunk of JSON: { "Comments": [ {"author": "Alex", "body": "…"}, {"author": "Jake", "body": "…"} ] } …deliver each JSON object on a new line: {"author": "Alex", "body": "…"} {"author": "Jake", "body": "…"} This is called "newline-delimited JSON" and there's a sort-of standard for it. Writing a parser for the above is much simpler. In 2017 we'll be able to express this as a series of composable transform streams: Sometime in 2017: const response = await fetch('comments.ndjson'); const comments = response.body // From bytes to text: .pipeThrough(new TextDecoder()) // Buffer until newlines: .pipeThrough(splitStream('\n')) // Parse chunks as JSON: .pipeThrough(parseJSON()); for await (const comment of comments) { // Process each comment and add it to the page: // (via whatever template or VDOM you're using) addCommentToPage(comment); } …where splitStream and parseJSON are reusable transform streams. But in the meantime, for maximum browser compatibility we can hack it on top of XHR. Again, I've built a little demo where you can compare the two, here are the 3g results: (function() { var graph = new TimingGraph(3500, 1000, 500); document.querySelector('.results-ndjson').appendChild(graph.container); graph.addResult('XHR + innerHTML', 2000, 3200); graph.addResult('Streaming iframe hack', 500, 2500); graph.addResult('XHR + JSON', 2100, 3200); graph.addResult('XHR + ND-JSON', 600, 2500); }()); Raw test data. Versus normal JSON, ND-JSON gets content on screen 1.5 seconds sooner, although it isn't quite as fast as the iframe solution. It has to wait for a complete JSON object before it can create elements, you may run into a lack-of-streaming if your JSON objects are huge. Don't go single-page-app too soon As I mentioned above, GitHub wrote a lot of code to create this performance problem. Reimplementing navigations on the client is hard, and if you're changing large parts of the page it might not be worth it. If we compare our best efforts to a simple browser navigation: (function() { var graph = new TimingGraph(3500, 1000, 500); document.querySelector('.results-navigation').appendChild(graph.container); graph.addResult('Streaming iframe hack', 500, 2500); graph.addResult('XHR + ND-JSON', 600, 2500); graph.addResult('Normal server render', 600, 2500); }()); Raw test data. …a simple no-JavaScript browser navigation to a server rendered page is roughly as fast. The test page is really simple aside from the comments list, your mileage may vary if you have a lot of complex content repeated between pages (basically, I mean horrible ad scripts), but always test! You might be writing a lot of code for very little benefit, or even making it slower. Thanks to Elliott Sprehn for telling me the HTML parser worked this way!

Sounds fun

I played with the web audio API for the first time recently, so I thought I'd write up what I learned. I think that's my job or something. Playing a sound The simplest demonstrable thing we can do with web audio is "play a sound". But to do that, we first we need to load & decode something: // The context is connected to the device speakers. // You only need one of these per document. const context = new AudioContext(); // Fetch the file fetch('sound.mp4') // Read it into memory as an arrayBuffer .then((response) => response.arrayBuffer()) // Turn it from mp3/aac/whatever into raw audio data .then((arrayBuffer) => context.decodeAudioData(arrayBuffer)) .then((audioBuffer) => { // Now we're ready to play! }); Unfortunately we need to work around a few things in Safari. We need to use webkitAudioContext - Safari doesn't support the unprefixed version. It doesn't support fetch yet (it's in development) so we'll need to use XHR). And decodeAudioData doesn't support promises, so we'll need to polyfill that. But once we've got our audio buffer, we can play it: // Create a source: // This represents a playback head. const source = context.createBufferSource(); // Give it the audio data we loaded: source.buffer = audioBuffer; // Plug it into the output: source.connect(context.destination); // And off we go! source.start(); Job done! * { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } .loop { background: rgba(255, 255, 255, 0.21); width: 0; border: 1px solid rgba(255, 255, 255, 0.41); border-width: 0 1px; box-sizing: border-box; background-clip: content-box; } .play-head { width: 1px; background: #fff; will-change: transform; display: none; } .bwq-loops, .aac-decode { display: flex; } .bwq-loops .audio-output, .aac-decode .audio-output { flex: 1; } .bwq-loops .audio-output:nth-child(2) { margin: 0 10px; } .bwq-loops .audio-output:nth-child(3) { flex: 0.2; } .aac-decode .audio-output:first-child { margin-right: 5px; } .aac-decode .audio-output:last-child { margin-left: 5px; } .audio-buttons { position: absolute; top: 0; left: 0; right: 0; padding: 16px 20px; } @media (min-width: 530px) { .audio-buttons { padding-left: 32px; } } .audio-container progress { width: 100%; } function bufferFetch(url, progressCb) { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.responseType = 'arraybuffer'; xhr.onload = () => resolve(xhr.response); xhr.onerror = () => reject(Error('Fetch failed')); if (progressCb) xhr.onprogress = event => progressCb(event.loaded / event.total); xhr.open('GET', url); xhr.send(); }); } const context = new (self.AudioContext || self.webkitAudioContext)(); const safetyOffset = 0.25; function drawAudio(canvas, buffer, start, end) { const resolution = 10; const rect = canvas.getBoundingClientRect(); canvas.width = Math.floor(rect.width * devicePixelRatio); canvas.height = Math.floor(rect.height * devicePixelRatio); const context = canvas.getContext('2d'); let data = buffer.getChannelData(0); if (start || end) { data = data.slice(start || 0, end || data.length); } context.fillStyle = '#12d67d'; for (let i = 0; i item) min = item; } const height = (max - min) / 2 * canvas.height; const startPixel = (1 - (max + 1) / 2) * canvas.height; context.fillRect(i, startPixel, 1, height) } } function drawLoop(el, start, width) { el.style.display = 'block'; el.style.left = start * 100 + '%'; el.style.width = width * 100 + '%'; } // Safari doesn't support promises in decodeAudioData :( if (!window.AudioContext && window.webkitAudioContext) { const oldFunc = webkitAudioContext.prototype.decodeAudioData; webkitAudioContext.prototype.decodeAudioData = function(arraybuffer) { return new Promise((resolve, reject) => { oldFunc.call(this, arraybuffer, resolve, reject); }); } } const loop1 = { barLength: (60 / 110 /*BPM*/) * 4, size: 1150563, buffer: null, startOffset: 0, downloadProgress: 0, url: '/c/loop1-46034bf9.mp4' }; const loop2 = { barLength: (60 / 123 /*BPM*/) * 4, size: 1062675, buffer: null, startOffset: 0, downloadProgress: 0, url: '/c/loop2-f1b50df7.mp4' }; const stab = { buffer: null, size: 110755, startOffset: 0, downloadProgress: 0, url: '/c/stab-6fedd1c5.mp4' }; const singleLoop = { size: 966911, buffer: null, startOffset: 0, downloadProgress: 0, url: '/c/sonic-fcf62fd5.mp4' }; function dispatchStateChange() { window.dispatchEvent(new Event('app-statechange')); } function downloadAudio(item) { item.downloadProgress = 0.0001; dispatchStateChange(); return bufferFetch(item.url, complete => { item.downloadProgress = complete; dispatchStateChange(); }).then(ab => context.decodeAudioData(ab)).then(buffer => { const l = buffer.getChannelData(0); const r = buffer.getChannelData(1); for (var i = 0; i (function() { let audioDrawn = false; let stabSource; let stabStart = 0; const buttonsEl = document.querySelector('.stab-only-buttons'); const playhead = document.querySelector('.stab-only .play-head'); function updateButtonsUi() { if (!stab.buffer) { if (stab.downloadProgress) { let progress = buttonsEl.firstElementChild; if (!progress || progress.tagName != 'PROGRESS') { buttonsEl.innerHTML = ` `; progress = buttonsEl.firstElementChild; } progress.value = stab.downloadProgress; return; } buttonsEl.innerHTML = ` Download audio (${humanSize(stab.size)}) `; return; } if (!audioDrawn) { drawAudio(document.querySelector('.stab-only canvas'), stab.buffer); audioDrawn = true; } buttonsEl.innerHTML = `Play`; } window.addEventListener('app-statechange', updateButtonsUi); updateButtonsUi(); function updatePlayheadUi() { if (!stabSource) { playhead.style.display = 'none'; return; } const rect = playhead.parentNode.getBoundingClientRect(); const posInTrack = context.currentTime - stabStart; const pos = Math.max(posInTrack / stab.buffer.duration, 0); playhead.style.display = 'block'; playhead.style.transform = `translate(${rect.width * pos}px, 0)`; requestAnimationFrame(updatePlayheadUi); } function start() { stabSource = context.createBufferSource(); stabSource.onended = event => { if (stabSource == event.target) stabSource = null; }; stabSource.buffer = stab.buffer; stabSource.connect(context.destination); stabStart = context.currentTime; stabSource.start(stabStart); } buttonsEl.addEventListener('click', event => { const button = event.target; if (!button) return; if (button.classList.contains('stab-load')) { downloadAudio(stab); return; } if (button.classList.contains('stab-play')) { start(); updatePlayheadUi(); return; } }); })(); So yeah, it's way more complicated than just using <audio src="…"> to play a sound, but web audio can do so much more. The amount of control web audio gives you is great fun, but also kinda daunting. In this post I'm just going to scratch the surface, and look at how to loop and queue sounds. The Big Web Quiz At Chrome Dev Summit Paul & I ran a web-based interactive quiz between talks. CSS properties on the Big Web Quiz We tried to make it as ridiculous as possible, and the music was a big part of that. The music was produced by Plan8, and it only took them a day to compose (we misread the licence on a piece of music we were going to use, so the deadline was our fault. Anyway, the music they made is way better). They also have JS libraries for scheduling audio, but hey I was in the mood for some procrastination, so I did it myself. Switching between clips The music in the Big Web Quiz has three phases, and we wanted to switch between them during questions. Using the code above, I loaded three buffers, phase1AudioBuffer, phase2AudioBuffer, and stabAudioBuffer, each representing a different phase of Big Web Quiz's music. A naive solution is to play phase 1, then later stop it and play phase 2: const phase1Source = context.createBufferSource(); phase1Source.buffer = phase1AudioBuffer; phase1Source.connect(context.destination); phase1Source.start(); // Then later… const phase2Source = context.createBufferSource(); phase2Source.buffer = phase2AudioBuffer; phase2Source.connect(context.destination); // Stop phase 1 phase1Source.stop(); // Start phase 2 phase2Source.start(); (function() { let uiState = 'stopped'; let audioDrawn = false; const buttonsEl = document.querySelector('.bwq-simple-buttons'); const outputEls = document.querySelectorAll('.bwq-simple .audio-output'); const playheadEls = document.querySelectorAll('.bwq-simple .play-head'); let loop1Source; let loop1Start; let loop2Source; let loop2Start; let stabSource; let stabStart; function updateButtonsUi() { const loops = [loop1, loop2, stab]; const notLoaded = loops.filter(item => !item.buffer); const allLoading = loops.every(item => item.downloadProgress > 0); const remaining = notLoaded.reduce((total, item) => total + item.size, 0); if (notLoaded.length) { if (allLoading) { let progress = buttonsEl.firstElementChild; if (!progress || progress.tagName != 'PROGRESS') { buttonsEl.innerHTML = ` `; progress = buttonsEl.firstElementChild; } progress.value = loops.reduce((total, loop) => total + loop.downloadProgress, 0) / loops.length; return; } buttonsEl.innerHTML = ` Download audio (${humanSize(remaining)}) `; return; } if (!audioDrawn) { drawAll(); audioDrawn = true; } if (uiState == 'stopped' || uiState == 'stab') { buttonsEl.innerHTML = ` Play `; return; } if (uiState == 'loop1') { buttonsEl.innerHTML = ` Next phase Stop `; return; } if (uiState == 'loop2') { buttonsEl.innerHTML = ` Stab Stop `; return; } } function updatePlayheadUi() { if (uiState == 'stopped') { for (let el of Array.from(playheadEls)) { el.style.display = 'none'; } return; } let container; let playHead; let buffer; let start; if (loop1Source) { container = outputEls[0]; playHead = playheadEls[0]; buffer = loop1.buffer; start = loop1Start; } else if (loop2Source) { container = outputEls[1]; playHead = playheadEls[1]; buffer = loop2.buffer; start = loop2Start; } else { container = outputEls[2]; playHead = playheadEls[2]; buffer = stab.buffer; start = stabStart; } const rect = container.getBoundingClientRect(); for (let el of Array.from(playheadEls)) { el.style.display = 'none'; } let posInTrack = context.currentTime - start; if (posInTrack > buffer.duration) { if (uiState == 'stab') return; posInTrack = posInTrack % buffer.duration; } const pos = Math.max(posInTrack / buffer.duration, 0); playHead.style.display = 'block'; playHead.style.transform = `translate(${rect.width * pos}px, 0)`; requestAnimationFrame(updatePlayheadUi); } window.addEventListener('app-statechange', updateButtonsUi); updateButtonsUi(); function drawAll() { const canvases = document.querySelectorAll('.bwq-simple canvas'); drawAudio(canvases[0], loop1.buffer); drawAudio(canvases[1], loop2.buffer); drawAudio(canvases[2], stab.buffer); } function start() { loop1Source = context.createBufferSource(); loop1Source.onended = event => { if (loop1Source == event.target) loop1Source = null; }; loop1Source.buffer = loop1.buffer; loop1Source.loop = true; loop1Source.loopStart = loop1.startOffset; loop1Source.connect(context.destination); loop1Start = context.currentTime; loop1Source.start(); } function stepItUp() { loop2Source = context.createBufferSource(); loop2Source.onended = event => { if (loop2Source == event.target) loop2Source = null; }; loop2Source.buffer = loop2.buffer; loop2Source.loop = true; loop2Source.loopStart = loop2.startOffset; loop2Source.connect(context.destination); loop2Source.start(); loop1Source.stop(); loop2Start = context.currentTime; } function playStab() { const stabSource = context.createBufferSource(); stabSource.buffer = stab.buffer; stabSource.connect(context.destination); stabSource.start(); loop2Source.stop(); stabStart = context.currentTime; } function stop() { if (loop1Source) { try { loop1Source.stop(); } catch (_) {} } if (loop2Source) { try { loop2Source.stop(); } catch (_) {} } } buttonsEl.addEventListener('click', event => { const button = event.target; if (!button) return; if (button.classList.contains('bwq-load')) { uiState = 'stopped'; [loop1, loop2, stab].filter(item => !item.buffer).map(item => downloadAudio(item)); return; } if (button.classList.contains('bwq-play')) { start(); uiState = 'loop1'; updateButtonsUi(); updatePlayheadUi(); return; } if (button.classList.contains('bwq-stop')) { stop(); uiState = 'stopped'; updateButtonsUi(); return; } if (button.classList.contains('bwq-step-up')) { stepItUp(); uiState = 'loop2'; updateButtonsUi(); return; } if (button.classList.contains('bwq-stab')) { playStab(); uiState = 'stab'; updateButtonsUi(); return; } }); })(); This doesn't really work. Switching between phases is jarring for a few reasons… Sound may not play instantly Even though we have our audio data loaded in memory, there's still a gap between us calling start() and the audio actually playing. This is fine if you want the sound to play as soon as possible and don't mind if it's a few milliseconds out, such as playing a sound in a game when the player collects a coin, but when syncronising two clips things need to be precise. To do anything precise with audio, you need to schedule things in advance. Both start and stop take an optional number, the time to actually start/stop, and context.currentTime gives you the current time as far as the audio context is concerned. How much advance notice you have to give depends on hardware, but Chris Wilson reliably informs me that a quarter of a second is super-safe more-than-enough, even for slow hardware. So: const safetyBuffer = 0.25; const switchTime = context.currentTime + safetyBuffer; phase1Source.stop(switchTime); phase2Source.start(switchTime); A quarter of a second is a long delay, but in this case syncronisation is more important to us than immediacy. Note: In the web audio API, time is in seconds, whereas most other web APIs use milliseconds. But there's another delay to tackle… Different decoders are different Encoding audio down to formats like MP3 or AAC is a lossy process, but you at least get to pick the encoder. When you use decodeAudioData you're relying on whatever decoder the browser uses, and this may come as a shock, but sometimes different browsers do things differently. Here's the start/end of an AAC clip decoded by your browser: context.decodeAudioData(ab)).then(buffer => { const canvases = document.querySelectorAll('.aac-decode canvas'); drawAudio(canvases[0], buffer, 0, 7000); drawAudio(canvases[1], buffer, -7000); }); The original clip is gapless at the start/end, but if you're in Chrome stable, Firefox, or Edge, you'll see a huge gap. By huge I mean 45 milliseconds, but y'know, that's a big deal when we're trying to instantly switch between two clips. The gap is almost gone in Chrome Canary. Safari on the other hand gets it spot-on, no gap at all. In the first draft of this article I congratulated Safari on a job well done, but actual expert Paul Adenot from Mozilla dropped a few knowledge bombs on me (in a friendly way of course). The gap at the start is specified by the encoder as metadata. From Apple's documentation: …encoders add at least 1024 samples of silence before the first ‘true’ audio sample, and often add more. This is called variously “priming”, “priming samples”, or “encoder delay”… Therefore, a playback system must trim the silent priming samples to preserve correct synchronization. This trimming by the playback system should be done in two places: When playback first begins When the playback position is moved to another location. For example, the user skips ahead or back to another part of the media and begins playback from that new location. The question is, should the browser remove the "priming samples" as part of decodeAudioData, or are we (as users of the web audio API) the "playback system", meaning we have to deal with it. I still feel that Safari is doing the right thing here, especially as finding out the number of priming samples from the metadata is really non-trivial. To try and bring some consistency here, I've filed an issue with the spec. In the meantime, we can work around the gap by finding out how long it is: function findStartGapDuration(audioBuffer) { // Get the raw audio data for the left & right channels. const l = audioBuffer.getChannelData(0); const r = audioBuffer.getChannelData(1); // Each is an array of numbers between -1 and 1 describing // the waveform, sample by sample. // Now to figure out how long both channels remain at 0: for (let i = 0; i < l.length; i++) { if (l[i] || r[i]) { // Now we know which sample is non-zero, but we want // the gap in seconds, not samples. Thankfully sampleRate // gives us the number of samples per second. return i / audioBuffer.sampleRate; } } // Hmm, the clip is entirely silent return audioBuffer.duration; } Once we have the gap, we can use source's second parameter to start playback at that point, after the silence: const phase1StartGap = findStartGapDuration(phase1AudioBuffer); const phase1Source = context.createBufferSource(); phase1Source.buffer = phase1AudioBuffer; phase1Source.connect(context.destination); // Cater for the gap: phase1Source.start(context.currentTime + safetyBuffer, phase1StartGap); // Then later… const phase2StartGap = findStartGapDuration(phase2AudioBuffer); const phase2Source = context.createBufferSource(); phase2Source.buffer = phase2AudioBuffer; phase2Source.connect(context.destination); const switchTime = context.currentTime + safetyBuffer; // Stop phase 1 phase1Source.stop(switchTime); // Start phase 2 phase2Source.start(switchTime, phase2StartGap); And here's the result: (function() { let uiState = 'stopped'; let audioDrawn = false; const buttonsEl = document.querySelector('.bwq-better-buttons'); const outputEls = document.querySelectorAll('.bwq-better .audio-output'); const playheadEls = document.querySelectorAll('.bwq-better .play-head'); let loop1Source; let loop1Start; let loop2Source; let loop2Start; let stabSource; let stabStart; function updateButtonsUi() { const loops = [loop1, loop2, stab]; const notLoaded = loops.filter(item => !item.buffer); const allLoading = loops.every(item => item.downloadProgress > 0); const remaining = notLoaded.reduce((total, item) => total + item.size, 0); if (notLoaded.length) { if (allLoading) { let progress = buttonsEl.firstElementChild; if (!progress || progress.tagName != 'PROGRESS') { buttonsEl.innerHTML = ` `; progress = buttonsEl.firstElementChild; } progress.value = loops.reduce((total, loop) => total + loop.downloadProgress, 0) / loops.length; return; } buttonsEl.innerHTML = ` Download audio (${humanSize(remaining)}) `; return; } if (!audioDrawn) { drawAll(); audioDrawn = true; } if (uiState == 'stopped' || uiState == 'stab') { buttonsEl.innerHTML = ` Play `; return; } if (uiState == 'loop1') { buttonsEl.innerHTML = ` Next phase Stop `; return; } if (uiState == 'loop2') { buttonsEl.innerHTML = ` Stab Stop `; return; } } function updatePlayheadUi() { if (uiState == 'stopped') { for (let el of Array.from(playheadEls)) { el.style.display = 'none'; } return; } let container; let playHead; let buffer; let start; if (loop1Source) { container = outputEls[0]; playHead = playheadEls[0]; buffer = loop1.buffer; start = loop1Start; } else if (loop2Source) { container = outputEls[1]; playHead = playheadEls[1]; buffer = loop2.buffer; start = loop2Start; } else { container = outputEls[2]; playHead = playheadEls[2]; buffer = stab.buffer; start = stabStart; } const rect = container.getBoundingClientRect(); for (let el of Array.from(playheadEls)) { el.style.display = 'none'; } let posInTrack = context.currentTime - start; if (posInTrack > buffer.duration) { if (uiState == 'stab') return; posInTrack = posInTrack % buffer.duration; } const pos = Math.max(posInTrack / buffer.duration, 0); playHead.style.display = 'block'; playHead.style.transform = `translate(${rect.width * pos}px, 0)`; requestAnimationFrame(updatePlayheadUi); } window.addEventListener('app-statechange', updateButtonsUi); updateButtonsUi(); function drawAll() { const canvases = document.querySelectorAll('.bwq-better canvas'); drawAudio(canvases[0], loop1.buffer); drawAudio(canvases[1], loop2.buffer); drawAudio(canvases[2], stab.buffer); } function start() { loop1Source = context.createBufferSource(); loop1Source.onended = event => { if (loop1Source == event.target) loop1Source = null; }; loop1Source.buffer = loop1.buffer; loop1Source.loop = true; loop1Source.loopStart = loop1.startOffset; loop1Source.connect(context.destination); loop1Start = context.currentTime + safetyOffset; loop1Source.start(loop1Start, loop1.startOffset); } function stepItUp() { loop2Source = context.createBufferSource(); loop2Source.onended = event => { if (loop2Source == event.target) loop2Source = null; }; loop2Source.buffer = loop2.buffer; loop2Source.loop = true; loop2Source.loopStart = loop2.startOffset; loop2Source.connect(context.destination); const startTime = context.currentTime + safetyOffset; loop2Source.start(startTime, loop2.startOffset); loop1Source.stop(startTime); loop2Start = startTime; } function playStab() { const stabSource = context.createBufferSource(); stabSource.buffer = stab.buffer; stabSource.connect(context.destination); const startTime = context.currentTime + safetyOffset; stabSource.start(startTime, stab.startOffset); loop2Source.stop(startTime); stabStart = startTime; } function stop() { if (loop1Source) { try { loop1Source.stop(); } catch (_) {} } if (loop2Source) { try { loop2Source.stop(); } catch (_) {} } } buttonsEl.addEventListener('click', event => { const button = event.target; if (!button) return; if (button.classList.contains('bwq-load')) { uiState = 'stopped'; [loop1, loop2, stab].filter(item => !item.buffer).map(item => downloadAudio(item)); return; } if (button.classList.contains('bwq-play')) { start(); uiState = 'loop1'; updateButtonsUi(); updatePlayheadUi(); return; } if (button.classList.contains('bwq-stop')) { stop(); uiState = 'stopped'; updateButtonsUi(); return; } if (button.classList.contains('bwq-step-up')) { stepItUp(); uiState = 'loop2'; updateButtonsUi(); return; } if (button.classList.contains('bwq-stab')) { playStab(); uiState = 'stab'; updateButtonsUi(); return; } }); })(); Better, but not perfect. Depending on when you press the button, the switch from phase 2 to the end stab can feel mistimed, but we can fix that… Musically-aware scheduling Ideally we want the phases to switch right at the end of a musical bar. Phase 1 is 110bpm, and phase 2 is 123bpm, so we can figure out the duration of each bar: function getBarDuration(bpm, beatsPerBar) { return (60 / bpm) * beatsPerBar; } const phase1BarDuration = getBarDuration(110, 4); const phase2BarDuration = getBarDuration(123, 4); We want to switch the phases at the end of the next bar, unless that's less than our safetyBuffer, in which case we want to switch at the end of the following bar. function getPhaseSwitchTime(currentTime, phaseStartTime, barDuration) { // How long the phase has been playing: const phasePlaybackPosition = currentTime - phaseStartTime; // How long has it been playing the current bar: const positionWithinBar = phasePlaybackPosition % barDuration; // How long until the next bar: let untilSwitch = barDuration - positionWithinBar; // If it's less than our safetyBuffer, add another bar: if (untilSwitch < safetyBuffer) untilSwitch += barDuration; // Add on the current time: return untilSwitch + currentTime; } Unfortunately the web audio API doesn't tell us the current playblack position of a source (it might eventually), so we have to track that ourselves: const phase1StartTime = context.currentTime + safetyBuffer; phase1Source.start(phase1StartTime, phase1StartGap); // Then later… const phase2StartTime = getPhaseSwitchTime( context.currentTime, phase1StartTime, phase1BarDuration, ); phase1Source.stop(phase2StartTime); phase2Source.start(phase2StartTime, phase2StartGap); Job done! Here it is: (function() { let uiState = 'stopped'; let audioDrawn = false; const buttonsEl = document.querySelector('.bwq-perfect-buttons'); const outputEls = document.querySelectorAll('.bwq-perfect .audio-output'); const playheadEls = document.querySelectorAll('.bwq-perfect .play-head'); let loop1Source; let loop1Start; let loop2Source; let loop2Start; let stabSource; let stabStart; function getBarSwitchTime(currentTime, loopStart, loopBarLength) { const loopPlaytime = currentTime - loopStart; const timeInBar = loopPlaytime % loopBarLength; let untilSwitch = loopBarLength - timeInBar; if (untilSwitch !item.buffer); const allLoading = loops.every(item => item.downloadProgress > 0); const remaining = notLoaded.reduce((total, item) => total + item.size, 0); if (notLoaded.length) { if (allLoading) { let progress = buttonsEl.firstElementChild; if (!progress || progress.tagName != 'PROGRESS') { buttonsEl.innerHTML = ` `; progress = buttonsEl.firstElementChild; } progress.value = loops.reduce((total, loop) => total + loop.downloadProgress, 0) / loops.length; return; } buttonsEl.innerHTML = ` Download audio (${humanSize(remaining)}) `; return; } if (!audioDrawn) { drawAll(); audioDrawn = true; } if (uiState == 'stopped' || uiState == 'stab') { buttonsEl.innerHTML = ` Play `; return; } if (uiState == 'loop1') { buttonsEl.innerHTML = ` Next phase Stop `; return; } if (uiState == 'loop2') { buttonsEl.innerHTML = ` Stab Stop `; return; } } function updatePlayheadUi() { if (uiState == 'stopped') { for (let el of Array.from(playheadEls)) { el.style.display = 'none'; } return; } let container; let playHead; let buffer; let start; if (loop1Source) { container = outputEls[0]; playHead = playheadEls[0]; buffer = loop1.buffer; start = loop1Start; } else if (loop2Source) { container = outputEls[1]; playHead = playheadEls[1]; buffer = loop2.buffer; start = loop2Start; } else { container = outputEls[2]; playHead = playheadEls[2]; buffer = stab.buffer; start = stabStart; } const rect = container.getBoundingClientRect(); for (let el of Array.from(playheadEls)) { el.style.display = 'none'; } let posInTrack = context.currentTime - start; if (posInTrack > buffer.duration) { if (uiState == 'stab') return; posInTrack = posInTrack % buffer.duration; } const pos = Math.max(posInTrack / buffer.duration, 0); playHead.style.display = 'block'; playHead.style.transform = `translate(${rect.width * pos}px, 0)`; requestAnimationFrame(updatePlayheadUi); } window.addEventListener('app-statechange', updateButtonsUi); updateButtonsUi(); function drawBarLines(canvas, loop) { const context = canvas.getContext('2d'); context.fillStyle = 'rgba(255, 255, 255, 0.2)'; const width = Math.floor(loop.barLength / loop.buffer.duration * canvas.width); for (let i = loop.startOffset + loop.barLength; i { if (loop1Source == event.target) loop1Source = null; }; loop1Source.buffer = loop1.buffer; loop1Source.loop = true; loop1Source.loopStart = loop1.startOffset; loop1Source.connect(context.destination); loop1Start = context.currentTime + safetyOffset; loop1Source.start(loop1Start, loop1.startOffset); } function stepItUp() { loop2Source = context.createBufferSource(); loop2Source.onended = event => { if (loop2Source == event.target) loop2Source = null; }; loop2Source.buffer = loop2.buffer; loop2Source.loop = true; loop2Source.loopStart = loop2.startOffset; loop2Source.connect(context.destination); const startTime = getBarSwitchTime(context.currentTime, loop1Start, loop1.barLength); loop2Source.start(startTime, loop2.startOffset); loop1Source.stop(startTime); loop2Start = startTime; } function playStab() { const stabSource = context.createBufferSource(); stabSource.buffer = stab.buffer; stabSource.connect(context.destination); const startTime = getBarSwitchTime(context.currentTime, loop2Start, loop2.barLength); stabSource.start(startTime, stab.startOffset); loop2Source.stop(startTime); stabStart = startTime; } function stop() { if (loop1Source) { try { loop1Source.stop(); } catch (_) {} } if (loop2Source) { try { loop2Source.stop(); } catch (_) {} } } buttonsEl.addEventListener('click', event => { const button = event.target; if (!button) return; if (button.classList.contains('bwq-load')) { uiState = 'stopped'; [loop1, loop2, stab].filter(item => !item.buffer).map(item => downloadAudio(item)); return; } if (button.classList.contains('bwq-play')) { start(); uiState = 'loop1'; updateButtonsUi(); updatePlayheadUi(); return; } if (button.classList.contains('bwq-stop')) { stop(); uiState = 'stopped'; updateButtonsUi(); return; } if (button.classList.contains('bwq-step-up')) { stepItUp(); uiState = 'loop2'; updateButtonsUi(); return; } if (button.classList.contains('bwq-stab')) { playStab(); uiState = 'stab'; updateButtonsUi(); return; } }); })(); Sometimes switching between clips can cause a click if the samples don't join at a zero value. If you get this, you can use a gain node to create a really short, imperceptible, fade-out and fade-in. Dynamic looping Switching multiple clips isn't the only way to create multi-phase audio. BEHOLD: (function() { let uiState = 'stopped'; let audioDrawn = false; const buttonsEl = document.querySelector('.single-loop-buttons'); const playheadEl = document.querySelector('.single-loop .play-head'); const loopEl = document.querySelector('.single-loop .loop'); const samplePhases = [ {start: 328948, end: 656828}, {start: 985007, end: 1314119}, {start: 1643272, end: 1972421}, {start: 1972421, end: 2137288}, ]; let phases; let currentPhase = 0; let loopSource; let loopStart; function updateButtonsUi() { if (!singleLoop.buffer) { if (singleLoop.downloadProgress) { let progress = buttonsEl.firstElementChild; if (!progress || progress.tagName != 'PROGRESS') { buttonsEl.innerHTML = ` `; progress = buttonsEl.firstElementChild; } progress.value = singleLoop.downloadProgress; return; } buttonsEl.innerHTML = ` Download audio (${humanSize(singleLoop.size)}) `; return; } if (!audioDrawn) { drawAudio(document.querySelector('.single-loop canvas'), singleLoop.buffer); phases = samplePhases.map(obj => ({ start: obj.start / 48000 + singleLoop.startOffset, end: obj.end / 48000 + singleLoop.startOffset })); audioDrawn = true; } if (uiState == 'stopped') { buttonsEl.innerHTML = ` Play `; return; } if (uiState == 'playing') { buttonsEl.innerHTML = ` Next loop Stop `; return; } } let lastTime; let posInTrack; function updatePlayheadUi() { if (uiState == 'stopped') { playheadEl.style.display = 'none'; return; } const time = context.currentTime; posInTrack += time - lastTime; lastTime = time; if (posInTrack > loopSource.loopEnd) { posInTrack = loopSource.loopStart + (posInTrack - loopSource.loopEnd); } const rect = playheadEl.parentNode.getBoundingClientRect(); const pos = Math.max(posInTrack / singleLoop.buffer.duration, 0); playheadEl.style.transform = `translate(${rect.width * pos}px, 0)`; playheadEl.style.display = 'block'; requestAnimationFrame(updatePlayheadUi); } window.addEventListener('app-statechange', updateButtonsUi); updateButtonsUi(); function start() { const buffer = singleLoop.buffer; currentPhase = 0; loopSource = context.createBufferSource(); loopSource.buffer = buffer; loopSource.loop = true; loopSource.loopStart = phases[currentPhase].start; loopSource.loopEnd = phases[currentPhase].end; loopSource.connect(context.destination); loopSource.start(context.currentTime + safetyOffset, singleLoop.startOffset); lastTime = context.currentTime; posInTrack = -safetyOffset + singleLoop.startOffset; loopEl.style.display = 'block'; drawLoop(loopEl, phases[currentPhase].start / buffer.duration, (phases[currentPhase].end - phases[currentPhase].start) / buffer.duration); } function nextPhase() { const buffer = singleLoop.buffer; currentPhase++; if (currentPhase != phases.length) { loopSource.loopStart = phases[currentPhase].start; loopSource.loopEnd = phases[currentPhase].end; drawLoop(loopEl, phases[currentPhase].start / buffer.duration, (phases[currentPhase].end - phases[currentPhase].start) / buffer.duration); return; } currentPhase = 1; loopSource.loopStart = phases[currentPhase].start; loopSource.loopEnd = 2631045 / 48000 + singleLoop.startOffset; drawLoop(loopEl, phases[currentPhase].start / buffer.duration, buffer.duration); setTimeout(() => { loopSource.loopEnd = phases[currentPhase].end; drawLoop(loopEl, phases[currentPhase].start / buffer.duration, (phases[currentPhase].end - phases[currentPhase].start) / buffer.duration); }, 658624 / 48); } function stop() { loopEl.style.display = 'none'; if (loopSource) { try { loopSource.stop(); } catch (_) {} } } buttonsEl.addEventListener('click', event => { const button = event.target; if (!button) return; if (button.classList.contains('single-loop-load')) { downloadAudio(singleLoop); return; } if (button.classList.contains('single-loop-play')) { start(); uiState = 'playing'; updateButtonsUi(); updatePlayheadUi(); return; } if (button.classList.contains('single-loop-stop')) { stop(); uiState = 'stopped'; updateButtonsUi(); return; } if (button.classList.contains('single-loop-next')) { nextPhase(); return; } }); })(); Credit: Sonic 2, chemical plant zone, Protostar remix. This is a single source that loops, but the loop-points change dynamically. Compared to what we've done already, looping a clip is pretty simple: // Cater for buggy AAC decoders as before: const sonicStartGap = findStartGapDuration(sonicAudioBuffer); // Create the source: const sonicSource = context.createBufferSource(); sonicSource.buffer = sonicAudioBuffer; sonicSource.connect(context.destination); // Loop it! sonicSource.loop = true; // Set loop points: sonicSource.loopStart = loopStartTime + sonicStartGap; sonicSource.loopEnd = loopEndTime + sonicStartGap; // Play! sonicSource.start(0, sonicStartGap); And changing those loop points is just… sonicSource.loopStart = anotherLoopStartTime + sonicStartGap; sonicSource.loopEnd = anotherLoopEndTime + sonicStartGap; Although discovering the loop points is easier said than done. Finding the loop points Tools like Audacity (free) and Adobe Audition (not so free) are great for chopping and looping audio. Once we've found the loop points, we need to find the sample they start & end on. This is the most accurate measurement we'll get. Selecting by sample in Audacity const loopPoints = [ { start: 328948, end: 656828 }, { start: 985007, end: 1314119 }, { start: 1643272, end: 1972421 }, { start: 1972421, end: 2137288 }, ]; But loopStart and loopEnd want the time in seconds, so we convert them: const loopPointTimes = loopPoints.map((loop) => ({ start: loop.start / 48000 + sonicStartGap, end: loop.end / 48000 + sonicStartGap, })); 48000 needs to be replaced with the sample rate of the clip as viewed in Audacity. Don't do what I did & use buffer.sampleRate, as the decoded sample rate can be different to the sample rate of the file. Audio is decoded to context.sampleRate, which is 44,100 on my mac, but 48,000 on my phone. Looping back to an earlier point At the end of the demo above, the clip loops back to an earlier point. Unfortunately, if you set loopEnd to a point earlier than the current playback point, it immediately goes back to loopStart, whereas we want it to play through to the end, then go back to an earlier loop. The least hacky way to do this would be to stop sonicSource looping, and queue up a new sonicSource2 to start looping once sonicSource reaches its natural finish. However, to do this, we'd need to know the current playback position of sonicSource, and as I mentioned earlier, this feature hasn't landed yet. We can't even reliably work around this - the source has been looping all over the place, and we can't be sure each write to loopStart and loopEnd made it to the sound card in time. I'm hacking it for the purposes of the visualisations above, but it isn't accurate enough for sound. To work around this we make two changes to the loop. We loop from the start of the earlier loop, right to the end of the clip. Then, once the clip has played past the end, we change loopEnd to the end of the earlier loop. // Current loop start const currentLoopStart = sonicSource.loopStart; // The earlier loop we want to move to. const targetLoop = loopPointTimes[1]; // The point we want to reach before looping back. // sonicSource.duration is not good enough here, due to // the AAC decoding bug mentioned earlier. const endSample = 658624; const endTime = endSample / 48000 + sonicStartGap; // Play to the end, then loop back to the start: sonicSource.loopStart = targetLoop.start; sonicSource.loopEnd = endTime; // But once it's gone back to loopStart, we don't want // it to play all the way to loopEnd, we want targetLoop.end. // Hack time! setTimeout(() => { sonicSource.loopEnd = targetLoop.end; }, (endTime - currentLoopStart) * 1000); endTime - currentLoopStart is the maximum time the clip could play before it loops back to targetLoop.start, after that it's safe to move the end point. Done! While the web audio API isn't something you'll use in every project, it's suprisingly powerful and fun. If you're wanting to dig a little deeper, I recommend this multi-part guide by Tero Parviainen. Speaking of procrastination, I really should be getting back to the service worker spec… Huge thanks to Paul Adenot, Stuart Memo, Chris Wilson, and Jen Ross for proof-reading and fact-checking.

SVG & media queries

One of the great things about SVG is you can use media queries to add responsiveness to images: <svg width="100" height="100" xmlns="http://www.w3.org/2000/svg"> <style> circle { fill: green; } @media (min-width: 100px) { circle { fill: blue; } } </style> <circle cx="50" cy="50" r="50"/> </svg> But when should the circle be blue? The specs say min-width should match on the width of the viewport, but… Which viewport? <img src="circle.svg" width="50" height="50" /> <img src="circle.svg" width="100" height="100" /> <iframe src="circle.svg" width="50" height="50"></iframe> <svg width="50" height="50"> …as above… </svg> Which of the above would draw a (potentially clipped) blue circle in an HTML document? As in, which viewport should be used? Should it be: The CSS size of the host document The width/height/viewBox attributes on the <svg> The width/height attributes on the <img> The CSS layout size of the <img> Here's an demo of the above: .inline-svg-circle { fill: green; } @media (min-width: 100px) { .inline-svg-circle { fill: blue; } } Most browsers say… For <img>, the SVG is scaled to fit the image element, and the viewport of the SVG is the CSS dimensions of the <img>. So the first <img> has a viewport width of 50, and the second has a viewport width of 100. This means the second <img> picks up the "blue" media query, but the first does not. For <iframe>, the viewport of the SVG is the viewport of the iframed document. So in the above example, the viewport width is 50 CSS pixels because that's the width of the iframe. For inline <svg>, the SVG doesn't have its own viewport, it's part of the parent document. This means the <style> is owned by the parent document - it isn't scoped to the SVG. This caught me out when I first used inline SVG, but it makes sense and is well defined in the spec. But what does the fox say? Firefox has other ideas. It behaves as above, except: For <img>, the viewport is the rendered size in device pixels, meaning the viewport changes depending on display density. The first image in the demo will appear green on 1x screens, but blue on 2x screens and above. This is a problem as some laptops and most phones have a pixel density greater than 1. This feels like a bug, especially as Firefox doesn't apply the same logic to the iframe, but we have to cut Firefox some slack here as the spec doesn't really cover how SVG-in-<img> should be scaled, let alone how media queries should be handled. I've filed an issue with the spec, hopefully this can be cleared up. But things get a lot more complicated when you start… Drawing SVG to a canvas You can also draw <img>s to <canvas>es: canvas2dContext.drawImage(img, x, y, width, height); But when should the circle be blue? There are a few more viewport choices this time. Should it be: The CSS size of the host window The width/height/viewBox attributes on the <svg> The width/height attributes on the <img> The CSS layout dimensions of the <img> The pixel-data dimensions of the <canvas> The CSS layout dimensions of the <canvas> The width/height specified in drawImage The width/height specified in drawImage, multiplied by whatever transform the 2d context has Which would you expect? Again, the spec is unclear, and this time every browser has gone in a different direction. Give it a try: SVG size 50x50 100x100 Use viewBox <img> size Unset 50x50 100x100 <img> CSS size Unset 50x50 100x100 Add to <body> <canvas> size 50x50 100x100 drawImage size 50x50 100x100 Context transform 0.5x 1x 2x 5x function loadImg(url, width, height) { return new Promise(function(resolve, reject) { var img = new Image(); if (width) { img.width = width; } if (height) { img.height = height; } img.src = url; img.onload = function() { resolve(img); }; img.onerror = function() { reject(Error('Image load failed')) }; }); } (function() { var svgImgs = { fixed50: '/c/fixed50-12375302.svg', fixed100: '/c/fixed100-728ffdaf.svg', viewbox50: '/c/viewbox50-c20857fc.svg', viewbox100: '/c/viewbox100-7c8fbd8a.svg', }; function createCanvas(width, height) { var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; return canvas; } function drawImgOnCanvas(canvas, img, width, height, scale) { var context = canvas.getContext('2d'); context.scale(scale, scale); context.drawImage(img, 0, 0, width, height); } var svgTestForm = document.querySelector('.svg-test-form'); var svgTestOutput = document.querySelector('.svg-test-output'); function processForm() { var svgSize = svgTestForm.querySelector('[name=img-type]:checked').value; var imgSize = Number(svgTestForm.querySelector('[name=img-size]:checked').value); var imgCssSize = Number(svgTestForm.querySelector('[name=img-css-size]:checked').value); var addImgToDom = !!svgTestForm.querySelector('[name=add-to-dom]:checked'); var useViewbox = !!svgTestForm.querySelector('[name=use-viewbox]:checked'); var canvasSize = Number(svgTestForm.querySelector('[name=canvas-size]:checked').value); var drawImageSize = Number(svgTestForm.querySelector('[name=drawimage-size]:checked').value); var contextTransform = Number(svgTestForm.querySelector('[name=context-transform]:checked').value); var imgUrl; if (useViewbox) { imgUrl = svgImgs['viewbox' + svgSize]; } else { imgUrl = svgImgs['fixed' + svgSize]; } svgTestOutput.innerHTML = ''; loadImg(imgUrl, imgSize, imgSize).then(img => { var el = img; if (imgCssSize) { img.style.width = img.style.height = imgCssSize + 'px'; } if (addImgToDom) { document.body.appendChild(img); } el = createCanvas(canvasSize, canvasSize); drawImgOnCanvas(el, img, drawImageSize, drawImageSize, contextTransform); if (addImgToDom) { document.body.removeChild(img); } svgTestOutput.appendChild(el); }); } processForm(); svgTestForm.addEventListener('change', function() { processForm(); }); }()); As far as I can tell, here's what the browsers are doing: Chrome Chrome goes for the width/height attributes specified in the SVG document. This means if the SVG document says width="50", you'll get the media queries for a 50px wide viewport. If you wanted to draw it using the media queries for a 100px wide viewport, tough luck. No matter what size you draw it to the canvas, it'll draw using the media queries for a 50px width. However, if the SVG specifies a viewBox rather than a fixed width, Chrome uses the pixel-data width of the <canvas> as the viewport width. You could argue this is similar to how things work with inline SVG, where the viewport is the whole window, but switching behaviours based on viewBox is really odd. Chrome wins the bizarro-pants award for "wonkiest behaviour". Safari Like Chrome, Safari uses the size specified in the SVG document, with the same downsides. But if the SVG uses a viewBox rather than a fixed width, it calculates the width from the viewBox, so an SVG with viewBox="50 50 200 200" would have a width of 150. So, less bizarre than Chrome, but still really restrictive. Firefox Firefox uses the width/height specified in the drawImage call, multiplied by any context transforms. This means if you draw your SVG so it's 300 canvas pixels wide, it'll have a viewport width of 300px. This kinda reflects their weird <img> behaviour - it's based on pixels drawn. This means you'll get the same density inconsistencies if you multiply your canvas width and height by devicePixelRatio (and scale back down with CSS), which you should do to avoid blurriness on high-density screens: <img>, <canvas>, multiplied <canvas>. Without multiplication, the canvas will appear blurry on high-density screens (function() { loadImg('/c/text-3da2c21e.svg').then(function(img) { var canvas = document.querySelector('.text-canvas'); var context = canvas.getContext('2d'); context.drawImage(img, 0, 0); }); loadImg('/c/text-3da2c21e.svg').then(function(img) { var canvas = document.querySelector('.text-canvas-sharp'); canvas.style.width = canvas.width + 'px'; canvas.style.height = canvas.height + 'px'; canvas.width = canvas.width * devicePixelRatio; canvas.height = canvas.height * devicePixelRatio; var context = canvas.getContext('2d'); context.drawImage(img, 0, 0, canvas.width, canvas.height); }); }()); There's logic to what Firefox is doing, but it means your media queries are tied to pixels drawn. Microsoft Edge Edge uses the layout size of the <img> to determine the viewport. If the <img> doesn't have layout (display:none or not in the document tree) then it falls back to the width/height attributes, if it doesn't have those it falls to the intrinsic dimensions of the <img>. This means you can draw the SVG at 1000x1000, but if the image is <img width="100">, it'll have a viewport width of 100px. In my opinion this is ideal. It means you can activate media queries for widths independent of the drawn width. It also feels consistent with responsive images. When you draw an <img srcset="…" sizes="…"> to a canvas, all browsers agree that the drawn image should be the resource currently selected by the <img>. Phew! I've filed an issue with the spec to adopt Edge's behaviour, and proposed an addition to createImageBitmap so the viewport can be specified in script. Hopefully we can get a little more consistency across browsers here! For completeness, here's how I gathered the data, and here are the full results.

Service worker meeting notes

On July 28th-29th we met up in the Mozilla offices in Toronto to discuss the core service worker spec. I'll try and cover the headlines here. Before I get stuck in to the meaty bits of the meeting, our intent here is to do what's best for developers and the future of the web, so if you disagree with any of this, please make your voice heard. You can do this in the relevent GitHub issue, in the comments below, or email me if you don't want to comment publicly. Attendees Mozilla - Ben Kelly, Ehsan Akhgari, Andrew Sutherland, Tom Tung Microsoft - Ali Alabbas Apple - Brady Eidson, Theresa O'Connor, Sam Weinig Google - Jake Archibald, Marijn Kruisselbrink, Kenji Baheux Samsung - Jungkee Song Facebook - Nathan Schloss Multiple service worker instances for parallelisation This is the big one, so if you only have time to give feedback on one thing, make it this thing. The service worker shuts down when it isn't in use to save resources, this means its global scope is unreliable. At the moment implementations will only run one instance of your active worker at once. Change: We're considering allowing the browser to run multiple concurrent instances of a service worker. If your page makes hundreds of requests (which may be best-practice in HTTP/2) we have to dispatch hundreds of fetch events. We don't have to wait for a response to each before we dispatch the next, but JavaScript is still single threaded, so we have to wait for each event callback to return before we can dispatch the next. This isn't true if we can run multiple instances of the service worker. Apple and Microsoft are particularly keen on this from a performance point of view, and Mozilla feel it would simplify their implementation. This is an idea we've been throwing around for years, but given this isn't how browsers currently work, it could break some existing assumptions. Although the global scope is unreliable over medium periods of time, events that happen in quick succession are (currently) likely to happen in the same service worker instance, so the global scope becomes somewhat reliable. This multi-instance change would break that. If you need a single global to coordinate things, you could use SharedWorker. No browser supports SharedWorker inside a service worker yet, but it's on our TODO list. The shared worker would shut down once all of its connected clients shut down, so it would have a lifecycle similar to service workers today. Over to you So what we want to know is: Are you maintaining global state within the service worker? Would this change break your stuff? If so, could you use a more persistent storage like shared workers / IndexedDB instead? Answers on the issue please! Changing update-checking to behave as expected We respect the max-age on service worker scripts & their imported scripts, but only up to a maximum of 24 hours to avoid lock-in. Respecting max-age seems to catch developers out, especially on places like GitHub pages where a max-age is set and you cannot change it. Some developers, notably from larger sites, find max-age useful for service worker scripts to stagger the rollout of new service workers. Change: We're planning to ignore max-age for service worker scripts & their imports, but provide an opt-in for the current behaviour. [issue] Also: When we check the SW for updates, we only check the top level script, not things imported via importScripts. This also seems to catch developers out, and service worker library developers find it particularly annoying because they lose control of the update cycle of their library. It's likely to catch more people out as we start using JS modules. Change: We're going to include imported scripts as part of the update check, so a service worker is considered 'new' if it or any of its imported scripts are byte different. [issue] Providing an opt-in concurrent fetch The service worker comes with a startup cost for the very first request. It's around 250ms right now, we can reduce it a lot, but it'll always be > 0. Native apps have a similar problem, and work around it by sending a simple UDP ping to the server while they're starting up. New feature: We could do something similar, like an opt-in concurrent fetch for navigations, which is surfaced as fetchEvent.concurrentResponse. Headers can be added to this, so the server knows it isn't a regular request. Here's a sketch of what the API could look like: In the page navigator.serviceWorker.register('/sw.js').then(reg => { // Yeah we'll come up with better names… reg.fetchManager.addNavigationConcurrentRequest({ addHeaders: {'Return-Middle-Only': 'Yep'} }); }); In the service worker self.addEventListener('fetch', event => { if (isArticleRequest) { const header = caches.match('/header.inc'); const footer = caches.match('/footer.inc'); // concurrentResponse is a promise const body = event.concurrentResponse || fetch(event.request, { headers: {'Return-Middle-Only': 'Yep'} }); const stream = createMergedStream(header, body, footer); event.respondWith(new Response(stream, { headers: {'Content-Type': 'text/html'} })); return; } }); Today, the fetch for the body of the page is delayed by the service worker startup & execution time. With this new feature it isn't, it can happen during SW startup, or maybe even during startup of large parts of the browser (while launching from homescreen). If you have any opinions on this, drop them in the issue. Fetch event & clients A "client" is a context with its own security policy, as in documents & workers. fetchEvent.clientId - the client ID that initiated the request. New feature: fetchEvent.reservedClientId - the client ID that the response will create (unless the response is a network error or Content-Disposition). fetchEvent.targetClientId - the client ID that this response will replace. At the moment there's only fetchEvent.clientId, which is the client ID that initiated the request or undefined for navigation fetches. This feels really inconsistent, so we're fixing that while providing developers with as much information as we can. In the case of: <a href="foo.html" target="bar">Click me!</a> …each client ID is different. In this case: self.addEventListener('fetch', event => { event.clientId; // the page containing the link element event.reservedClientId; // the client of foo.html's document event.targetClientId; // the client of whatever's in the window named "bar", if anything // postMessage to reserved clients is buffered until the client exists event.waitUntil( clients.get(event.reservedClientId) .then(client => client.postMessage("Hey there!")) ) }); Client IDs will be undefined if they don't apply (eg an <img> doesn't have a reserved or target client) or the client is cross-origin. If you have any comments, let us know! Dropping "opener" for service worker pages Following a target="_blank" link from an HTTP page to an HTTPS page means the HTTPS is not considered a secure context. This is because the HTTPS page has a channel back to the HTTP page via window.opener. This means the otherwise secure HTTPS page won't be able to use things like geolocation and service worker. This means, depending on how you get to your site, it may not work offline. We think this creates a really odd user experience. No browser does this yet, but it's what the Secure Contexts spec says. In future you'll be able to use a header to "disown" window.opener so you'll remain a secure context no matter how you were loaded. [issue] …unfortunately a response header is too late for service worker, since it needs to decide whether it should trigger a fetch event for this request. Change: If browsers go ahead and comply with this part of security contexts, we plan to auto-disown opener for service worker controlled pages if the opener is not a secure context, meaning the service worker (and geolocation and such) will work no matter how you're linked to. [issue] Wow I love bullet points. Handling failed requests If you respondWith to a navigation request and fail to provide a valid response, you'll get a browser error page. Change: We're going to help these errors be less generic. [issue] Also, it was suggested that browsers should auto-retry without the service worker in this case. [issue] Other developers I've spoken to don't like the "magic" of this. Instead we're going to leave this to browsers. If an error is generated in the service worker, the browser can provide a "Retry without service worker" button (using language that user would actually understand). Some thinking is happening: We're throwing around ideas for how to better recover from fetch errors. [issue] The main problem is that there's no easy way to know if the browser will happily take a response you give it. Eg things will fail post-service worker if you provide an opaque response to something that requires a non-opaque response (CORS). An isResponseAcceptable method would help here, or maybe a global fetcherror event is better. Fetches from within a service worker that should go via a fetch event At the moment all fetches made within a service worker bypass the service worker. This is how we avoid infinite loops. This isn't great for registration.showNotification(…), as icons should ideally work offline. Change: We're going to special-case notification so it triggers a fetch event. [issue] We may do the same for other APIs in future. Let us know if there's an API we should do this for! Range requests This is a pretty minor thing, but I find it interesting… We need to spec when browsers make ranged requests and which responses are acceptable, specifically in terms of security. All browsers should improve their handling of HTTP 200 responses when it comes to media. We're not making range-specific changes to cache API matching yet, but may in future. [issue] If you need to generate ranged responses from a full response, you already can. Change: We're going to prevent 206 responses entering the cache, we may later come up with special behaviour of this. [issue] Adding ranged requests & responses to the HTML spec The way browsers use ranges is completely unspecified, so we need to sort that out. Like most unspecced things, it's a bit of a cross-browser nightmare. Specifically, we should fix browsers so they accept HTTP 200 correctly in response to a ranged request, which the HTTP spec mandates, but I did some quick testing and the reality is all over the place. As part of this spec work, we need to prevent the service worker responding with multiple opaque ranges from different request URLs if this can be used to break the origin model. As in, if a CSS resource is requested in three ranged parts, we can't let those be: html { background: url(' Your private messages ');} …else you'd be able to use window.getComputedStyle to read someone's emails. The only web things I'm aware of that generate range requests are <audio> and <video> (and I guess resumable downloads), so it's not exploitable as above, but we need to spec when range requests can be made, and that a given resource won't accept ranges from cross-origin locations it wasn't expecting. Only the thing initiating the request can know this, so these changes need to be in the HTML spec, not the fetch or service worker spec. We've been fixing these security issues in browsers as we've implemented service worker, but the how-tos for this need to live in the spec rather than deep in the comments of GitHub issues. Yeah ok, maybe I'm the only person who finds this interesting. Minor things Change: caches.match(request) currently generates a HEAD response from a stored GET if request.method is HEAD. This isn't how the HTTP cache works, so it will now fail to match & resolve with undefined. [issue] Change: We're removing client.frameType until we get better use cases. client.ancestorOrigins solves a lot of similar cases and provides more detail. [issue] Brains be thinkin': Early ideas for a clientcontrolled event, since this can happen without a fetch when iframes are reattached to a document, or a page is fetched from the bfcache. Also a clientuncontrolled event to match. Let us know if this is useful to you & why. [issue] Change: We're removing fetchEvent.isReload in favour of fetchEvent.request.cache which communicates the browser's intended interaction with the HTTP cache. [issue] I dunno, does this count as a change?: We plan on splitting the cache API into its own spec. Hopefully this will help communicate that it's available on window objects too. We'll do this the next time we're making a major change/addition to the cache API. [issue] Possible change: We're debating the ordering of results in clients.matchAll, the intent is to return clients with the most relevent first, but the current specced behaviour (most-recently-focused) isn't easily doable for Apple. [issue] Change: Throwing within an event handler shouldn't have any special meaning. At the moment, in Chrome, a throw during the dispatch of the install event will prevent install happening, but no other event behaves like this so we're going to change it. I'm a little worried about this. [issue] Change: We're aware that copying a request with minor changes is tough right now & full of edge cases. We're working to make this easier. [issue] Related links Full meeting notes - we took notes in IRC, this is the log. Massive thanks to Kenji for note-taking! Agenda - follow links to the various issues to see resolutions, but I've summarised the important bits in this post Phew! I think I need a lie down after all that.

The performance benefits of rel=noopener

If you have links to another origin, you should use rel="noopener", especially if they open in a new tab/window. <a href="http://example.com" target="_blank" rel="noopener"> Example site </a> Without this, the new page can access your window object via window.opener. Thankfully the origin security model of the web prevents it reading your page, but no-thankfully some legacy APIs mean it can navigate your page to a different URL using window.opener.location = newURL. Web superhero Mathias Bynens wrote about this in detail, but I just discovered there's a performance benefit too. Demo Generate random numbers The random numbers act like a heartbeat for this page. If random numbers aren't being generated every frame, something is holding up the thread. Now click one of these to open a page that runs some expensive JavaScript: <a target="_blank"> <a target="_blank" rel="noopener"> Without rel="noopener", the random numbers are disrupted by the new page's JavaScript. Not only that, all main-thread activity is disrupted - try selecting text on the page. But with rel="noopener" the random numbers keep generating at 60fps. Well, in Chrome & Opera anyway. Update: Edge doesn't experience jank for either link because it doesn't support window.opener for _blank links. So why does this happen? Windows & processes Most browsers are multi-process with the exception of Firefox (and they're working on it). Each process has multiple threads, including what we often call the "main" thread. This is where parsing, style calculation, layout, painting and non-worker JavaScript runs. This means JavaScript running on one domain runs on a different thread to a window/tab running another domain. However, due to the synchronous cross-window access the DOM gives us via window.opener, windows launched via target="_blank" end up in the same process & thread. The same is true for iframes and windows opened via window.open. rel="noopener" prevents window.opener, so there's no cross-window access. Chromium browsers optimise for this and open the new page in its own process. Site isolation Here in Chrome HQ we're looking at moving cross-domain iframes and new windows into their own process even if they don't have rel="noopener". This means the limited cross-window access will become asynchronous, but the benefit is improved security and performance. In the meantime, rel="noopener" gives you the performance & security benefit today! Fun fact: Note I talk about "domain" above rather than "origin". This is because the somewhat frightening document.domain allows two domains to synchronously become part of the same origin. Ugh.

Caching best practices & max-age gotchas

Getting caching right yields huge performance benefits, saves bandwidth, and reduces server costs, but many sites half-arse their caching, creating race conditions resulting in interdependent resources getting out of sync. The vast majority of best-practice caching falls into one of two patterns: Pattern 1: Immutable content + long max-age Cache-Control: max-age=31536000 The content at this URL never changes, therefore… The browser/CDN can cache this resource for a year without a problem Cached content younger than max-age seconds can be used without consulting the server Page: Hey, I need "/script-v1.js", "/styles-v1.css" and "/cats-v1.jpg" 10:24 Cache: I got nuthin', how about you Server? 10:24 Server: Sure, here you go. Btw Cache: these are fine to use for like, a year. 10:25 Cache: Thanks! 10:25 Page: Cheers! 10:25 The next day Page: Hey, I need "/script-v2.js", "/styles-v2.css" and "/cats-v1.jpg" 08:14 Cache: I've got the cats one, here you go. I don't have the others. Server? 08:14 Server: Sure, here's the new CSS & JS. Btw Cache: these are also fine to use for a year. 08:15 Cache: Brilliant! 08:15 Page: Thanks! 08:15 Later Cache: Hm, I haven't used "/script-v1.js" & "/styles-v1.css" for a while. I'll just delete them. 12:32 In this pattern, you never change content at a particular URL, you change the URL: <script src="/script-f93bca2c.js"></script> <link rel="stylesheet" href="/styles-a837cb1e.css" /> <img src="/cats-0e9a2ef4.jpg" alt="…" /> Each URL contains something that changes along with its content. It could be a version number, the modified date, or a hash of the content - which is what I do on this blog. Most server-side frameworks come with tools to make this easy (I use Django's ManifestStaticFilesStorage), and there are smaller Node.js libraries that achieve the same thing, such as gulp-rev. However, this pattern doesn't work for things like articles & blog posts. Their URLs cannot be versioned and their content must be able to change. Seriously, given the basic spelling and grammar mistakes I make, I need to be able to update content quickly and frequently. Pattern 2: Mutable content, always server-revalidated Cache-Control: no-cache The content at this URL may change, therefore… Any locally cached version isn't trusted without the server's say-so Page: Hey, I need the content for "/about/" and "/sw.js" 11:32 Cache: I can't help you. Server? 11:32 Server: Yep, I've got those, here you go. Cache: you can hold onto these, but don't use them without asking. 11:33 Cache: Sure thing! 11:33 Page: Thanks! 11:33 The next day Page: Hey, I need content for "/about/" and "/sw.js" again 09:46 Cache: One moment. Server: am I good to use my copies of these? My copy of "/about/" was last updated on Monday, and "/sw.js" was last updated yesterday. 09:46 Server: "/sw.js" hasn't changed since then… 09:47 Cache: Cool. Page: here's "/sw.js". 09:47 Server: …but "/about/" has, here's the new version. Cache: like before, you can hold onto this, but don't use it without asking. 09:47 Cache: Got it! 09:47 Page: Excellent! 09:47 Note: no-cache doesn't mean "don't cache", it means it must check (or "revalidate" as it calls it) with the server before using the cached resource. no-store tells the browser not to cache it at all. Also must-revalidate doesn't mean "must revalidate", it means the local resource can be used if it's younger than the provided max-age, otherwise it must revalidate. Yeah. I know. In this pattern you can add an ETag (a version ID of your choosing) or Last-Modified date header to the response. Next time the client fetches the resource, it echoes the value for the content it already has via If-None-Match and If-Modified-Since respectively, allowing the server to say "Just use what you've already got, it's up to date", or as it spells it, "HTTP 304". If sending ETag/Last-Modified isn't possible, the server always sends the full content. This pattern always involves a network fetch, so it isn't as good as pattern 1 which can bypass the network entirely. It's not uncommon to be put off by the infrastructure needed for pattern 1, but be similarly put off by the network request pattern 2 requires, and instead go for something in the middle: a smallish max-age and mutable content. This is a baaaad compromise. max-age on mutable content is often the wrong choice …and unfortunately it isn't uncommon, for instance it happens on Github pages. Imagine: /article/ /styles.css /script.js …all served with: Cache-Control: must-revalidate, max-age=600 Content at the URLs changes If the browser has a cached version less than 10 minutes old, use it without consulting the server Otherwise make a network fetch, using If-Modified-Since or If-None-Match if available Page: Hey, I need "/article/", "/script.js" and "/styles.css" 10:21 Cache: Nothing here, Server? 10:21 Server: No problem, here they are. Btw Cache: these are fine to use for the next 10 minutes. 10:22 Cache: Gotcha! 10:22 Page: Thanks! 10:22 6 minutes later Page: Hey, I need "/article/", "/script.js" and "/styles.css" again 10:28 Cache: Omg, I'm really sorry, I lost the "/styles.css", but I've got the others, here you go. Server: can you give me "/styles.css"? 10:28 Server: Sure thing, it's actually changed since you last asked for it. It's also fine to use for the next 10 minutes. 10:29 Cache: No problem. 10:29 Page: Thanks! Wait! Everything's broken!! What's going on? 10:29 This pattern can appear to work in testing, but break stuff in the real world, and it's really difficult to track down. In the example above, the server actually had updated HTML, CSS & JS, but the page ended up with the old HTML & JS from the cache, and the updated CSS from the server. The version mismatch broke things. Often, when we make significant changes to HTML, we're likely to also change the CSS to reflect the new structure, and update the JS to cater for changes to the style and content. These resources are interdependent, but the caching headers can't express that. Users may end up with the new version of one/two of the resources, but the old version of the other(s). max-age is relative to the response time, so if all the above resources are requested as part of the same navigation they'll be set to expire at roughly the same time, but there's still the small possibility of a race there. If you have some pages that don't include the JS, or include different CSS, your expiry dates can get out of sync. And worse, the browser drops things from the cache all the time, and it doesn't know that the HTML, CSS, & JS are interdependent, so it'll happily drop one but not the others. Multiply all this together and it becomes not-unlikely that you can end up with mismatched versions of these resources. For the user, this can result in broken layout and/or functionality. From subtle glitches, to entirely unusable content. Thankfully, there's an escape hatch for the user… A refresh sometimes fixes it If the page is loaded as part of a refresh, browsers will always revalidate with the server, ignoring max-age. So if the user is experiencing something broken because of max-age, hitting refresh should fix everything. Of course, forcing the user to do this reduces trust, as it gives the perception that your site is temperamental. A service worker can extend the life of these bugs Say you have the following service worker: const version = '2'; self.addEventListener('install', (event) => { event.waitUntil( caches .open(`static-${version}`) .then((cache) => cache.addAll(['/styles.css', '/script.js'])), ); }); self.addEventListener('activate', (event) => { // …delete old caches… }); self.addEventListener('fetch', (event) => { event.respondWith( caches .match(event.request) .then((response) => response || fetch(event.request)), ); }); This service worker… Caches the script and styles up front Serves from the cache if there's a match, otherwise goes to the network If we change our CSS/JS we bump the version to make the service worker byte-different, which triggers an update. However, since addAll fetches through the HTTP cache (like almost all fetches do), we could run into the max-age race condition and cache incompatible versions of the CSS & JS. Once they're cached, that's it, we'll be serving incompatible CSS & JS until we next update the service worker - and that's assuming we don't run into another race condition in the next update. You could bypass the cache in the service worker: self.addEventListener('install', (event) => { event.waitUntil( caches .open(`static-${version}`) .then((cache) => cache.addAll([ new Request('/styles.css', { cache: 'no-cache' }), new Request('/script.js', { cache: 'no-cache' }), ]), ), ); }); Unfortunately the cache options aren't yet supported in Chrome/Opera and only recently landed in Firefox Nightly, but you can sort-of do it yourself: self.addEventListener('install', (event) => { event.waitUntil( caches.open(`static-${version}`).then((cache) => Promise.all( ['/styles.css', '/script.js'].map((url) => { // cache-bust using a random query string return fetch(`${url}?${Math.random()}`).then((response) => { // fail on 404, 500 etc if (!response.ok) throw Error('Not ok'); return cache.put(url, response); }); }), ), ), ); }); In the above I'm cache-busting with a random number, but you could go one step further and use a build-step to add a hash of the content (similar to what sw-precache does). This is kinda reimplementing pattern 1 in JavaScript, but only for the benefit of service worker users rather than all browsers & your CDN. The service worker & the HTTP cache play well together, don't make them fight! As you can see, you can hack around poor caching in your service worker, but you're way better off fixing the root of the problem. Getting your caching right makes things easier in service worker land, but also benefits browsers that don't support service worker (Safari, IE/Edge), and lets you get the most out of your CDN. Correct caching headers means you can massively streamline service worker updates too: const version = '23'; self.addEventListener('install', (event) => { event.waitUntil( caches .open(`static-${version}`) .then((cache) => cache.addAll([ '/', '/script-f93bca2c.js', '/styles-a837cb1e.css', '/cats-0e9a2ef4.jpg', ]), ), ); }); Here I'd cache the root page using pattern 2 (server revalidation), and the rest of the resources using pattern 1 (immutable content). Each service worker update will trigger a request for the root page, but the rest of the resources will only be downloaded if their URL has changed. This is great because it saves bandwidth and improves performance whether you're updating from the previous version, or 10 versions ago. This is a huge advantage over native, where the whole binary is downloaded even for a minor change, or involves complex binary diffing. Here we can update a large web app with relatively little download. Service workers work best as an enhancement rather than a workaround, so instead of fighting the cache, work with it! Used carefully, max-age & mutable content can be beneficial max-age on mutable content is often the wrong choice, but not always. For instance this page has a max-age of three minutes. Race conditions aren't an issue here because this page doesn't have any dependencies that follow the same caching pattern (my CSS, JS & image URLs follow pattern 1 - immutable content), and nothing dependent on this page follows the same pattern. This pattern means, if I'm lucky enough to write a popular article, my CDN (Cloudflare) can take the heat off my server, as long as I can live with it taking up to three minutes for article updates to be seen by users, which I am. This pattern shouldn't be used lightly. If I added a new section to one article and linked to it in another article, I've created a dependency that could race. The user could click the link and be taken to a copy of the article without the referenced section. If I wanted to avoid this, I'd update the first article, flush Cloudflare's cached copy using their UI, wait three minutes, then add the link to it in the other article. Yeah… you have to be pretty careful with this pattern. Used correctly, caching is a massive performance enhancement and bandwidth saver. Favour immutable content for any URL that can easily change, otherwise play it safe with server revalidation. Only mix max-age and mutable content if you're feeling brave, and you're sure your content has no dependancies or dependents that could get out of sync.

Streaming template literals

Template literals are pretty cool right? const areThey = 'Yes'; console.log(`${areThey}, they are`); // Logs: Yes, they are You can also assign a function to process the template, known as "tagged" templates: function strongValues(strings, ...values) { return strings.reduce((totalStr, str, i) => { totalStr += str; if (i in values) totalStr += `<strong>${values[i]}</strong>`; return totalStr; }, ''); } const areThey = 'Yes'; console.log(strongValues`${areThey}, they are`); // Logs: <strong>Yes</strong>, they are The syntax for tagging a template seems really un-JavaScripty to me, and I haven't been able to figure out why strings is an array yet each of the values is a separate argument, but meh, it's a cool feature. You don't even have to return a string: function daftTag() { return [1, 2, 3]; } console.log(daftTag`WHY ARE YOU IGNORING ME?`); // Logs: [1, 2, 3] "But how can we involve streams in this?" I hear me cry Generating streams in a service worker allows you to serve a mixture of cached & network content in a single response. This is amazing for performance, but manually combining the streams feels a bit, well, manual. Say we wanted to output: <h1>Title</h1> …content… …where the title and content come from different sources. That would look like this: const stream = new ReadableStream({ start(controller) { const encoder = new TextEncoder(); // Promise for the title const titlePromise = fetch('/get-metadata') .then(r => r.json()).then(data => data.title); // Promise for the content stream const contentPromise = fetch('/get-content') .then(r => r.body); // Tie them all together pushString('<h1>'); titlePromise .then(pushString) .then(() => pushString('</h1>\n')) .then(() => contentPromise) .then(pushStream) .then(() => controller.close()); // Helper functions function pushString(str) { controller.enqueue(encoder.encode(str)); } function pushStream(stream) { // Get a lock on the stream var reader = stream.getReader(); return reader.read().then(function process(result) { if (result.done) return; // Push the value to the combined stream controller.enqueue(result.value); // Read more & process return reader.read().then(process); }); } } }); Ew. Imagine we could just do this: // Promise for the title const title = fetch('/get-metadata') .then(r => r.json()).then(data => data.title); // Promise for the content stream const content = fetch('/get-content').then(r => r.body); const stream = templateStream` <h1>${title}</h1> ${content} `; Well, you can! View demo Note: You'll need Chrome Canary with chrome://flags/#enable-experimental-web-platform-features enabled. This means the title can be displayed as soon as it arrives, without waiting for the main content. The main content stream is piped, meaning it can render as it downloads. The implementation of templateStream is pretty light too, making it a cheap and easy way of building streams from multiple sources. If you're wanting something with a few more features (eg conditionals & iteration), DustJS is the only template engine I'm aware of that supports this, but I'm not a fan of the syntax. Hopefully we'll see other template engines such as handlebars adopt a similar model, although it's not something they're interested in right now.

Control CSS loading with custom properties

Last week I wrote about a simple method to load CSS progressively, and on the very same day some scientists taught gravity how to wave. Coincidence? Yes. The pattern in the previous post covers the 90% case of multi-stage CSS loading, and it's really simple to understand. But would you like to hear about a pattern that covers ~100% and is absurdly complicated? Well take my hand, and follow me into the next paragraph… The missing 10% <link>-in-<body> blocks the parser while CSS loads, meaning that rendering is blocked for all subsequent content. Taking the demo page from last week's article, rendering is split into the following phases: Mobile 1 header 2 main 3 comments 4 about-me 5 footer Desktop 1 header 2 main 3 comments 4 about-me 5 footer This is perfect on mobile, where each section's CSS blocks itself and all following sections, but on desktop the CSS for main and comments in the left column blocks rendering of the about-me right column, even if about-me's CSS loads first. This is because the blocking is based on source order, but in this design it'd be fine for the right column to display before the left column. We want to build a dependency tree where each element is render-blocked until other specific elements have rendered. Also, dependencies need to be able to change when viewport width changes. Sounds fun right? We can do this with CSS custom properties… CSS custom properties MDN has a great article on CSS custom properties, but for the sake of this article, here's all you need to know… html { background: var(--gloop, red); } Here we're asking the page background to be the value of custom property --gloop, else fallback to red. As a result, the background is red. But if we added: :root { --gloop: green; } …we've set the custom property --gloop to green, so now the page background is green. But if we added: :root { --gloop: initial; } initial gets special treatment here. It effectively unsets --gloop, so now the page background is back to red. Building a rendering dependency tree with CSS custom properties Writing that heading made me feel really smart. The HTML <head> <link rel="stylesheet" href="/initial.css"> <script> [ '/main.css', '/comments.css', '/about-me.css', '/footer.css' ].map(url => { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = url; document.head.appendChild(link); }); </script> </head> So we load /initial.css via a <link>, or we could inline it, as long as it blocks rendering. But we load all the other stylesheets asynchronously. initial.css main, .comments, .about-me, footer { display: none; } :root { --main-blocker: none; --comments-blocker: none; --about-me-blocker: none; --footer-blocker: none; } /* Rest of the initial styles… */ We hide all the sections we're not ready to render yet, then create a "blocker" custom property for each section. main.css :root { --main-blocker: initial; } main { display: var(--main-blocker, block); } /* Rest of the main styles… */ The main content doesn't have any render dependencies. As soon as the CSS loads it unsets its blocker (using initial) and displays itself. comments.css :root { --comments-blocker: var(--main-blocker); } .comments { display: var(--comments-blocker, block); } /* Rest of the comment styles… */ The comments shouldn't render before the main content, so the comments blocker is linked to --main-blocker. .comments becomes visible once this CSS loads and --main-blocker is unset. about-me.css :root { --about-me-blocker: var(--comments-blocker); } .about-me { display: var(--about-me-blocker, block); } Similar to above, .about-me depends on its own CSS and the comments. But when the page is wider it's displayed in two columns, so we no longer want .about-me to depend on the comments in terms of rendering: @media (min-width: 600px) { :root { --about-me-blocker: initial; } } /* Rest of the about-me styles… */ Done! When the viewport width is over 600px, .about-me displays as soon as its CSS loads. footer.css :root { --footer-blocker: var(--main-blocker, var(--about-me-blocker)); } footer { display: var(--footer-blocker, block); } /* Rest of the styles… */ The footer shouldn't render until both the main content & about-me sections have rendered. To achieve this, --footer-blocker gets its value from --main-blocker, but once --main-blocker is unset it falls back to getting its value from --about-me-blocker. Demo View demo - requires Chrome Canary or Firefox. In this demo the CSS is loaded async, with each sheet taking randomly between 0-5 seconds to load. Despite this, the page never renders out of order, and each section renders as soon as possible depending on the browser width. But… is it practical? It's way more complicated than progressive CSS, with very little gain, and has a huge backwards compatibility problem. But it does show the power of CSS custom properties, you can't do this with compile-time solutions like Sass variables. If you really want to do something like this today, you can get most of the way there by async loading your CSS with loadCSS, and adding class names to <html> as particular styles load (view demo), although this lands you with a lot of specificity issues to hack through. I think this post should be filed under "fun demo", but we're only just starting to discover the power of CSS custom properties. Thanks to Remy Sharp for corrections. Will I ever post an article without spelling mistakes? Noep.

The future of loading CSS

Chrome is intending to change the behaviour of <link rel="stylesheet">, which will be noticeable when it appears within <body>. The impact and benefits of this aren't clear from the blink-dev post, so I wanted to go into detail here. Update: This is now in Chrome Canary. The current state of loading CSS <head> <link rel="stylesheet" href="/all-of-my-styles.css" /> </head> <body> …content… </body> CSS blocks rendering, leaving the user staring at a white screen until all-of-my-styles.css fully downloads. It's common to bundle all of a site's CSS into one or two resources, meaning the user downloads a large number of rules that don't apply to the current page. This is because sites can contain many types of pages with a variety of "components", and delivering CSS at a component level hurts performance in HTTP/1. This isn't the case with SPDY and HTTP/2, where many smaller resources can be delivered with little overhead, and independently cached. <head> <link rel="stylesheet" href="/site-header.css" /> <link rel="stylesheet" href="/article.css" /> <link rel="stylesheet" href="/comment.css" /> <link rel="stylesheet" href="/about-me.css" /> <link rel="stylesheet" href="/site-footer.css" /> </head> <body> …content… </body> This fixes the redundancy issue, but it means you need to know what the page will contain when you're outputting the <head>, which can prevent streaming. Also, the browser still has to download all the CSS before it can render anything. A slow loading /site-footer.css will delay the rendering of everything. View demo. The current state-of-the-art of loading CSS <head> <script> // https://github.com/filamentgroup/loadCSS !(function (e) { 'use strict'; var n = function (n, t, o) { function i(e) { return f.body ? e() : void setTimeout(function () { i(e); }); } var d, r, a, l, f = e.document, s = f.createElement('link'), u = o || 'all'; return ( t ? (d = t) : ((r = (f.body || f.getElementsByTagName('head')[0]).childNodes), (d = r[r.length - 1])), (a = f.styleSheets), (s.rel = 'stylesheet'), (s.href = n), (s.media = 'only x'), i(function () { d.parentNode.insertBefore(s, t ? d : d.nextSibling); }), (l = function (e) { for (var n = s.href, t = a.length; t--; ) if (a[t].href === n) return e(); setTimeout(function () { l(e); }); }), s.addEventListener && s.addEventListener('load', function () { this.media = u; }), (s.onloadcssdefined = l), l(function () { s.media !== u && (s.media = u); }), s ); }; 'undefined' != typeof exports ? (exports.loadCSS = n) : (e.loadCSS = n); })('undefined' != typeof global ? global : this); </script> <style> /* The styles for the site header, plus: */ .main-article, .comments, .about-me, footer { display: none; } </style> <script> loadCSS('/the-rest-of-the-styles.css'); </script> </head> <body></body> In the above, we have some inline styles to get us a fast initial render, plus hide the stuff we don't have styles for yet, then load the rest of the CSS async using JavaScript. The rest of the CSS would override the display: none on .main-article etc. This method is recommended by performance experts as a way to get a fast first render, and with good reason: View demo. In the real world, I did this wiki-offline, and it worked a treat: 0.6s faster first render on 3g. Full results before vs after. But there are a couple of shortfalls: It requires a (small) JavaScript library Unfortunately this is due to WebKit's implementation. As soon as a <link rel="stylesheet"> is added to the page, WebKit blocks rendering until the sheet loads, even if the sheet was added with JavaScript. In Firefox and IE/Edge, JS-added stylesheets load entirely async. Chrome stable currently has the WebKit behaviour, but in Canary we've switched to the Firefox/Edge behaviour. You're restricted to two phases of loading In the pattern above, inline CSS hides unstyled content using display:none, then the async-loaded CSS reveals it. If you scale this to two or more CSS files they're likely to load out-of-order, resulting in content shifting around as it loads: View demo. Content shifting around is right up there with pop-up ads in terms of user frustration. Kill it with fire. Since you're restricted to two phases of loading, you have to decide what's in your quick first render and what's in the rest. You want "above the fold" content in the quick render of course, but "the fold" is viewport dependent. Well, tough shitties, you've got to pick a one-size-fits-all solution. Update: If you want to make things really complicated, you can build a kind-of rendering dependency tree using CSS custom properties. A simpler, better way <head> </head> <body> <!-- HTTP/2 push this resource, or inline it, whichever's faster --> <link rel="stylesheet" href="/site-header.css" /> <header>…</header> <link rel="stylesheet" href="/article.css" /> <main>…</main> <link rel="stylesheet" href="/comment.css" /> <section class="comments">…</section> <link rel="stylesheet" href="/about-me.css" /> <section class="about-me">…</section> <link rel="stylesheet" href="/site-footer.css" /> <footer>…</footer> </body> The plan is for each <link rel="stylesheet"> to block rendering of subsequent content while the stylesheet loads, but allow the rendering of content before it. The stylesheets load in parallel, but they apply in series. This makes <link rel="stylesheet"> behave similar to <script src="…"></script>. Let's say the site-header, article, and footer CSS have loaded, but the rest are still pending, here's how the page would look: Header: rendered Article: rendered Comments: not rendered, CSS before it hasn't loaded yet (/comment.css) About me: not rendered, CSS before it hasn't loaded yet (/comment.css) Footer: not rendered, CSS before it hasn't loaded yet (/comment.css), even though its own CSS has loaded This gives you a sequential render of the page. You don't need decide what's "above the fold", just include a page component's CSS just before the first instance of the component. It's fully streaming compatible, because you don't need to output the <link> until just before you need it. You need to take care when using layout systems where content dictates layout (such as tables & flexbox) to avoid content-shifting during load. This isn't a new problem, but progressive rendering can expose it more frequently. You can hack flexbox to behave, but CSS grid is a much better system for overall page layout (flexbox is still great for smaller components). Changes to Chrome The HTML spec doesn't cover how page rendering should be blocked by CSS, and it discourages <link rel="stylesheet"> in the body, but all browsers allow it. Of course, they all deal with link-in-body in their own way: Chrome & Safari: Stops rendering as soon as the <link rel="stylesheet"> is discovered, and won't render until all discovered stylesheets have loaded. This often results in unrendered content above the <link> being blocked. Firefox: <link rel="stylesheet"> in the head blocks rendering until all discovered stylesheets have loaded. <link rel="stylesheet"> in the body does not block rendering unless a stylesheet in the head is already blocking rendering. This can result in a flash of unstyled content (FOUC). IE/Edge: Blocks the parser until the stylesheet loads, but allows content above the <link> to render. At Chrome, we like the IE/Edge behaviour, so we're going to align with it. This allows the progressive rendering pattern of CSS described above. We're working on getting this into the spec, starting with allowing <link> in <body>. The current Chrome/Safari behaviour is backwards compatible, it just ends up blocking rendering for longer than it needs to. The Firefox behaviour is slightly more complicated, but there's a workaround… Firefixing Because Firefox doesn't always block rendering for link-in-body, we'll need to work around it a bit to avoid a FOUC. Thankfully this is pretty easy, as <script> blocks parsing, but also waits for pending stylesheets to load: <link rel="stylesheet" href="/article.css" /> <script></script> <main>…</main> The script elements have to be non-empty for this to work, a space is good enough. See it in action! View demo. Firefox & Edge/IE will give you a lovely progressive render, whereas Chrome & Safari give you a white screen until all the CSS loads. The current Chrome/Safari behaviour is no worse than putting all your stylesheets in the <head>, so you can start using this method today. Over the coming months, Chrome will move to the Edge model, and more users will get a faster render. So there we go! A much simpler way to load only the CSS you need, and get a faster render in the process. Thanks to @lukedjn, Paul Lewis, and Domenic Denicola for corrections. Thanks in particular to Boris Zbarsky who helped me understand the Firefox behaviour. Also Paul came up with the "Firefixing" joke but I'm hoping you're not reading this so I can take full credit myself.

Service workers and base URIs

Previously when we've run into a contentious service worker design issue, we've asked web developers what they think. This has worked out pretty well in the past, with developer feedback directly informing spec changes. It's also great because we can blame y'all if you pick the wrong thing. Well, it's that time again! Help us! Making a regular request In a service worker fetch event, if you don't call event.respondWith within the event callback the browser handles the request and response as it usually would. However, you can also do: self.addEventListener('fetch', (event) => { event.respondWith(fetch(event.request)); }); Our goal is for the above to behave as much as possible as if the service worker wasn't there. This means you can decide whether to do the normal browser thing asynchronously, or do the normal browser thing then do something with the response (eg cache it), without picking up weird behavioral changes. CSS & base URIs CSS is unique in that it's a subresource with its own base URI. Documents and workers also have their own base URI, but they aren't subresources. The base URI of a CSS resource is its final response URL. The page and request URL are not involved in selecting CSS's base URI. So if https://example.com/hello/ contains: <!DOCTYPE html> <link rel="stylesheet" href="/foo.css" /> And /foo.css redirects to https://static.example.com/foo/style.css which contains: html { background: url('bar.jpg'); } The background's full URL is https://static.example.com/foo/bar.jpg. But what about with a service worker? If https://example.com/question1/ contains: <!DOCTYPE html> <link rel="stylesheet" href="/bar.css" /> …and is controlled by a service worker containing: self.addEventListener('fetch', (event) => { if (event.request.url.endsWith('/bar.css')) { event.respondWith(fetch('https://static.example.com/bar/style.css')); } }); …and https://static.example.com/bar/style.css contains: html { background: url('yo.jpg'); } What should the full URL of the background image be when the user loads https://example.com/question1/? If your answer differs from the "base URI of a CSS resource is its response URL" behaviour that occurs without service workers (therefore breaking our current rule of event.respondWith(fetch(event.request)) behaving as if the service worker isn't present), why? Would you expect the same behaviour if fetch was replaced with caches.match in the above example? (assuming the resource is cached) This isn't a quiz about current behaviour, your answer doesn't need to be what the spec says, or what a browser currently does. Pages & base URIs Pages also have a base URI. It's independent to the URL in the browser's location bar, as the base URI can be set using <base>. But what about with a service worker? If https://example.com/question2/ is controlled by the following service worker: self.addEventListener('fetch', (event) => { if (event.request.url.endsWith('/question2/')) { event.respondWith(fetch('https://static.example.com/foo/page-shell.html')); } }); …and https://static.example.com/foo/page-shell.html contains: <!DOCTYPE html> <a href="bar/">Click me!</a> What is displayed in the URL bar when the user loads https://example.com/question2/? (assuming the full URL is shown) Where does the user go when they click the link? If your answer differs from the CSS behaviour, why? Would you expect the same behaviour if fetch was replaced with caches.match in the above example? (assuming the resource is cached) Again, this isn't a quiz about current behaviour, your answer doesn't need to be what the spec says, or what a browser currently does. Over to you! Pop your answer in the comments below, with as much reasoning as you can bare. Thank you! We'd like an answer for each question so we know we're representing you properly, you can use the following template in Disqus-friendly format: <strong>CSS:</strong> <blockquote> What should the full URL of the background image be when the user loads <code>https://example.com/question1/</code>? </blockquote> -Your answer- <blockquote> If your answer differs from the "base URI of a CSS resource is its response URL" behaviour that occurs without service workers (therefore breaking our current rule of <code>event.respondWith(fetch(event.request))</code> behaving as if the service worker isn't present), why? </blockquote> -Your answer- <blockquote> Would you expect the same behaviour if <code>fetch</code> was replaced with <code>caches.match</code> in the above example? (assuming the resource is cached) </blockquote> -Your answer- <strong>Pages:</strong> <blockquote> What is displayed in the URL bar when the user loads <code>https://example.com/question2/</code>? (assuming the full URL is shown) </blockquote> -Your answer- <blockquote>Where does the user go when they click the link?</blockquote> -Your answer- <blockquote>If your answer differs from the CSS behaviour, why?</blockquote> -Your answer- <blockquote> Would you expect the same behaviour if <code>fetch</code> was replaced with <code>caches.match</code> in the above example? (assuming the resource is cached) </blockquote> -Your answer-

2016 - the year of web streams

Yeah, ok, it's a touch bold to talk about something being the thing of the year as early as January, but the potential of the web streams API has gotten me all excited. TL;DR: Streams can be used to do fun things like turn clouds to butts, transcode MPEG to GIF, but most importantly, they can be combined with service workers to become the fastest way to serve content. Streams, huh! What are they good for? Absolutely… some things. Promises are a great way to represent async delivery of a single value, but what about representing multiple values? Or multiple parts of larger value that arrives gradually? Say we wanted to fetch and display an image. That involves: Fetching some data from the network Processing it, turning it from compressed data into raw pixel data Rendering it We could do this one step at a time, or we could stream it: @font-face { font-family: 'Just Another Hand'; font-style: normal; font-weight: 400; src: url(https://fonts.gstatic.com/s/justanotherhand/v7/fKV8XYuRNNagXr38eqbRf8DbBFScDQWNirGEA9Q9Yto.woff) format('woff'); } .stream-diagram .color-in { fill: none; stroke: #0045d6; stroke-width: 16.6; stroke-opacity: 0.23; } .stream-diagram .color-in-heading { fill: none; stroke: #009800; stroke-width: 12.8; stroke-opacity: 0.23; } .stream-diagram .label { font-size: 60.47px; line-height: 125%; font-family: 'Just Another Hand'; fill: #000000; text-anchor: middle; } .stream-diagram .heading { font-size: 50.7px; } .stream-diagram .outline { fill: none; stroke: #000000; stroke-width: 9.1; } .stream-diagram .data-flow { fill: none; stroke: #00c871; stroke-linecap: round; } .stream-diagram .data-flow.stream { stroke-width: 9.6; } .stream-diagram .data-flow.buffer { stroke-width: 37.4; } .stream-diagram .buffer.fetch { stroke-dasharray: 0, 300; stroke-dashoffset: -15; } .animate.stream-diagram .buffer.fetch { animation: stream-diagram-buffer-fetch 5s linear infinite both; } .stream-diagram .buffer.process { stroke-dasharray: 0, 300; stroke-dashoffset: -40; } .animate.stream-diagram .buffer.process { animation: stream-diagram-buffer-process 5s linear infinite both; } .stream-diagram .stream.fetch { stroke-dasharray: 0, 16, 0, 16, 0, 16, 0, 16, 0, 16, 0, 16, 0, 16, 0, 16, 0, 16, 0, 300; stroke-dashoffset: 130; } .animate.stream-diagram .stream.fetch { animation: stream-diagram-stream-fetch 5s linear infinite both; } .stream-diagram .stream.process { stroke-dasharray: 0, 49.33, 0, 49.33, 0, 49.33, 0, 49.33, 0, 49.33, 0, 49.33, 0, 49.33, 0, 49.33, 0, 49.33, 0, 300; stroke-dashoffset: 427; } .animate.stream-diagram .stream.process { animation: stream-diagram-stream-process 5s linear infinite both; } @keyframes stream-diagram-buffer-fetch { 74% { stroke-dashoffset: -185; } to { stroke-dashoffset: -185; } } @keyframes stream-diagram-buffer-process { 76% { stroke-dashoffset: -40; } to { stroke-dashoffset: -205; } } @keyframes stream-diagram-stream-fetch { 74% { stroke-dashoffset: -156; } to { stroke-dashoffset: -156; } } @keyframes stream-diagram-stream-process { 36% { stroke-dashoffset: 427; } 85% { stroke-dashoffset: -150; } to { stroke-dashoffset: -150; } } Without streaming ProcessRenderFetch With streaming (function() { function debounce(func, delay) { var timeout; return function() { clearTimeout(timeout); timeout = setTimeout(func, delay); }; } var diagram = document.querySelector('.stream-diagram'); var animating = false; var checkForDiagramInView = debounce(function() { requestAnimationFrame(function() { var box = diagram.getBoundingClientRect(); var viewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0); if (animating) { if (box.top + box.height viewportHeight) { diagram.setAttribute('class', diagram.getAttribute('class').replace(' animate', '')); animating = false; } return; } if (box.top + box.height 0) { diagram.setAttribute('class', diagram.getAttribute('class') + ' animate'); animating = true; } }); }, 100); window.addEventListener('resize', checkForDiagramInView); window.addEventListener('scroll', checkForDiagramInView); }()); If we handle & process the response bit by bit, we get to render some of the image way sooner. We even get to render the whole image sooner, because the processing can happen in parallel with the fetching. This is streaming! We're reading a stream from the network, transforming it from compressed data to pixel data, then writing it to the screen. You could achieve something similar with events, but streams come with benefits: Start/end aware - although streams can be infinite Buffering of values that haven't been read - whereas events that happen before listeners are attached are lost Chaining via piping - you can pipe streams together to form an async sequence Built-in error handling - errors will be propagated down the pipe Cancellation support - and that cancellation message is passed back up the pipe Flow control - you can react to the speed of the reader That last one is really important. Imagine we were using streams to download and display a video. If we can download and decode 200 frames of video per second, but only want to display 24 frames a second, we could end up with a huge backlog of decoded frames and run out of memory. This is where flow control comes in. The stream that's handling the rendering is pulling frames from the decoder stream 24 times a second. The decoder notices that it's producing frames faster than they're being read, and slows down. The network stream notices that it's fetching data faster than it's being read by the decoder, and slows the download. Because of the tight relationship between stream & reader, a stream can only have one reader. However, an unread stream can be "teed", meaning it's split into two streams that receive the same data. In this case, the tee manages the buffer across both readers. Ok, that's the theory, and I can see you're not ready to hand over that 2016 trophy just yet, but stay with me. The browser streams loads of things by default. Whenever you see the browser displaying parts of a page/image/video as it's downloading, that's thanks to streaming. However, it's only recently, thanks to a standardisation effort, that streams are becoming exposed to script. Streams + the fetch API Response objects, as defined by the fetch spec, let you read the response as a variety of formats, but response.body gives you access to the underlying stream. response.body is supported in the current stable version of Chrome. Say I wanted to get the content-length of a response, without relying on headers, and without keeping the whole response in memory. I could do it with streams: // fetch() returns a promise that // resolves once headers have been received fetch(url).then((response) => { // response.body is a readable stream. // Calling getReader() gives us exclusive access to // the stream's content var reader = response.body.getReader(); var bytesReceived = 0; // read() returns a promise that resolves // when a value has been received return reader.read().then(function processResult(result) { // Result objects contain two properties: // done - true if the stream has already given // you all its data. // value - some data. Always undefined when // done is true. if (result.done) { console.log('Fetch complete'); return; } // result.value for fetch streams is a Uint8Array bytesReceived += result.value.length; console.log('Received', bytesReceived, 'bytes of data so far'); // Read some more, and call this function again return reader.read().then(processResult); }); }); View demo (1.3mb) The demo fetches 1.3mb of gzipped HTML from the server, which decompresses to 7.7mb. However, the result isn't held in memory. Each chunk's size is recorded, but the chunks themselves are garbage collected. result.value is whatever the creator of the stream provides, which can be anything: a string, number, date, ImageData, DOM element… but in the case of a fetch stream it's always a Uint8Array of binary data. The whole response is each Uint8Array joined together. If you want the response as text, you can use TextDecoder: var decoder = new TextDecoder(); var reader = response.body.getReader(); // read() returns a promise that resolves // when a value has been received reader.read().then(function processResult(result) { if (result.done) return; console.log(decoder.decode(result.value, { stream: true })); // Read some more, and recall this function return reader.read().then(processResult); }); {stream: true} means the decoder will keep a buffer if result.value ends mid-way through a UTF-8 code point, since a character like ♥ is represented as 3 bytes: [0xE2, 0x99, 0xA5]. TextDecoder is currently a little clumsy, but it's likely to become a transform stream in the future (once transform streams are defined). A transform stream is an object with a writable stream on .writable and a readable stream on .readable. It takes chunks into the writable, processes them, and passes something out through the readable. Using transform streams will look like this: Hypothetical future-code: var reader = response.body.pipeThrough(new TextDecoder()).getReader(); reader.read().then((result) => { // result.value will be a string }); The browser should be able to optimise the above, since both the response stream and TextDecoder transform stream are owned by the browser. Cancelling a fetch A stream can be cancelled using stream.cancel() (so response.body.cancel() in the case of fetch) or reader.cancel(). Fetch reacts to this by stopping the download. View demo (also, note the amazing random URL JSBin gave me). This demo searches a large document for a term, only keeps a small portion in memory, and stops fetching once a match is found. Anyway, this is all so 2015. Here's the fun new stuff… Creating your own readable stream In Chrome Canary with the "Experimental web platform features" flag enabled, you can now create your own streams. var stream = new ReadableStream( { start(controller) {}, pull(controller) {}, cancel(reason) {}, }, queuingStrategy, ); start is called straight away. Use this to set up any underlying data sources (meaning, wherever you get your data from, which could be events, another stream, or just a variable like a string). If you return a promise from this and it rejects, it will signal an error through the stream. pull is called when your stream's buffer isn't full, and is called repeatedly until it's full. Again, If you return a promise from this and it rejects, it will signal an error through the stream. Also, pull will not be called again until the returned promise fulfills. cancel is called if the stream is cancelled. Use this to cancel any underlying data sources. queuingStrategy defines how much this stream should ideally buffer, defaulting to one item - I'm not going to go into depth on this here, the spec has more details. As for controller: controller.enqueue(whatever) - queue data in the stream's buffer. controller.close() - signal the end of the stream. controller.error(e) - signal a terminal error. controller.desiredSize - the amount of buffer remaining, which may be negative if the buffer is over-full. This number is calculated using the queuingStrategy. So if I wanted to create a stream that produced a random number every second, until it produced a number > 0.9, I'd do it like this: var interval; var stream = new ReadableStream({ start(controller) { interval = setInterval(() => { var num = Math.random(); // Add the number to the stream controller.enqueue(num); if (num > 0.9) { // Signal the end of the stream controller.close(); clearInterval(interval); } }, 1000); }, cancel() { // This is called if the reader cancels, //so we should stop generating numbers clearInterval(interval); }, }); See it running. Note: You'll need Chrome Canary with chrome://flags/#enable-experimental-web-platform-features enabled. It's up to you when to pass data to controller.enqueue. You could just call it whenever you have data to send, making your stream a "push source", as above. Alternatively you could wait until pull is called, then use that as a signal to collect data from the underlying source and then enqueue it, making your stream a "pull source". Or you could do some combination of the two, whatever you want. Obeying controller.desiredSize means the stream is passing data along at the most efficient rate. This is known has having "backpressure support", meaning your stream reacts to the read-rate of the reader (like the video decoding example earlier). However, ignoring desiredSize won't break anything unless you run out of device memory. The spec has a good example of creating a stream with backpressure support. Creating a stream on its own isn't particularly fun, and since they're new, there aren't a lot of APIs that support them, but there is one: new Response(readableStream); You can create an HTTP response object where the body is a stream, and you can use these as responses from a service worker! Serving a string, slowly View demo. Note: You'll need Chrome Canary with chrome://flags/#enable-experimental-web-platform-features enabled. You'll see a page of HTML rendering (deliberately) slowly. This response is entirely generated within a service worker. Here's the code: // In the service worker: self.addEventListener('fetch', (event) => { var html = '…html to serve…'; var stream = new ReadableStream({ start(controller) { var encoder = new TextEncoder(); // Our current position in `html` var pos = 0; // How much to serve on each push var chunkSize = 1; function push() { // Are we done? if (pos >= html.length) { controller.close(); return; } // Push some of the html, // converting it into an Uint8Array of utf-8 data controller.enqueue(encoder.encode(html.slice(pos, pos + chunkSize))); // Advance the position pos += chunkSize; // push again in ~5ms setTimeout(push, 5); } // Let's go! push(); }, }); event.respondWith( new Response(stream, { headers: { 'Content-Type': 'text/html' }, }), ); }); When the browser reads a response body it expects to get chunks of Uint8Array, it fails if passed something else like a plain string. Thankfully TextEncoder can take a string and returns a Uint8Array of bytes representing that string. Like TextDecoder, TextEncoder should become a transform stream in future. Serving a transformed stream Like I said, transform streams haven't been defined yet, but you can achieve the same result by creating a readable stream that produces data sourced from another stream. "Cloud" to "butt" View demo. Note: You'll need Chrome Canary with chrome://flags/#enable-experimental-web-platform-features enabled. What you'll see is this page (taken from the cloud computing article on Wikipedia) but with every instance of "cloud" replaced with "butt". The benefit of doing this as a stream is you can get transformed content on the screen while you're still downloading the original. Here's the code, including details on some of the edge-cases. MPEG to GIF Video codecs are really efficient, but videos don't autoplay on mobile. GIFs autoplay, but they're huge. Well, here's a really stupid solution: View demo. Note: You'll need Chrome Canary with chrome://flags/#enable-experimental-web-platform-features enabled. Streaming is useful here as the first frame of the GIF can be displayed while we're still decoding MPEG frames. So there you go! A 26mb GIF delivered using only 0.9mb of MPEG! Perfect! Except it isn't real-time, and uses a lot of CPU. Browsers should really allow autoplaying of videos on mobile, especially if muted, and it's something Chrome is working towards right now. Full disclosure: I cheated somewhat in the demo. It downloads the whole MPEG before it begins. I wanted to get it streaming from the network, but I ran into an OutOfSkillError. Also, the GIF really shouldn't loop while it's downloading, that's a bug we're looking into. Creating one stream from multiple sources to supercharge page render times This is probably the most practical application of service worker + streams. The benefit is huge in terms of performance. A few months ago I built a demo of an offline-first wikipedia. I wanted to create a truly progressive web-app that worked fast, and added modern features as enhancements. In terms of performance, the numbers I'm going to talk about are based on a lossy 3g connection simulated using OSX's Network Link Conditioner. Without the service worker it displays content sent to it by the server. I put a lot of effort into performance here, and it paid off: div { position: absolute; bottom: 0; left: 0; } .timing-graph .scale > div::after { display: block; content: ''; width: 1px; height: 10px; background: #000; } .timing-graph .scale > div:last-child::after { margin-left: -1px; } .timing-graph .scale .label { position: absolute; left: 0; top: -27px; transform: translateX(-50%); } .timing-graph .result { height: 2.4rem; position: relative; display: flex; margin: 6px 0; color: #fff; opacity: 0.5; } .timing-graph .result:last-child { opacity: 1; } .timing-graph .result .title { position: absolute; top: 0; left: 0; bottom: 0; right: 0; display: flex; align-items: center; font: normal 1.2rem/1 sans-serif; margin: 0 10px; text-shadow: 0 1.3px 1.4px rgba(0,0,0,0.6); } .timing-graph .results { margin: 0; padding: 0; } .timing-graph .result .white-time, .timing-graph .result .shell-time { height: 100%; } .timing-graph .result, .results-key .content::before { background: #21AF63; } .timing-graph .result .white-time, .results-key .nothing::before { background: #DB4437; } .timing-graph .result .shell-time, .results-key .header::before { background: #F4B401; } .timing-graph .result::after { content: ''; position: absolute; left: 85%; top: 0; bottom: 0; background: linear-gradient(to right, #21AF63, #fff); } .timing-graph .non-visual { position: absolute; width: 0; height: 0; opacity: 0; overflow: hidden; } .results-key { display: flex; flex-flow: row wrap; justify-content: center; } .results-key > div { display: flex; align-items: center; margin: 0 0.6rem; } .results-key > div::before { content: ''; display: block; width: 1rem; height: 1rem; margin-right: 0.3rem; } .timing-graph .scale::after, .timing-graph .result::after { right: -20px; right: -20px; } @media (min-width: 530px) { .timing-graph .scale::after, .timing-graph .result::after { right: -32px; right: -32px; } } Nothing rendered Header & background Article content function TimingGraph(size, majorTick, minorTick) { this.container = document.createElement('div'); this.container.className = 'timing-graph'; this.container.innerHTML = ' '; this.size = size; this.results = this.container.querySelector('.results'); var scale = this.container.querySelector('.scale'); for (var i = 0; i ' + '' + ''; var titleEl = result.querySelector('.title'); var whiteTimeEl = result.querySelector('.white-time'); var shellTimeEl = result.querySelector('.shell-time'); titleEl.innerHTML = title + ': ' + (shellTime / 1000) + ' seconds until initial render, ' + (contentTime / 1000) + ' seconds until content render'; whiteTimeEl.style.width = (shellTime/this.size) * 100 + '%'; shellTimeEl.style.width = ((contentTime - shellTime)/this.size) * 100 + '%'; this.results.appendChild(result); }; (function() { var graph = new TimingGraph(4500, 1000, 500); document.querySelector('.results-server-render').appendChild(graph.container); graph.addResult('Server render', 730, 1800); }()); View demo Not bad. I added a service worker to mix in some offline-first goodness and improve performance further. And the results? (function() { var graph = new TimingGraph(4500, 1000, 500); document.querySelector('.results-client-render').appendChild(graph.container); graph.addResult('Server render', 730, 1800); graph.addResult('Service worker client render', 100, 3800); }()); View demo So um, first render is faster, but there's a massive regression when it comes to rendering content. The fastest way would be to serve the entire page from the cache, but that involves caching all of Wikipedia. Instead, I served a page that contained the CSS, JavaScript and header, getting a fast initial render, then let the page's JavaScript set about fetching the article content. And that's where I lost all the performance - client-side rendering. HTML renders as it downloads, whether it's served straight from a server or via a service worker. But I'm fetching the content from the page using JavaScript, then writing it to innerHTML, bypassing the streaming parser. Because of this, the content has to be fully downloaded before it can be displayed, and that's where the two second regression comes from. The more content you're downloading, the more the lack of streaming hurts performance, and unfortunately for me, Wikipedia articles are pretty big (the Google article is 100k). This is why you'll see me whining about JavaScript-driven web-apps and frameworks - they tend to throw away streaming as step zero, and performance suffers as a result. I tried to claw some performance back using prefetching and pseudo-streaming. The pseudo-streaming is particularly hacky. The page fetches the article content and reads it as a stream. Once it receives 9k of content, it's written to innerHTML, then it's written to innerHTML again once the rest of the content arrives. This is horrible as it creates some elements twice, but hey, it's worth it: (function() { var graph = new TimingGraph(4500, 1000, 500); document.querySelector('.results-client-render-hacks').appendChild(graph.container); graph.addResult('Server render', 730, 1800); graph.addResult('Service worker client render', 100, 3800); graph.addResult('…with hacks', 100, 2500); }()); View demo So the hacks improve things but it still lags behind server render, which isn't really acceptable. Furthermore, content that's added to the page using innerHTML doesn't quite behave the same as regular parsed content. Notably, inline <script>s aren't executed. This is where streams step in. Instead of serving an empty shell and letting JS populate it, I let the service worker construct a stream where the header comes from a cache, but the body comes from the network. It's like server-rendering, but in the service worker: (function() { var graph = new TimingGraph(4500, 1000, 500); document.querySelector('.results-stream').appendChild(graph.container); graph.addResult('Server render', 730, 1800); graph.addResult('Service worker client render', 100, 3800); graph.addResult('…with hacks', 100, 2500); graph.addResult('Service worker stream', 100, 1000); }()); View demo. Note: You'll need Chrome Canary with chrome://flags/#enable-experimental-web-platform-features enabled. Using service worker + streams means you can get an almost-instant first render, then beat a regular server render by piping a smaller amount of content from the network. Content goes through the regular HTML parser, so you get streaming, and none of the behavioural differences you get with adding content to the DOM manually. Render time comparison Crossing the streams Because piping isn't supported yet, combining the streams has to be done manually, making things a little messy: var stream = new ReadableStream({ start(controller) { // Get promises for response objects for each page part // The start and end come from a cache var startFetch = caches.match('/page-start.inc'); var endFetch = caches.match('/page-end.inc'); // The middle comes from the network, with a fallback var middleFetch = fetch('/page-middle.inc').catch(() => caches.match('/page-offline-middle.inc'), ); function pushStream(stream) { // Get a lock on the stream var reader = stream.getReader(); return reader.read().then(function process(result) { if (result.done) return; // Push the value to the combined stream controller.enqueue(result.value); // Read more & process return reader.read().then(process); }); } // Get the start response startFetch // Push its contents to the combined stream .then((response) => pushStream(response.body)) // Get the middle response .then(() => middleFetch) // Push its contents to the combined stream .then((response) => pushStream(response.body)) // Get the end response .then(() => endFetch) // Push its contents to the combined stream .then((response) => pushStream(response.body)) // Close our stream, we're done! .then(() => controller.close()); }, }); There are some templating languages such as Dust.js which stream their output, and also handle streams as values within the template, piping the content too and even HTML-escaping it on the fly. All that's missing is support for web streams. The future for streams Aside from readable streams, the streams spec is still being developed, but what you can already do is pretty incredible. If you're wanting to improve the performance of a content-heavy site and provide an offline-first experience without rearchitecting, constructing streams within a service worker will become the easiest way to do it. It's how I intend to make this blog work offline-first anyway! Having a stream primitive on the web means we'll start to get script access to all the streaming capabilities the browser already has. Things like: Gzip/deflate Audio/video codecs Image codecs The streaming HTML/XML parser It's still early days, but if you want to start preparing your own APIs for streams, there's a reference implementation that can be used as a polyfill in some cases. Streaming is one of the browser's biggest assets, and 2016 is the year it's unlocked to JavaScript. Thanks to Dominic Szablewski's JS MPEG1 decoder (check out his talk about it), and Eugene Ware's GIF encoder. And thanks to Domenic Denicola, Takeshi Yoshino, Yutaka Hirano, Lyza Danger Gardner, Nicolás Bevacqua, and Anne van Kesteren for corrections & ideas. Yes that's how many people it takes to catch all my mistakes.

The anatomy of responsive images

I just had my responsive images epiphany and I'm writing it all down before I forget everything. This is what I know… Fixed size, varying density If your image is a fixed size in pixels, but you want to cater for screens of different density, here's the solution: .img-diagram-figure { position: relative; } .img-diagram-figure svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } .img-d-1 .st0{fill:#5F6464;} .img-d-1 .st1{font-family:Inconsolata; font-weight: bold;} .img-d-1 .st2{font-size:26.9775px;} .img-d-1 .st3{fill:#C92C2C;} .img-d-1 .st4{fill:#309D47;} .img-d-1 .st5{fill:#1990B8;} .img-d-1 .st6{font-family:'Just Another Hand';} .img-d-1 .st7{font-size:42.9124px;} .img-d-1 .st8{fill:none;stroke:#ED1F24;stroke-width:3;stroke-miterlimit:10;} .img-d-1 .st9{fill:#ED1F24;} .img-d-1 .st10{fill:none;} .img-d-1 .st11{font-size:42.9091px;} .img-d-1 .st12{font-size:42.9092px;}<img alt="A cat" width="320" height="213" src="cat.jpg" srcset="cat-2x.jpg 2x, cat-3x.jpg 3x">Fixed size, here or in CSS This is used as the 1x src & by browsers that don’t support srcset Image urlPixel density of screen This works in all modern browsers, and falls back to src in older browsers. A few more rules, not covered in the image above: Each item within srcset is <url> <density>x, eg cat-2x.jpg 2x The order of items within the srcset doesn't matter If you don't specify width/height, the browser will display the image at its native width/height divided by the density. Eg if the 2x resource is picked, it'll be rendered at 50% of the resources width/height This is only a hint, even on a 3x device the browser may use the 1x image, perhaps due to poor connectivity Live example Varying size and density Images of varying width are commonly used as part of the content on responsive sites. On this blog, content imagery takes up 100% of the article width, but the article isn't always 100% of the window. In order for the browser to pick the right image, it needs to know: URLs for the image at various sizes The decoded width of each of those image resources The width of the <img> That last one is particularly tricky, as images start downloading before CSS is ready, so the width of the <img> cannot be detected from the page layout. The key to understanding this syntax is knowing which of these values refer to the window width, the decoded image width, and the <img> width. .img-d-2 .st0{fill:#5F6464;} .img-d-2 .st1{font-family:Inconsolata; font-weight: bold;} .img-d-2 .st2{font-size:25.7772px;} .img-d-2 .st3{fill:#C92C2C;} .img-d-2 .st4{fill:#309D47;} .img-d-2 .st5{fill:#1990B8; white-space: pre;} .img-d-2 .st6{fill:#309D48;} .img-d-2 .st7{fill:none;} .img-d-2 .st8{font-family:'Just Another Hand'} .img-d-2 .st9{font-size:41px;} .img-d-2 .st10{fill:none;stroke:#ED1F24;stroke-width:3;stroke-miterlimit:10;} .img-d-2 .st11{fill:#ED1F24;}<img alt="A red panda eating leaves" src="panda-689.jpg" srcset="panda-689.jpg 689w, panda-1378.jpg 1378w, panda-500.jpg 500w, panda-1000.jpg 1000w" sizes="(min-width: 1066px) 689px, (min-width: 800px) calc(75vw - 137px), (min-width: 530px) calc(100vw - 96px), 100vw"> Only for browsers that don’t support srcset Image urlWidth of the image data Width of the windowWidth of the img elementwhen the condition matches Fallback width, when no media conditions match Via srcset, the browser knows the resources available and their widths. Via sizes it knows the width of the <img> for a given window width. It can now pick the best resource to load. You don't need to specify density, the browser figure that out itself. If the window is 1066px wide or greater, we've signalled that the <img> will be 689px wide. On a 1x device the browser may download panda-689.jpg, but on a 2x device the browser may download panda-1378.jpg. A few more rules, not covered in the image above: Each item within srcset is <url> <width-descriptor>w, eg panda-689.jpg 689w The order of items within the srcset doesn't matter If srcset contains a width descriptor, the src is ignored by browsers that support srcset Each item within sizes is <media-condition> <image-element-width>, except for the last entry which is just <image-element-width> Both of the widths in sizes are in CSS pixels The browser uses the first media condition match in sizes, so the order matters As before, the browser may download a lower resolution image due to other factors such as poor connectivity Picking which sizes to list is pretty straight forward. Start with your window at its narrowest, and as you increase its size, create a new rule whenever the <img> size vs window size changes formula. When this window is at its narrowest, the <img> is full width, or 100vw. When the window goes beyond 530px the content area on this page gets 32px padding on the left and 64px on the right, so the <img> is now calc(100vw - 96px). The browser won't call the police if it finds out you lied about the <img> width. I've been accurate with my sizes, but a rough answer can be good enough, eg sizes="(min-width: 1066px) 689px, (min-width: 800px) 75vw, 100vw". Picking which resources to create and include in srcset is much harder, and I don't think I've mastered it. In the above example I include the maximum size the <img> can be (689px) and double that for 2x devices (1378px). The other two are rough in-between values. I didn't include smaller widths such as 320px, under the assumption that screens of that size will be 2x density or greater. srcset + sizes works in Chrome, Firefox, and Opera. For other browsers, it'll safely fall back to src. You don't have to wait long for better support here, it's in WebKit nightly & will appear in the next stable version of Edge. Live example Varying width, density, and art direction Similar to the previous example, but the framing changes at different widths. This allows you to focus in on the subject at smaller widths. .img-d-3 .st0{fill:#5F6464; white-space: pre;} .img-d-3 .st1{font-family:Inconsolata; font-weight: bold;} .img-d-3 .st2{font-size:24.2164px;} .img-d-3 .st3{fill:#C92C2C;} .img-d-3 .st4{fill:#309D47;} .img-d-3 .st5{fill:#1990B8; white-space: pre;} .img-d-3 .st6{fill:none;} .img-d-3 .st7{font-family:'Just Another Hand'} .img-d-3 .st8{font-size:41px;} .img-d-3 .st9{fill:none;stroke:#ED1F24;stroke-width:3;stroke-miterlimit:10;} .img-d-3 .st10{fill:#ED1F24;}<picture> <source media="(max-width: 800px)" srcset="f1-focused-800.jpg 800w, f1-focused-1406.jpg 1406w" sizes="(min-width: 530px) calc(100vw - 96px), 100vw"> <img alt="F1 car in the gravel" src="f1-689.jpg" srcset="f1-689.jpg 689w, f1-1378.jpg 1378w, f1-500.jpg 500w, f1-1000.jpg 1000w" sizes="(min-width: 1066px) 689px, calc(75vw - 137px)"></picture> If this query matches the window, use these to select the src …else use these You can have as many <source>s as you want You must include an <img> The media query on <source> will always be obeyed, it's not just a hint The media query is based on the window's width, not the <img> The first matching <source> will be used, so the order matters If no matching <source> is found, the <img> is used The <img> must appear after all <source>s <source> doesn't support src, but srcset="whatever.jpg" works just as well Once the <source> or <img> is selected, the srcset and sizes attributes work as in previous examples, so you can mix and match techniques. The <picture> element works in Chrome, Firefox, and Opera, and falls back to the <img> in other browsers. I'm told it might make it into the next release of Edge, which is nice. Live example Varying on type This method allows you to serve better-optimised formats to browsers that support them. .img-d-4 .st0{fill:#5F6464; white-space: pre;} .img-d-4 .st1{font-family:Inconsolata; font-weight: bold;} .img-d-4 .st2{font-size:25.7851px;} .img-d-4 .st3{fill:#C92C2C;} .img-d-4 .st4{fill:#309D47;} .img-d-4 .st5{fill:#1990B8;} .img-d-4 .st6{fill:none;} .img-d-4 .st7{font-family:'Just Another Hand';} .img-d-4 .st8{font-size:41px;} .img-d-4 .st9{fill:none;stroke:#ED1F24;stroke-width:3;stroke-miterlimit:10;} .img-d-4 .st10{fill:#ED1F24;}<picture> <source type="image/webp" srcset="snow.webp"> <img alt="Hut in the snow" src="snow.jpg"></picture> If this type is supported, use this …else this type is a mime type You can have multiple sources and mix type with media, srcset, and even sizes to create something truly monstrous/awesome This works in Chrome, Firefox, and Opera, and falls back to the <img> in other browsers. Live example Further reading Hopefully the above helps as a kind of quick reference to the various use-cases, but if not, dig into these: A 10-part novella on responsive images - by Jason Grigsby Responsive Images: Use Cases and Code Snippets - similar to this article, but covers more combinations of use-cases Client hints - a server-side alternative to responsive images Thanks to Mike Hall, Jason Grigsby, Simon Peters, and Yoav Weiss for proofreading and point-sharpening.

Tasks, microtasks, queues and schedules

When I told my colleague Matt Gaunt I was thinking of writing a piece on microtask queueing and execution within the browser's event loop, he said "I'll be honest with you Jake, I'm not going to read that". Well, I've written it anyway, so we're all going to sit here and enjoy it, ok? Actually, if video's more your thing, Philip Roberts gave a great talk at JSConf on the event loop - microtasks aren't covered, but it's a great introduction to the rest. Anyway, on with the show… Take this little bit of JavaScript: console.log('script start'); setTimeout(function () { console.log('setTimeout'); }, 0); Promise.resolve() .then(function () { console.log('promise1'); }) .then(function () { console.log('promise2'); }); console.log('script end'); In what order should the logs appear? Try it Clear log Run test The correct answer: script start, script end, promise1, promise2, setTimeout, but it's pretty wild out there in terms of browser support. Microsoft Edge, Firefox 40, iOS Safari and desktop Safari 8.0.8 log setTimeout before promise1 and promise2 - although it appears to be a race condition. This is really weird, as Firefox 39 and Safari 8.0.7 get it consistently right. Why this happens To understand this you need to know how the event loop handles tasks and microtasks. This can be a lot to get your head around the first time you encounter it. Deep breath… Each 'thread' gets its own event loop, so each web worker gets its own, so it can execute independently, whereas all windows on the same origin share an event loop as they can synchronously communicate. The event loop runs continually, executing any tasks queued. An event loop has multiple task sources which guarantees execution order within that source (specs such as IndexedDB define their own), but the browser gets to pick which source to take a task from on each turn of the loop. This allows the browser to give preference to performance sensitive tasks such as user-input. Ok ok, stay with me… Tasks are scheduled so the browser can get from its internals into JavaScript/DOM land and ensures these actions happen sequentially. Between tasks, the browser may render updates. Getting from a mouse click to an event callback requires scheduling a task, as does parsing HTML, and in the above example, setTimeout. setTimeout waits for a given delay then schedules a new task for its callback. This is why setTimeout is logged after script end, as logging script end is part of the first task, and setTimeout is logged in a separate task. Right, we're almost through this, but I need you to stay strong for this next bit… Microtasks are usually scheduled for things that should happen straight after the currently executing script, such as reacting to a batch of actions, or to make something async without taking the penalty of a whole new task. The microtask queue is processed after callbacks as long as no other JavaScript is mid-execution, and at the end of each task. Any additional microtasks queued during microtasks are added to the end of the queue and also processed. Microtasks include mutation observer callbacks, and as in the above example, promise callbacks. Once a promise settles, or if it has already settled, it queues a microtask for its reactionary callbacks. This ensures promise callbacks are async even if the promise has already settled. So calling .then(yey, nay) against a settled promise immediately queues a microtask. This is why promise1 and promise2 are logged after script end, as the currently running script must finish before microtasks are handled. promise1 and promise2 are logged before setTimeout, as microtasks always happen before the next task. So, step by step: console.log('script start'); setTimeout(function () { console.log('setTimeout'); }, 0); Promise.resolve() .then(function () { console.log('promise1'); }) .then(function () { console.log('promise2'); }); console.log('script end'); Tasks Run script setTimeout callback Microtasks Promise then Promise then JS stack Log script start script end promise1 promise2 setTimeout Yes that's right, I created an animated step-by-step diagram. How did you spend your Saturday? Went out in the sun with your friends? Well I didn't. Um, in case it isn't clear from my amazing UI design, click the arrows above to advance. What are some browsers doing differently? Some browsers log script start, script end, setTimeout, promise1, promise2. They're running promise callbacks after setTimeout. It's likely that they're calling promise callbacks as part of a new task rather than as a microtask. This is sort-of excusable, as promises come from ECMAScript rather than HTML. ECMAScript has the concept of "jobs" which are similar to microtasks, but the relationship isn't explicit aside from vague mailing list discussions. However, the general consensus is that promises should be part of the microtask queue, and for good reason. Treating promises as tasks leads to performance problems, as callbacks may be unnecessarily delayed by task-related things such as rendering. It also causes non-determinism due to interaction with other task sources, and can break interactions with other APIs, but more on that later. Here's an Edge ticket for making promises use microtasks. WebKit nightly is doing the right thing, so I assume Safari will pick up the fix eventually, and it appears to be fixed in Firefox 43. Really interesting that both Safari and Firefox suffered a regression here that's since been fixed. I wonder if it's just a coincidence. How to tell if something uses tasks or microtasks Testing is one way. See when logs appear relative to promises & setTimeout, although you're relying on the implementation to be correct. The certain way, is to look up the spec. For instance, step 14 of setTimeout queues a task, whereas step 5 of queuing a mutation record queues a microtask. As mentioned, in ECMAScript land, they call microtasks "jobs". In step 8.a of PerformPromiseThen, EnqueueJob is called to queue a microtask. Now, let's look at a more complicated example. Cut to a concerned apprentice "No, they're not ready!". Ignore him, you're ready. Let's do this… Level 1 bossfight Before writing this post I'd have gotten this wrong. Here's a bit of html: <div class="outer"> <div class="inner"></div> </div> Given the following JS, what will be logged if I click div.inner? // Let's get hold of those elements var outer = document.querySelector('.outer'); var inner = document.querySelector('.inner'); // Let's listen for attribute changes on the // outer element new MutationObserver(function () { console.log('mutate'); }).observe(outer, { attributes: true, }); // Here's a click listener… function onClick() { console.log('click'); setTimeout(function () { console.log('timeout'); }, 0); Promise.resolve().then(function () { console.log('promise'); }); outer.setAttribute('data-random', Math.random()); } // …which we'll attach to both elements inner.addEventListener('click', onClick); outer.addEventListener('click', onClick); Go on, give it a go before peeking at the answer. Clue: Logs can happen more than once. Test it Click the inner square to trigger a click event: Clear log Was your guess different? If so, you may still be right. Unfortunately the browsers don't really agree here: li { margin-left: 1px; background: #eee; padding: 0.5rem; padding-bottom: 0.4rem; line-height: 1.5; } @media (min-width: 400px) { .browser-results > li { margin-left: 1rem; } } .browser-results > li:first-child { margin-left: 0; } .browser-results ul { display: block; margin: 0; padding: 0; } .browser-results img { width: 100%; max-width: 100px; } click promise mutate click promise mutate timeout timeout click mutate click mutate timeout promise promise timeout click mutate click mutate promise promise timeout timeout click click mutate timeout promise timeout promise Who's right? Dispatching the 'click' event is a task. Mutation observer and promise callbacks are queued as microtasks. The setTimeout callback is queued as a task. So here's how it goes: // Let's get hold of those elements var outer = document.querySelector('.outer'); var inner = document.querySelector('.inner'); // Let's listen for attribute changes on the // outer element new MutationObserver(function () { console.log('mutate'); }).observe(outer, { attributes: true, }); // Here's a click listener… function onClick() { console.log('click'); setTimeout(function () { console.log('timeout'); }, 0); Promise.resolve().then(function () { console.log('promise'); }); outer.setAttribute('data-random', Math.random()); } // …which we'll attach to both elements inner.addEventListener('click', onClick); outer.addEventListener('click', onClick); Tasks Dispatch click setTimeout callback setTimeout callback Microtasks Promise then Mutation observers Promise then Mutation observers JS stack Log click promise mutate click promise mutate timeout timeout So it's Chrome that gets it right. The bit that was 'news to me' is that microtasks are processed after callbacks (as long as no other JavaScript is mid-execution), I thought it was limited to end-of-task. This rule comes from the HTML spec for calling a callback: If the stack of script settings objects is now empty, perform a microtask checkpoint — HTML: Cleaning up after a callback step 3 …and a microtask checkpoint involves going through the microtask queue, unless we're already processing the microtask queue. Similarly, ECMAScript says this of jobs: Execution of a Job can be initiated only when there is no running execution context and the execution context stack is empty… — ECMAScript: Jobs and Job Queues …although the "can be" becomes "must be" when in an HTML context. What did browsers get wrong? Firefox and Safari are correctly exhausting the microtask queue between click listeners, as shown by the mutation callbacks, but promises appear to be queued differently. This is sort-of excusable given that the link between jobs & microtasks is vague, but I'd still expect them to execute between listener callbacks. Firefox ticket. Safari ticket. With Edge we've already seen it queue promises incorrectly, but it also fails to exhaust the microtask queue between click listeners, instead it does so after calling all listeners, which accounts for the single mutate log after both click logs. Bug ticket. Level 1 boss's angry older brother Ohh boy. Using the same example from above, what happens if we execute: inner.click(); This will start the event dispatching as before, but using script rather than a real interaction. Try it Clear log Run test And here's what the browsers say: click click promise mutate promise timeout timeout click click mutate timeout promise promise timeout click click mutate promise promise timeout timeout click click mutate timeout promise timeout promise And I swear I keep getting different results from Chrome, I've updated this chart a ton of times thinking I was testing Canary by mistake. If you get different results in Chrome, tell me which version in the comments. Why is it different? Here's how it should happen: // Let's get hold of those elements var outer = document.querySelector('.outer'); var inner = document.querySelector('.inner'); // Let's listen for attribute changes on the // outer element new MutationObserver(function () { console.log('mutate'); }).observe(outer, { attributes: true, }); // Here's a click listener… function onClick() { console.log('click'); setTimeout(function () { console.log('timeout'); }, 0); Promise.resolve().then(function () { console.log('promise'); }); outer.setAttribute('data-random', Math.random()); } // …which we'll attach to both elements inner.addEventListener('click', onClick); outer.addEventListener('click', onClick); inner.click(); Tasks Run script setTimeout callback setTimeout callback Microtasks Promise then Mutation observers Promise then JS stack Log click click promise mutate promise timeout timeout So the correct order is: click, click, promise, mutate, promise, timeout, timeout, which Chrome seems to get right. After each listener callback is called… If the stack of script settings objects is now empty, perform a microtask checkpoint — HTML: Cleaning up after a callback step 3 Previously, this meant that microtasks ran between listener callbacks, but .click() causes the event to dispatch synchronously, so the script that calls .click() is still in the stack between callbacks. The above rule ensures microtasks don't interrupt JavaScript that's mid-execution. This means we don't process the microtask queue between listener callbacks, they're processed after both listeners. Does any of this matter? Yeah, it'll bite you in obscure places (ouch). I encountered this while trying to create a simple wrapper library for IndexedDB that uses promises rather than weird IDBRequest objects. It almost makes IDB fun to use. When IDB fires a success event, the related transaction object becomes inactive after dispatching (step 4). If I create a promise that resolves when this event fires, the callbacks should run before step 4 while the transaction is still active, but that doesn't happen in browsers other than Chrome, rendering the library kinda useless. You can actually work around this problem in Firefox, because promise polyfills such as es6-promise use mutation observers for callbacks, which correctly use microtasks. Safari seems to suffer from race conditions with that fix, but that could just be their broken implementation of IDB. Unfortunately, things consistently fail in IE/Edge, as mutation events aren't handled after callbacks. Hopefully we'll start to see some interoperability here soon. You made it! In summary: Tasks execute in order, and the browser may render between them Microtasks execute in order, and are executed: after every callback, as long as no other JavaScript is mid-execution at the end of each task Hopefully you now know your way around the event loop, or at least have an excuse to go and have a lie down. Actually, is anyone still reading? Hello? Hello? Thanks to Anne van Kesteren, Domenic Denicola, Brian Kardell, and Matt Gaunt for proofreading & corrections. Yeah, Matt actually read it in the end, I didn't even need to go full "Clockwork Orange" on him.

If we stand still, we go backwards

Recently, ppk claimed the web is going too fast in the wrong direction, and asked for a year's moratorium on web features. I was so angry I ran straight to a dictionary to find out what "moratorium" meant. Turns out it means "suspension". I got a bit snarky about it on Twitter, which isn't really fair, so here's a more considered response: "The web is getting more complicated" Is it? Let's look at the modern web's "hello world": <!DOCTYPE html> <title>Hello</title> World Let's compare that to the pre-HTML5 "hello world": <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Hello</title> </head> <body> <div>World</div> </body> </html> "Hello world" is simpler now than it has ever been. Ok ok, as many have pointed out, "Hello world" is not a fair judge of complexity, but take the world's first web site - it still works in modern browsers. For any given page, the complexity of creating it today is the same, or easier. So it's not that achieving the same has gotten more complicated, it's that… User expectations are higher We shouldn't be surprised by this. Expectations change. Back in 1995, one of my favourite comedians said (something like) this: I'm not interested in Star Trek or child pornography so the internet has nothing for me — Stewart Lee The web has come a long way in twenty years. Transferring data across the globe is no longer a big deal. Making an online purchase is not something you'd wow a dinner party with. Given the web's progress, last decade's "pretty good" is today's "a bit shit". User experience has become a competitive element on the web. As a result, us developers and designers have had to up our game, but we're getting a better web as a result. Is the web platform too big? To enable us to build better experiences, the web platform gained new features. Is this a problem? When I left university, I felt I knew all of the web platform. Partially because the web was simpler, but also I was an arrogant little shit who didn't know how much I didn't know. As new features appeared and I learned more about the existing platform, I realised I wasn't capable of knowing it all. At first this felt like a personal failing, but I've since made peace with it. Is the web platform too big? For one person, yes. Is it a problem? No. No one can be an expert in the whole web. Surgeons aren't experts in all types of surgery, scientists aren't experts in all of science, web developers aren't experts in all of web development. A couple of years ago, I ran a little quiz on browser requests. The top scorer was usually below the 50% mark, not because the audience were idiots, far from it, they were competent web developers, some of them experts. It just wasn't stuff everyone needed to instantly recall, it was stuff you'd be able to work out as and when needed, using developer tools (just as I did when making the quiz). A few years ago, the sum-total of my "offline web" knowledge was "there is a thing called appcache that does it", and that was enough. I didn't have to learn any more about it until a project came along that would benefit from it. Of course I now know that every project benefits from offline, right? Right? Just because it's there, doesn't mean you must learn it and use it Here's a picture I took of some planes doing a thing: It could be better, so let's get Photoshop on the case. When faced with Photoshop's expansive set of tools, it's a common beginner mistake to throw everything at it: This may be Michael Bay's wet dream, but he's the exception. However, I could use a subset of features more subtly: Same goes for the web. You don't need to achieve 100% browser code coverage in a single page. Use what's right for the project. The best user experience is the simplest thing that achieves the user's goals. Are we adding the right features to the web? ppk argues that we're blindly copying native, I don't think we are, but that doesn't mean we should ignore native altogether. We should add features using evidence, and native is a great source of evidence. Through native, we've seen users benefit from push messaging, offline data access, gps, speedy payments. Through native we've also seen store management harm openness, packaging hurt linking, and up-front permissioning harm security and privacy. Native's successes and failures are great data points. And let's be clear here, when we say "native" we mean the class of devices that arrived after the web, mobile & tablet. The web was ready for mobile as soon as we put a browser on it, but it wasn't optimised for it. Media queries, gps, push messaging, and soon, pointer events, these are features that adapt to the new device inputs, new screen dimensions, new usage patterns. Of course, native isn't the only source of UX evidence. Going back a few years, we were looking at what Flash did best, and took from there. When I read "we shouldn't do [feature] because that's what native does", I wonder if that same person thinks we shouldn't have <video> because that's what Flash did. Libraries and tools are other sources of evidence, they show what developers are fighting. jQuery showed developer's frustration with the stagnation of IE, and which DOM features were lacking. Coffeescript showed frustrations with JavaScript language features. Sass showed frustrations with CSS. These things, and their popularity, were a clear signal that developers found parts of the web platform lacking, and were unhappy with the pace of development. Other features, such as HTTP/2, were created by looking at the weak points of the web. Sure, HTTP/2 is more complex than HTTP/1.1, but it also reduces the need for a lot of hacks like concatenation, inlining, spriting, which complicate today's web. We haven't always gotten it right, see appcache. In that case we didn't have enough evidence, and created a high-level solution that couldn't be used the way developers wanted. Following that, we adopted a different approach, the Extensible Web. Now we try to introduce lower-level features first. Those are more flexible, but require more JavaScript to build into a full experience. Developers' use of these low-level features becomes a new source of evidence for future higher-level features. Halting is not a successful strategy Back in 2007, Apple brought out the iPhone. Compared to its competitors, it was a step forward in terms of its excellent touch screen and interaction design. In other areas, it was lacking. No 3g connectivity, no GPS, no front camera. Did it stick to what it was good at and impose a moratorium on the rest? No. But one company did. They were called "Research In Motion", and produced a phone called a "BlackBerry". They decided to concentrate on their current set of users, who they decided didn't want change. But that set of users decreased as expectations changed. BlackBerry had a head start in the mobile race, but they stopped. By the time they realised they were behind, their attempts to catch up came too little, too late. Meanwhile, the iPhone gradually assimilated the advantages of its competitors. Not all of them, just the features that mattered, ones that had a proven track record. I love the web, therefore I react in shock to any suggestion that we should adopt the BlackBerry strategy. I was a developer during the last moratorium of the web, IE6-8. IE6 came out in 2001 and it was great, but then Microsoft stopped. IE7 came out in 2006, and IE8 in 2009, but they were mostly bug-fix releases, they had little in the way of new features. We're still recovering from that, but it feels like there's light at the end of the tunnel. I don't want to go back there! Improve the web forward The great thing about the web is it doesn't hit 1.0 and stop, it's continual. Table layouts were observed, and CSS was delivered to offer express layout without all the markup cruft. DOM selector libraries were observed, and we got querySelectorAll. Viewport diversity was observed, and we got media queries. Mobile usage has sky-rocketed, and it's an exciting new thing to observe. We shouldn't get snobby about features that appeared on other platforms first. Native apps are looking to gain the web's advantages too, they're figuring out linking, and at some point they're going to figure out progressive loading and get rid of the install process. If we stop, we lose. The web is feature-rich, and it's great. Media queries, touch events, flexbox, service worker, push messaging, web audio, canvas… these are all features you may use to improve user experience. I'm not sure which of these fall into ppk's "this far, no further" basket, but you don't have to use any of them. You are welcome to draw a line in the sand and refuse to learn new web features from this day forth. But if a competitor does use them, and uses them to create a better user experience than the one you offer, you lose. But users win. Update: Opera's Bruce Lawson responded to ppk at almost the same time I did. I think I published first by like 5 minutes, so I win. In your face Bruce! Thanks to Anne van Kesteren, Alex Russell, Peter Beverloo, and Paul Lewis for proofreading and general point-sharpening. For reading this far you win a free TIE-kitten dogfight desktop wallpaper.

That's so fetch!

There's been some confusion around the new fetch API recently. Let's clear things up. The first thing you'll notice about fetch is it's a massive improvement on XMLHttpRequest in terms of API design. Here's how to get some JSON using XHR: var xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.responseType = 'json'; xhr.onload = function () { console.log(xhr.response); }; xhr.onerror = function () { console.log('Booo'); }; xhr.send(); Now mop up that vomit and take a look at how fetch does the same thing: fetch(url) .then(function (response) { return response.json(); }) .then(function (data) { console.log(data); }) .catch(function () { console.log('Booo'); }); Mix in some ES6 arrow functions and it gets even more compact: fetch(url) .then((r) => r.json()) .then((data) => console.log(data)) .catch((e) => console.log('Booo')); And with ES7 async functions it can be structured like sync code, whilst still being async: (async () => { try { var response = await fetch(url); var data = await response.json(); console.log(data); } catch (e) { console.log('Booo'); } })(); Unfortunately, not everyone was throwing "SHUT UP AND TAKE MY MONEY" memes at it. One particular high-profile JavaScript community member was unconvinced… Dramatic reconstruction (function() { var figure = document.querySelector('.fetch-vid-figure'); var vid = document.querySelector('.fetch-vid'); var playBtn = document.querySelector('.fetch-vid-play'); figure.style.display = ''; if (navigator.userAgent.indexOf('iPhone') !== -1) { // Apple hates the web var obj = document.createElement('object'); obj.type = 'image/svg+xml'; obj.data = '/c/fetch-fallback-7488ad38.svg'; figure.removeChild(vid); figure.removeChild(playBtn); figure.insertBefore(obj, figure.firstChild); return; } vid.addEventListener('play', function() { requestAnimationFrame(function() { playBtn.style.display = 'none'; }) }); playBtn.addEventListener('click', function() { vid.play(); playBtn.style.display = 'none'; }); document.addEventListener('touchend', function startPlay() { vid.play(); document.removeEventListener('touchend', startPlay); }); }()); They thought we shouldn't be adding high level features to the platform, especially as we're in dire need of lower level primitives when it comes to requests and responses. To that I say, well, actually, that's soooo fetch, and we are going to make fetch happen. Let's clear up the misconceptions… Don't be fooled by the nice API A nice, compact API can be a sign of "high level", and therefore restrictive. But that isn't the case here. XHR is so bad, a lower-level and more featureful API can be also simpler and easier to use. XHR is now defined in terms of fetch (see the calls to fetch in XHR's .send()), meaning fetch is lower level than XHR. Fetch isn't done yet What exists in Chrome today doesn't cover the full spec, and the spec doesn't cover all of the planned features. In some cases this is because they haven't been designed yet, in others it's because they're dependent on other in-progress specs. Basically, we're following this pattern: Bit of a rant: it bothers me that as developers, we preach iterative development and release, but when we're the customers of that approach the reaction is all too often "HOW DARE YOU PRESENT ME WITH SUCH INCOMPLETE IMPERFECTION". The alternative would have been to sit on the feature for months (or years?) instead of getting large parts of it into developers' hands today. Iterative release also means we've been able to get feedback from real-world usage, and that steers future iterations in terms of design and priority. Anyway, let's take a look at what fetch can do that XHR cannot: Request/Response primitives XHR kinda smushes the request and response together, meaning they can't be used separately. Fetch is different thanks to the Request and Response constructors. This is particularly useful within a ServiceWorker: self.addEventListener('fetch', function (event) { if (event.request.url === new URL('/', location).href) { event.respondWith( new Response('<h1>Hello!</h1>', { headers: { 'Content-Type': 'text/html' }, }), ); } }); In the above, event.request is a Request. There's no response yet, and instead of letting the browser go to the network I respond with my own Response. Alternatively, I could get the response from the network using fetch(event.request), or even get a response from the cache. The Cache API is a store of Responses keyed against Requests, having separates allows you to add your own pairings. This is in Chrome stable today from within a ServiceWorker. The fetch API is also available from pages in Chrome Beta. Soon, request.context will be able to tell you the source of that request, so you can tell apart requests triggered by hyperlinks vs <img> etc. 'no-cors' and opaque responses If I request //google.com from this site using XHR or plain fetch it will fail. This is because it's a CORS request and the response doesn't have CORS headers. However, with fetch, you can make a no-cors request: fetch('//google.com', { mode: 'no-cors', }).then(function (response) { console.log(response.type); // "opaque" }); This is similar to the request an <img> makes. Of course, you can't read the content of the response as it could contain private information, but it can be consumed by other APIs: self.addEventListener('fetch', function (event) { event.respondWith( fetch('//www.google.co.uk/images/srpr/logo11w.png', { mode: 'no-cors', }), ); }); The above is fine within a ServiceWorker, as long as the receiver is happy with a no-cors response. <img> is, <img crossorigin> isn't. You can also store these responses in the Cache API for use later, which is great for CDN content such as scripts, CSS, and imagery, which often lack CORS headers. For more on the origin of CORS, see Anne VK's article on same-origin policy. This all works in Chrome stable today. In Chrome Canary, although you can use fetch() from a page, no-cors isn't enabled there yet (ticket). Streams XHR lacks streaming. You can get access to .responseText while the request is in progress, but the whole response is still going to buffer into memory. With fetch, you get access to the low-level body stream. Say I wanted to load a massive CSV and find the value in the cell after the one containing "Jake": fetch('/big-data.csv') .then(function (response) { var reader = response.body.getReader(); var partialCell = ''; var returnNextCell = false; var returnCellAfter = 'Jake'; var decoder = new TextDecoder(); function search() { return reader.read().then(function (result) { partialCell += decoder.decode(result.value || new Uint8Array(), { stream: !result.done, }); // Split what we have into CSV 'cells' var cellBoundry = /(?:,|\r\n)/; var completeCells = partialCell.split(cellBoundry); if (!result.done) { // Last cell is likely incomplete // Keep hold of it for next time partialCell = completeCells[completeCells.length - 1]; // Remove it from our complete cells completeCells = completeCells.slice(0, -1); } for (var cell of completeCells) { cell = cell.trim(); if (returnNextCell) { reader.cancel('No more reading needed.'); return cell; } if (cell === returnCellAfter) { returnNextCell = true; } } if (result.done) { throw Error('Could not find value after ' + returnCellAfter); } return search(); }); } return search(); }) .then(function (result) { console.log("Got the result! It's '" + result + "'"); }) .catch(function (err) { console.log(err.message); }); Here I'm reading through the CSV (yes, I know my regex is naive), but with only a chunk of content in memory at a given time. Once I find the value I'm looking for, I cancel the stream, closing the connection. response.body is a ReadableStream as defined by the streams spec. Streaming was planned from the outset, but it's one of the bits we launched without as the spec was still in progress. TextDecoder is part of the encoding spec. If the chunk it receives via .decode(input, {stream: true}) ends with a partial multi-byte character, it will return and flush everything but that partial. The next call to decode appends onto the partial, hopefully forming a whole character. This stuff is starting to land in Canary, here's a demo of the above, and here's a demo with a larger dataset (warning: running the demo may download many megabytes). Streams are one of the things I'm really looking forward to having on the platform. I want to be able to stream-parse some JSON, generate some HTML as a result, and stream that to the browser's parser. JS-driven apps lack an easy way to get progressive-rendering from a single data source, streams can solve that. Transform streams are coming soon, which would make the code above simpler. Ideally TextDecoder would be a transform stream, and another transform stream could chunk it into CSV rows. Something like: fetch('/big-data.csv').then(function (response) { var csvStream = response.body .pipeThrough(new TextDecoder()) .pipeThrough(new CSVDecoder()); csvStream.read().then(function (result) { // array of cell values for the first row console.log(result.value); }); }); Transform streams also become really exciting within a ServiceWorker: self.addEventListener('fetch', function (event) { event.respondWith( fetch('video.unknowncodec').then(function (response) { var h264Stream = response.body .pipeThrough(codecDecoder) .pipeThrough(h264Encoder); return new Response(h264Stream, { headers: { 'Content-type': 'video/h264' }, }); }), ); }); In the above, I'm using transform streams to take a video the browser doesn't understand, decode it with JS, and encode it in a format the browser does understand. It'd be amazing to see if the browser could do this in real time. Stream readers & cloning As I mentioned before, we initially shipped fetch without streams support so developers could get the other benefits sooner. To make up for a lack of streams & to subsequently offer a simple way to get common data types, we added some readers: fetch(url).then(function (response) { return response.json(); }); That, as you might expect, reads the whole stream as JSON. Here's the full list of readers: .arrayBuffer() .blob() .formData() .json() .text() They exist on Request objects as well as responses, so you can use them to read (for example) POST data within a ServiceWorker. These are true stream readers, meaning they drain the stream: fetch(url).then(function (response) { return response.json().catch(function () { // This does not work: return response.text(); }); }); The call to .text() fails as the stream has already been read. You can work around this using .clone(): fetch(url).then(function (response) { return response .clone() .json() .catch(function () { return response.text(); }); }); .clone() opts you into buffering. The clone gets read as JSON, but the original is still there and can be read in another format. Of course, this means the raw response data needs to be kept around in memory until all copies are read or garbage collected. Alternatively, you could look at the headers of the response: fetch(url).then(function (response) { if (response.headers.get('Content-Type') === 'application/json') { return response.json(); } return response.text(); }); This is another feature fetch has over XHR, you can decide which format to read the body as after you've inspected the headers. Other bits There are more features that fetch has over XHR that I'm not going to cover in too much detail, they include: Headers class Fetch has a headers class which can be used to read/write headers, and has an ES6 iterator. Cache control The cache mode lets you specify the interaction with the cache. As in, should the cache be consulted? Should the response go into the cache if it's valid? Should the response only come from the cache? The latter is a bit contentious as it can expose user history, so it may come with a CORS restriction before it lands in Chrome. No-credential same-origin requests XHR forces you to serve credentials with requests to the same origin, fetch doesn't. In fact, no-credentials is the default for all requests made by fetch, making it less magic than XHR. What's missing? Of course, there are some features XHR has that fetch doesn't have. Request aborting This is the big one. In Canary you can cancel the stream, but there's no way to abort a request before headers have arrived. We're going to fix this using cancellable promises, which other specs will benefit from. Track the discussion of this over at GitHub. Progress events Progress events are a high level feature that won't arrive in fetch for now. You can create your own by looking at the Content-Length header and using a pass-through stream to monitor the bytes received. This means you can explicitly handle responses without a Content-Length differently. And of course, even if Content-Length is there it can be a lie. With streams you can handle these lies however you want. Synchronous requests Noooope. The fetch spec documents them, but they won't be part of the API. Sync requests are awful. Couldn't this have been built on top of XHR? XHR was an ugly baby and time has not been kind to it. It's 16 now. In a few years it'll be old enough to drink, and it's enough of a pain in the arse when it's sober. Sure, a lot of this stuff could have been hacked on top of XHR, but it would have been a hack. Not only did fetch give us an opportunity to add lower-level features without the cruft of a badly designed API, it allowed us to create a better API for the simple things, using modern JavaScript features like promises and iterators. If you want to stop using XHR today, there's a fetch polyfill. This is built on top of XHR, so it can't do the stuff that XHR can't, but it does give you the benefits of a nicer API. Let's make fetch happen! Further reading Intro to ServiceWorkers ES6 iterators ES7 async functions Partial fetch polyfill - built on top of XHR Thanks to Matt Gaunt, Mat Scales, Anne van Kesteren, Domenic Denicola, Mikeal Rogers, Ben Kelly, and Joshua Bell for disguising the fact I can't really spell, grammar, or indeed code.

The offline cookbook

Update: Together with Udacity I created a free offline-first interactive course. It involves taking an online-only site to full offline-first glory. Many of the patterns in this article are used. When AppCache arrived on the scene it gave us a couple of patterns to make content work offline. If those were the patterns you needed, congratulations, you won the AppCache lottery (the jackpot remains unclaimed), but the rest of us were left huddled in a corner rocking back & forth. With ServiceWorker (intro) we gave up trying to solve offline, and gave developers the moving parts to go solve it themselves. It gives you control over caching and how requests are handled. That means you get to create your own patterns. Let's take a look at a few possible patterns in isolation, but in practice you'll likely use many of them in tandem depending on URL & context. All code examples work today in Chrome & Firefox, unless otherwise noted. For full details on service worker support, see "Is Service Worker Ready?". For a working demo of some of these patterns, see Trained-to-thrill, and this video showing the performance impact. Contents The cache machine - when to store resources On install - as a dependency On install - not as a dependency On activate On user interaction On network response Stale-while-revalidate On push message On background-sync Cache persistence Serving suggestions - responding to requests Cache only Network only Cache, falling back to network Cache & network race Network falling back to cache Cache then network Generic fallback ServiceWorker-side templating Putting it together The cache machine - when to store resources ServiceWorker lets you handle requests independently from caching, so we'll look at them separately. First up, caching, when should it be done? On install - as a dependency * { display: none; } ServiceWorkerinstallactivateCacheNetwork 1 23 4 ServiceWorker gives you an install event. You can use this to get stuff ready, stuff that must be ready before you handle other events. While this happens any previous version of your ServiceWorker is still running & serving pages, so the things you do here mustn't disrupt that. Ideal for: CSS, images, fonts, JS, templates… basically anything you'd consider static to that "version" of your site. These are things that would make your site entirely non-functional if they failed to fetch, things an equivalent native-app would make part of the initial download. self.addEventListener('install', (event) => { event.waitUntil(async function() { const cache = await caches.open('mysite-static-v3'); await cache.addAll([ '/css/whatever-v3.css', '/css/imgs/sprites-v6.png', '/css/fonts/whatever-v8.woff', '/js/all-min-v4.js' // etc ]); }()); }); event.waitUntil takes a promise to define the length & success of the install. If the promise rejects, the installation is considered a failure and this ServiceWorker will be abandoned (if an older version is running, it'll be left intact). caches.open and cache.addAll return promises. If any of the resources fail to fetch, the cache.addAll call rejects. On trained-to-thrill I use this to cache static assets. On install - not as a dependency ServiceWorkerinstallactivateCacheNetwork 1 23 2 Similar to above, but won't delay install completing and won't cause installation to fail if caching fails. Ideal for: Bigger resources that aren't needed straight away, such as assets for later levels of a game. self.addEventListener('install', (event) => { event.waitUntil(async function() { const cache = await caches.open('mygame-core-v1'); cache.addAll( // levels 11-20 ); await cache.addAll( // core assets & levels 1-10 ); }()); }); We're not awaiting the cache.addAll promise for levels 11-20, so even if it fails, the game will still be available offline. Of course, you'll have to cater for the possible absence of those levels & reattempt caching them if they're missing. The ServiceWorker may be killed while levels 11-20 download since it's finished handling events, meaning they won't be cached. In future we plan to add a background downloading API to handle cases like this, and larger downloads such as movies. On activate ServiceWorkerinstallactivateDeleted!!Cache 12 34 Ideal for: Clean-up & migration. Once a new ServiceWorker has installed & a previous version isn't being used, the new one activates, and you get an activate event. Because the old version is out of the way, it's a good time to handle schema migrations in IndexedDB and also delete unused caches. self.addEventListener('activate', (event) => { event.waitUntil(async function() { const cacheNames = await caches.keys(); await Promise.all( cacheNames.filter((cacheName) => { // Return true if you want to remove this cache, // but remember that caches are shared across // the whole origin }).map(cacheName => caches.delete(cacheName)) ); }()); }); During activation, other events such as fetch are put into a queue, so a long activation could potentially block page loads. Keep your activation as lean as possible, only use it for things you couldn't do while the old version was active. On trained-to-thrill I use this to remove old caches. On user interaction PageclickNetworkCache 12 3 Ideal for: If the whole site can't be taken offline, you may allow the user to select the content they want available offline. E.g. a video on something like YouTube, an article on Wikipedia, a particular gallery on Flickr. Give the user a "Read later" or "Save for offline" button. When it's clicked, fetch what you need from the network & pop it in the cache. document.querySelector('.cache-article').addEventListener('click', async (event) => { event.preventDefault(); const id = this.dataset.articleId; const cache = await caches.open('mysite-article-' + id); const response = await fetch('/get-article-urls?id=' + id); const urls = await response.json(); await cache.addAll(urls); }); The caches API is available from pages as well as service workers, meaning you don't need to involve the service worker to add things to the cache. On network response ServiceWorkerPageNetworkCache 1 2 3 Ideal for: Frequently updating resources such as a user's inbox, or article contents. Also useful for non-essential content such as avatars, but care is needed. If a request doesn't match anything in the cache, get it from the network, send it to the page & add it to the cache at the same time. If you do this for a range of URLs, such as avatars, you'll need to be careful you don't bloat the storage of your origin — if the user needs to reclaim disk space you don't want to be the prime candidate. Make sure you get rid of items in the cache you don't need any more. self.addEventListener('fetch', (event) => { event.respondWith(async function() { const cache = await caches.open('mysite-dynamic'); const cachedResponse = await cache.match(event.request); if (cachedResponse) return cachedResponse; const networkResponse = await fetch(event.request); event.waitUntil( cache.put(event.request, networkResponse.clone()) ); return networkResponse; }()); }); To allow for efficient memory usage, you can only read a response/request's body once. In the code above, .clone() is used to create additional copies that can be read separately. On trained-to-thrill I use this to cache Flickr images. Stale-while-revalidate ServiceWorkerPageNetworkCache 1 2 3 4 5 Ideal for: Frequently updating resources where having the very latest version is non-essential. Avatars can fall into this category. If there's a cached version available, use it, but fetch an update for next time. self.addEventListener('fetch', (event) => { event.respondWith(async function() { const cache = await caches.open('mysite-dynamic'); const cachedResponse = await cache.match(event.request); const networkResponsePromise = fetch(event.request); event.waitUntil(async function() { const networkResponse = await networkResponsePromise; await cache.put(event.request, networkResponse.clone()); }()); // Returned the cached response if we have one, otherwise return the network response. return cachedResponse || networkResponsePromise; }()); }); This is very similar to HTTP's stale-while-revalidate. On push message Push MessageServiceWorkerNetworkNotificationCache 12 34 The Push API is another feature built on top of ServiceWorker. This allows the ServiceWorker to be awoken in response to a message from the OS's messaging service. This happens even when the user doesn't have a tab open to your site, only the ServiceWorker is woken up. You request permission to do this from a page & the user will be prompted. Ideal for: Content relating to a notification, such as a chat message, a breaking news story, or an email. Also infrequently changing content that benefits from immediate sync, such as a todo list update or a calendar alteration. The common final outcome is a notification which, when tapped, opens/focuses a relevant page, but updating caches before this happens is extremely important. The user is obviously online at the time of receiving the push message, but they may not be when they finally interact with the notification, so making this content available offline is important. The Twitter native app, which is for the most part an excellent example of offline-first, gets this a bit wrong: Twitter notification while offline Without a connection, Twitter fails to provide the content relating to the push message. Tapping it does remove the notification however, leaving the user with less information than before they tapped. Don't do this! This code updates caches before showing a notification: self.addEventListener('push', (event) => { if (event.data.text() == 'new-email') { event.waitUntil(async function() { const cache = await caches.open('mysite-dynamic'); const response = await fetch('/inbox.json'); await cache.put('/inbox.json', response.clone()); const emails = await response.json(); registration.showNotification("New email", { body: "From " + emails[0].from.name tag: "new-email" }); }()); } }); self.addEventListener('notificationclick', function(event) { if (event.notification.tag == 'new-email') { // Assume that all of the resources needed to render // /inbox/ have previously been cached, e.g. as part // of the install handler. new WindowClient('/inbox/'); } }); On background-sync ServiceWorkersyncNetworkCache 12 3 Note: Background sync hasn't yet landed in Chrome stable. Background sync is another feature built on top of ServiceWorker. It allows you to request background data synchronisation as a one-off, or on an (extremely heuristic) interval. This happens even when the user doesn't have a tab open to your site, only the ServiceWorker is woken up. You request permission to do this from a page & the user will be prompted. Ideal for: Non-urgent updates, especially those that happen so regularly that a push message per update would be too frequent, such as social timelines or news articles. self.addEventListener('sync', (event) => { if (event.id == 'update-leaderboard') { event.waitUntil(async function() { const cache = await caches.open('mygame-dynamic'); await cache.add('/leaderboard.json'); }()); } }); Cache persistence Your origin is given a certain amount of free space to do what it wants with. That free space is shared between all origin storage: LocalStorage, IndexedDB, Filesystem, and of course Caches. The amount you get isn't spec'd, it will differ depending on device and storage conditions. You can find out how much you've got via: navigator.storageQuota.queryInfo("temporary").then((info) => { console.log(info.quota); // Result: <quota in bytes> console.log(info.usage); // Result: <used data in bytes> }); However, like all browser storage, the browser is free to throw it away if the device becomes under storage pressure. Unfortunately the browser can't tell the different between those movies you want to keep at all costs, and the game you don't really care about. To work around this, there's a proposed API, requestPersistent: // From a page: navigator.storage.requestPersistent().then((granted) => { if (granted) { // Hurrah, your data is here to stay! } }); Of course, the user has to grant permission. Making the user part of this flow is important, as we can now expect them to be in control of deletion. If their device comes under storage pressure, and clearing non-essential data doesn't solve it, the user gets to make a judgement call on which items to keep and remove. For this to work, it requires operating systems to treat "durable" origins as equivalent to native apps in their breakdowns of storage usage, rather than reporting the browser as a single item. Serving suggestions - responding to requests It doesn't matter how much caching you do, the ServiceWorker won't use the cache unless you tell it when & how. Here are a few patterns for handling requests: Cache only ServiceWorkerPageCache 1 2 3 Ideal for: Anything you'd consider static to that "version" of your site. You should have cached these in the install event, so you can depend on them being there. self.addEventListener('fetch', (event) => { // If a match isn't found in the cache, the response // will look like a connection error event.respondWith(caches.match(event.request)); }); …although you don't often need to handle this case specifically, "Cache, falling back to network" covers it. Network only ServiceWorkerPageNetwork 1 2 3 Ideal for: Things that have no offline equivalent, such as analytics pings, non-GET requests. self.addEventListener('fetch', (event) => { event.respondWith(fetch(event.request)); // or simply don't call event.respondWith, which // will result in default browser behaviour }); …although you don't often need to handle this case specifically, "Cache, falling back to network" covers it. Cache, falling back to network ServiceWorkerPageCacheNetwork 1 2 3 4 Ideal for: If you're building offline-first, this is how you'll handle the majority of requests. Other patterns will be exceptions based on the incoming request. self.addEventListener('fetch', (event) => { event.respondWith(async function() { const response = await caches.match(event.request); return response || fetch(event.request); }()); }); This gives you the "Cache only" behaviour for things in the cache and the "Network only" behaviour for anything not-cached (which includes all non-GET requests, as they cannot be cached). Cache & network race ServiceWorkerPageNetworkCache 1 2 3 2 3 Ideal for: Small assets where you're chasing performance on devices with slow disk access. With some combinations of older hard drives, virus scanners, and faster internet connections, getting resources from the network can be quicker than going to disk. However, going to the network when the user has the content on their device can be a waste of data, so bear that in mind. // Promise.race is no good to us because it rejects if // a promise rejects before fulfilling. Let's make a proper // race function: function promiseAny(promises) { return new Promise((resolve, reject) => { // make sure promises are all promises promises = promises.map(p => Promise.resolve(p)); // resolve this promise as soon as one resolves promises.forEach(p => p.then(resolve)); // reject if all promises reject promises.reduce((a, b) => a.catch(() => b)) .catch(() => reject(Error("All failed"))); }); }; self.addEventListener('fetch', (event) => { event.respondWith( promiseAny([ caches.match(event.request), fetch(event.request) ]) ); }); Network falling back to cache ServiceWorkerPageNetworkCache 1 2 3 4 Ideal for: A quick-fix for resources that update frequently, outside of the "version" of the site. E.g. articles, avatars, social media timelines, game leader boards. This means you give online users the most up-to-date content, but offline users get an older cached version. If the network request succeeds you'll most-likely want to update the cache entry. However, this method has flaws. If the user has an intermittent or slow connection they'll have to wait for the network to fail before they get the perfectly acceptable content already on their device. This can take an extremely long time and is a frustrating user experience. See the next pattern, "Cache then network", for a better solution. self.addEventListener('fetch', (event) => { event.respondWith(async function() { try { return await fetch(event.request); } catch (err) { return caches.match(event.request); } }()); }); Cache then network ServiceWorkerPageNetworkCache 1 2 1 2 3 Ideal for: Content that updates frequently. E.g. articles, social media timelines, game leaderboards. This requires the page to make two requests, one to the cache, one to the network. The idea is to show the cached data first, then update the page when/if the network data arrives. Sometimes you can just replace the current data when new data arrives (e.g. game leaderboard), but that can be disruptive with larger pieces of content. Basically, don't "disappear" something the user may be reading or interacting with. Twitter adds the new content above the old content & adjusts the scroll position so the user is uninterrupted. This is possible because Twitter mostly retains a mostly-linear order to content. I copied this pattern for trained-to-thrill to get content on screen as fast as possible, but still display up-to-date content once it arrives. Code in the page: async function update() { // Start the network request as soon as possible. const networkPromise = fetch('/data.json'); startSpinner(); const cachedResponse = await caches.match('/data.json'); if (cachedResponse) await displayUpdate(cachedResponse); try { const networkResponse = await networkPromise; const cache = await caches.open('mysite-dynamic'); cache.put('/data.json', networkResponse.clone()); await displayUpdate(networkResponse); } catch (err) { // Maybe report a lack of connectivity to the user. } stopSpinner(); const networkResponse = await networkPromise; } async function displayUpdate(response) { const data = await response.json(); updatePage(data); } Generic fallback ServiceWorkerPageNetworkCache 1 2 3 4 5 If you fail to serve something from the cache and/or network you may want to provide a generic fallback. Ideal for: Secondary imagery such as avatars, failed POST requests, "Unavailable while offline" page. self.addEventListener('fetch', (event) => { event.respondWith(async function() { // Try the cache const cachedResponse = await caches.match(event.request); if (cachedResponse) return cachedResponse; try { // Fall back to network return await fetch(event.request); } catch (err) { // If both fail, show a generic fallback: return caches.match('/offline.html'); // However, in reality you'd have many different // fallbacks, depending on URL & headers. // Eg, a fallback silhouette image for avatars. } }()); }); The item you fallback to is likely to be an install dependency. If your page is posting an email, your ServiceWorker may fall back to storing the email in an IDB 'outbox' & respond letting the page know that the send failed but the data was successfully retained. ServiceWorker-side templating ServiceWorkerPageCacheTemplating Engine 1 2 3 4 Ideal for: Pages that cannot have their server response cached. Rendering pages on the server makes things fast, but that can mean including state data that may not make sense in a cache, e.g. "Logged in as…". If your page is controlled by a ServiceWorker, you may instead choose to request JSON data along with a template, and render that instead. importScripts('templating-engine.js'); self.addEventListener('fetch', (event) => { const requestURL = new URL(event.request); event.responseWith(async function() { const [template, data] = await Promise.all([ caches.match('/article-template.html').then(r => r.text()), caches.match(requestURL.path + '.json').then(r => r.json()), ]); return new Response(renderTemplate(template, data), { headers: {'Content-Type': 'text/html'} }) }()); }); Putting it together You don't have to pick one of these methods, you'll likely use many of them depending on request URL. For example, trained-to-thrill uses: Cache on install, for the static UI and behaviour Cache on network response, for the Flickr images and data Fetch from cache, falling back to network, for most requests Fetch from cache, then network, for the Flickr search results Just look at the request and decide what to do: self.addEventListener('fetch', (event) => { // Parse the URL: const requestURL = new URL(event.request.url); // Handle requests to a particular host specifically if (requestURL.hostname == 'api.example.com') { event.respondWith(/* some combination of patterns */); return; } // Routing for local URLs if (requestURL.origin == location.origin) { // Handle article URLs if (/^\/article\//.test(requestURL.pathname)) { event.respondWith(/* some other combination of patterns */); return; } if (requestURL.pathname.endsWith('.webp')) { event.respondWith(/* some other combination of patterns */); return; } if (request.method == 'POST') { event.respondWith(/* some other combination of patterns */); return; } if (/cheese/.test(requestURL.pathname)) { event.respondWith( new Response("Flagrant cheese error", { status: 512 }) ); return; } } // A sensible default pattern event.respondWith(async function() { const cachedResponse = await caches.match(event.request); return cachedResponse || fetch(event.request); }()); }); …you get the picture. If you come up with additional patterns, throw them at me in the comments! Credits …for the lovely icons: Code by buzzyrobot Calendar by Scott Lewis Network by Ben Rizzo SD by Thomas Le Bas CPU by iconsmind.com Trash by trasnik Notification by @daosme Layout by Mister Pixel Cloud by P.J. Onori And thanks to Jeff Posnick for catching many howling errors before I hit "publish". Further reading Intro to ServiceWorkers Is ServiceWorker ready? - track the implementation status across the main browsers JavaScript promises, there and back again - guide to promises

Iterators gonna iterate

ES6 gives us a new way to iterate, and it's already supported in stable releases of Firefox, Chrome, & Opera. Here it is: for (var num of [1, 2, 3]) { console.log(num); } // Result: 1 // Result: 2 // Result: 3 Unlike for (part in thing) which iterates through property names of an object in a generic way, for (part of thing) lets the object decide which values it gives up on each iteration. Let's pull its guts out Pop the array on the ol' operating table there, and prepare it for surgery. How does it work? Well… var numbers = [1, 2, 3]; numbers[Symbol.iterator]; // Result: function ArrayValues() { [native code] } The ES6 spec defines non-string 'symbol' property names of objects to describe particular behaviours. Symbol.iterator is one of them, it describes how iteration works. var numbersIterator = numbers[Symbol.iterator](); numbersIterator.next(); // Result: Object {value: 1, done: false} numbersIterator.next(); // Result: Object {value: 2, done: false} numbersIterator.next(); // Result: Object {value: 3, done: false} numbersIterator.next(); // Result: Object {value: undefined, done: true} The above is what for (var num of numbers) is doing under the hood. When we call numbers[Symbol.iterator] we get an object back with a .next method. Calling .next gives us an object containing the value, or an indication there are no further values. Let's make our own Let's make an object that iterates over words in a string (in an overly-simple way). Firstly the constructor: function Words(str) { this._str = str; } Then the iterator-factory: Words.prototype[Symbol.iterator] = function() { var re = /\S+/g; var str = this._str; return { next: function() { var match = re.exec(str); if (match) { return {value: match[0], done: false}; } return {value: undefined, done: true}; } } }; We're returning an object with a next method, which returns {value: nextWordInTheString, done: false} until there are none left. And it works! var helloWorld = new Words("Hello world"); for (var word of helloWorld) { console.log(word); } // Result: "Hello" // Result: "world" Well, actually, it doesn't work in Firefox, because Firefox doesn't support the Symbol object yet, it uses the non-standard @@iterator form. You can make it work in Firefox using: Words.prototype[self.Symbol ? Symbol.iterator : "@@iterator"] = func; Edit: Symbol is supported in Firefox 36, which should reach stable in February 2015. Generators Generators, which are defined using function*, are a more convenient way of creating iterator factories. From a generator you yield the values you want to provide, and it takes care of the value/done object for you: function* someNumbers() { yield 1; yield 2; yield 3; } var iter = someNumbers(); iter.next(); // Result: Object {value: 1, done: false} iter.next(); // Result: Object {value: 2, done: false} iter.next(); // Result: Object {value: 3, done: false} iter.next(); // Result: Object {value: undefined, done: true} // or just: for (var num of someNumbers()) { console.log(num); } // Result: 1 // Result: 2 // Result: 3 Although the for-of loop above looks intuitive, it only works through a bit of trickery. For-of calls the object's Symbol.iterator method to get an iterator, but we're giving for-of someNumbers() which is already an iterator. To work around this, iterators returned by generators have a Symbol.iterator method that returns itself, meaning iter[Symbol.iterator]() === iter. It's a bit odd, but it makes the code above work as expected. Generators are usually an easier way to describe iteration. For our Words example, it's much simpler: Words.prototype[Symbol.iterator] = function*() { var re = /\S+/g; var str = this._str; var match; while (match = re.exec(str)) { yield match[0]; } }; What's the use? Not only does this provide a way to allow iteration to be defined on an object-by-object basis, it's also… Lazy You don't need to calculate all the values ahead of time, you can provide them as needed. The Words example does this, we don't seek out the next word until .next is called. This means… Iterators can be infinite No no, nono no no, nono no no, no no there's no limit: function* powersOf2() { var i = 2; yield i; while (true) yield i *= i; } for (var i of powersOf2()) { console.log(i); if (i > 10000) break; } …but make sure you break at some point if you're looping over them. NodeList iteration We've wanted to make NodeList array-like for ages, but it's been a compatibility problem. However, we don't have this problem with iterators: for (var node of document.querySelectorAll('a')) { console.log(node); } \ahem** except Chrome & Opera don't support the above yet. Thankfully, you can polyfill it pretty easily: NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]; String iteration String also has an iterator. This may not sound spectacular, but as Mathias Bynens points out iterating over the symbols of a string is a real pain in ES5 due to unicode issues. String's iterator makes it easy: for (var symbol of string) { console.log(symbol); } Tasty tasty sugar You can convert iterable objects to arrays using [...iterable]. [...new Words("Hello world")] // Result: Array [ "Hello", "world" ] …unfortunately Firefox is the only browser with that feature in stable. Browser support Browser support for these features is pretty good. It's in stable versions of Chrome, Opera, & Firefox, and development editions of IE. That means you can use it today if you're in some kind of context that only runs in modern browsers *ahem* ServiceWorker *ahem*. For use in other browsers, you can use a transpiler such as Traceur. Further reading ES7 async functions - using generators to manage async code, and the evolution of that in ES7 An in-depth guide to generators

Launching ServiceWorker without breaking the web

Update: Thanks to everyone who read and commented, you influenced the direction of the API. We're going for B, the path-based method, but allowing a header to relax these rules so you can put your worker script wherever you want. Many thanks! With ServiceWorkers you can control requests to any page on your origin, and any of the subresource requests made by those pages. You can completely change what those requests return, from returning cached content, to generating your own response with JavaScript. navigator.serviceWorker.register('/sw.js').then(function() { // you now control all pages on the origin! }); This is powerful stuff, and I'm curious to know what security measure you think is appropriate: A: JS content type & request header The ServiceWorker script must be on the same origin it intends to control. The script must be served with a recognised JavaScript content-type header, eg application/javascript. The request contains a service-worker: script header, so the service can tell these apart from normal script requests. Pros An XSS attack isn't enough, the attacker needs to be able to create a resource on the origin to act as the ServiceWorker. The JS content-type stops most user-generated content being used as a ServiceWorker (although poor JSONP implementations could be a way in) An origin can block requests that have the service-worker: script header to disallow ServiceWorkers. Works on all static hosts (github pages, s3 etc). Cons Some hosts give you a portion of an origin to do what you want with, such as //web.stanford.edu/~gwalton/ or //jsbin.com/honawo/. This is false security, as you're sharing localstorage, IDB, cookies etc. However, being able to hijack the request to / because you 'own' /~whatever/ could be considered a security breach too far. B: Scoping to the ServiceWorker script location All the stuff in A. Your area of control is limited by the location of the ServiceWorker script, so if your script is in /~username/sw.js you can only control URLs that begin /~username/. If you want to control a whole origin, you need to be able to put a JS file in the root. Pros All the benefits of A. Protects /~username/ hosts. Appcache recently adopted this tactic for FALLBACK rules. Cons Doesn't protect hosts that allow you to put JS in the root such as JSbin, and maybe /index.php?path=/whatever/blah.js type URLs would be an issue (if they serve with a JS content-type). These sites can still block ServiceWorkers via the request header, which JSBin already has. Can't put your ServiceWorker in your static folder along with your other JS (although you often don't want to serve the ServiceWorker with the same expires headers as other JS) Depending on their path, may still be vulnerable to XSS + broken JSONP Perhaps this breaks a "web rule" that features shouldn't be restricted by resource location (eg, /favicon.ico is bad). C: Require a special header All the stuff in A. Require a special header like Service-Worker-Allowed: true or a specific content-type like text/sw+javascript. Pros All the benefits of A (except static hosts). Protects /~username/ hosts. Protects hosts that allow you to serve JS (even from the root). No restrictions on where you can serve the ServiceWorker from, as long as it's on the same origin. Cons No static sites support it from the start, even ones with a sensible security model like github pages. If a host allows you to run .htaccess, PHP, Perl within cgi-bin etc, you can still take over the origin. Over to you Out of A, B and C, what's your order of preference and why?

Using ServiceWorker in Chrome today

The implementation for ServiceWorker has been landing in Chrome Canary over the past few months, and there's now enough of it to do some cool shit! Unnecessary representation of "cool shit" What is ServiceWorker? ServiceWorker is a background worker, it gives us a JavaScript context to add features such as push messaging, background sync, geofencing and network control. In terms of network control, it acts like a proxy server sitting on the client, you get to decide what to do on a request-by-request basis. You can use this to make stuff work faster, offline, or build new features. I'm biased, but I think ServiceWorker changes the scope of the web more than any feature since XHR. If you want more of an overview on ServiceWorker, check out: ServiceWorker is coming, look busy - 30min talk Is ServiceWorker ready? - feature support status across browsers ServiceWorker first draft - blog post ServiceWorker API - on MDN In Canary today Google & Mozilla are actively developing ServiceWorker. The developer tooling in Canary makes it easier to use at the moment, but expect Firefox to (catch up soon). If you want to hack around with ServiceWorker, open Chrome Canary, go to chrome://flags and enable "experimental Web Platform features". Give the browser a restart, and you're ready to go! Enable experimental Web Platform features By playing with this today, you'll become one of the first developers in the world to do. You may create some cool shit, but there's also a chance you'll step in some uncool shit. If that happens, please file bugs. If you're unsure if you've found a bug or some unexpected-but-correct behaviour, comment on this article (demos of the issue will help massively). Demos Check out ServiceWorkersDemos, which includes things like offline-first news, a clientside wiki, and some trains. Trained-to-thrill The trains one in particular highlights the performance benefits of ServiceWorker. Not only does it work offline, it feels lightning-fast on any connection. Here's a comparrison on a connection throttled to 250kbits: Without vs with ServiceWorker With the ServiceWorker, we get to first-render over a second faster, and our first render includes images, which takes 16 seconds without ServiceWorker. They're cached images of course, but ServiceWorker allows us to present a fully-rendered view while we go to the network for latest content. Getting started To begin, you need to register for a ServiceWorker from your page: navigator.serviceWorker.register('/worker.js').then( function (reg) { console.log('◕‿◕', reg); }, function (err) { console.log('ಠ_ಠ', err); }, ); As you can see, .register take a URL for your worker script and returns a promise. The ServiceWorker script must be on the same origin as your page. If you're new to promises, check out the HTML5Rocks article - I use visits to this to justify my employment, so thanks! You can also limit the scope of the ServiceWorker to a subset of your origin: navigator.serviceWorker .register('/worker.js', { scope: '/trained-to-thrill/', }) .then( function (reg) { console.log('◕‿◕', reg); }, function (err) { console.log('ಠ_ಠ', err); }, ); This is particularly useful for origins that contain many separate sites, such as Github pages. HTTPS only Using ServiceWorker you can hijack connections, respond differently, & filter responses. Powerful stuff. While you would use these powers for good, a man-in-the-middle might not. To avoid this, you can only register for ServiceWorkers on pages served over HTTPS, so we know the ServiceWorker the browser receives hasn't been tampered with during its journey through the network. Github Pages are served over HTTPS, so they're a great place to host demos. Devtools It's early days for Chrome's ServiceWorker devtools, but what we have today is better than we ever had with AppCache. Go to chrome://serviceworker-internals, you'll get a list of all the ServiceWorker registrations the browser is aware of along with the state of each worker within it. Click "Inspect" to open a devtools window for the worker, this allows you to set breakpoints & test code in the console. chrome://serviceworker-intervals Installation Status - This is useful during updates. Running Status / Start / Stop - The ServiceWorker closes when it isn't needed to save memory, this gives you control over that. Sync/Push - These fire events in the ServiceWorker. They're not particularly useful right now. Inspect - Launch a devtools window for the worker. This lets you set breakpoints & interact with the console. Unregister - Throw the ServiceWorker away. Opens the DevTools window … on start - What it says on the tin. Useful for debugging startup issues. Also, shift+refresh loads the current page without the ServiceWorker, this is useful for checking CSS and page script changes without having to wait for a background update. worker.js Let's start with: // The SW will be shutdown when not in use to save memory, // be aware that any global state is likely to disappear console.log('SW startup'); self.addEventListener('install', function (event) { console.log('SW installed'); }); self.addEventListener('activate', function (event) { console.log('SW activated'); }); self.addEventListener('fetch', function (event) { console.log('Caught a fetch!'); event.respondWith(new Response('Hello world!')); }); Now if you navigate to your page, the SW above should install & activate. Refreshing the page will cause the SW to take over the page and respond "Hello world". Here's a demo. You won't see the above console logs in your page, they happen within the worker itself. You can launch a devtools window for the worker from chrome://serviceworker-internals. event.respondWith This is what you call to hijack the fetch and respond differently. It must be called synchronously, passing in a Response or a promise that resolves to one. If you don't call it, you get normal browser behaviour. You'll likely get your response from: new Response(body, opts) - manually created as above. This API is in the fetch spec. fetch(urlOrRequest) - the network. This API is also in the fetch spec. caches.match(urlOrRequest) - the cache. We'll pick this up later in the article. event.request gives you information about the request, so you can do what you want per request. Also, since match and fetch are promise-based, you can combine them. Fallback from cache, to network, to a manually created response, for instance. Updating your ServiceWorker The lifecycle of a ServiceWorker is based on Chrome's update model: Do as much as possible in the background, don't disrupt the user, complete the update when the current version closes. Whenever you navigate to page within scope of your ServiceWorker, the browser checks for updates in the background. If the script is byte-different, it's considered to be a new version, and installed (note: only the script is checked, not external importScripts). However, the old version remains in control over pages until all tabs using it are gone. Then the old version is garbage collected and the new version takes over. This avoids the problem of two versions of a site running at the same time, in different tabs. Our current strategy for this is "cross fingers, hope it doesn't happen". You navigate through this using the install & activate events: install This is called when the browser sees this version of the ServiceWorker for the first time. You can pass a promise to event.waitUntil to extend this part of the lifecycle: self.addEventListener('install', function (event) { console.log('Installing…'); event.waitUntil( somethingThatReturnsAPromise().then(function () { console.log('Installed!'); }), ); }); If the promise rejects, that indicates installation failure, and this ServiceWorker will become redundant and be garbage collected. This all happens in the background while an existing version remains in control. You're not disrupting the user, so you can do some heavy-lifting. Fetch stuff from the network, populate caches, get everything ready. Just don't delete/move anything that would disrupt the current version. The installing worker can take over immediately if it wants, kicking the old version out by calling event.replace(), although this isn't implemented in Chrome yet. By default, the old version will remain in control until no pages are open within its scope. activate This happens when the old version is gone. Here you can make changes that would have broken the old version, such as deleting old caches and migrating data. self.addEventListener('activate', function (event) { console.log('Activating…'); event.waitUntil( somethingThatReturnsAPromise().then(function () { console.log('Activated!'); }), ); }); waitUntil is here too. Fetch (and other) events will be delayed until this promise settles. This may delay the load of a page & resources, so use install for anything that can be done while an old version is still in control. On navigate When a new document is loaded, it decides which ServiceWorker it will use for its lifetime. So when you first visit an origin, and it registers, installs, & activates a ServiceWorker, the page won't be controlled by this worker until the next navigate (or refresh). The exception is if the worker calls event.replace() in the install event, then it takes control of all in-scope pages immediately. navigator.serviceWorker.controller points to the ServiceWorker controlling the page, which is null if the page isn't controlled. In practice: Here's how that looks: Updating a ServiceWorker And here's the code. In that demo I'm forcing install/activate to take 5 seconds. Unfortunately refreshing a single tab isn't enough to allow an old worker to be collected and a new one take over. Browsers make the next page request before unloading the current page, so there isn't a moment when current active worker can be released. We're looking to solve this issue with some special-casing, or a "Replace" button in serviceworker-internals. The easiest way at the moment is to close & reopen the tab (cmd+w, then cmd+shift+t on Mac), or shift+reload then normal reload. The Cache ServiceWorker comes with a caching API, letting you create stores of responses keyed by request. The entry point, caches, is present in Canary, but it isn't useful yet. In the mean time, check out the polyfill. importScripts('serviceworker-cache-polyfill.js'); self.addEventListener('install', function (event) { // pre cache a load of stuff: event.waitUntil( cachesPolyfill.open('myapp-static-v1').then(function (cache) { return cache.addAll([ '/', '/styles/all.css', '/styles/imgs/bg.png', '/scripts/all.js', ]); }), ); }); self.addEventListener('fetch', function (event) { event.respondWith( cachesPolyfill.match(event.request).then(function (response) { return response || fetch(event.request); }), ); }); Trained-to-thrill manages caches dynmanically and at install time to create its offline-first experience. The cache polyfill is backed by IndexedDB, so you can kinda sorta use devtools to see what's in the cache. Cache polyfill in IndexedDB Rough edges & gotchas As I said earlier, this stuff is really new. Here's a collection of issues that get in the way. Hopefully I'll be able to delete this section in the coming weeks, in fact I've deleted loads since I originally posted this article. fetch() doesn't send credentials by default When you use fetch, those request won't contain credentials such as cookies. If you want credentials, instead call: fetch(url, { credentials: 'include', }); This behaviour is on purpose, and is arguably better than XHR's more complex default of sending credentials if the URL is same-origin, but omiting them otherwise. Fetch's behaviour is more like other CORS requests, such as <img crossorigin>, which never sends cookies unless you opt-in with <img crossorigin="use-credentials"> However, I'm concerned this is going to catch developers out. Interested to hear your feedback on this! fetch() is only available in ServiceWorkers fetch should be available in pages, but Chrome's implementation isn't. The cache API should also be available from pages too, but the polyfill depends on fetch, so that too is ServiceWorker-only for the moment. Here's the ticket. This got in the way a little when building Trained-To-Thrill. I wanted to fetch content from the cache while fetching the same content from the network (the search feed from Flickr). I did this by making two XHR requests, but adding a made-up header to one so I could tell them apart when they hit the ServiceWorker. The cache polyfill cannot store opaque responses self.addEventListener('fetch', function (event) { if (/\.jpg$/.test(event.request.url)) { event.respondWith( fetch('https://www.google.co.uk/….gif', { mode: 'no-cors', }), ); } }); The above works fine, even though the image doesn't have CORS headers. It's an opaque response, you can use it in response to requests that don't require CORS (such as <img>), but you cannot get access to the response's content with JavaScript. You can also store these responses in the cache for later use, however this isn't possible with the polyfill, which depends on JavaScript access to the response. No workarounds for this, we need to wait for the native implementation. If installation fails, we're not so good at telling you about it If a worker registers, but then doesn't appear in serviceworker-internals, it's likely it failed to install due an error being thrown, or a rejected promise being passed to event.waitUntil. To work around this, check "Opens the DevTools window for ServiceWorker on start for debugging", and put a debugger; statement at the start of your install event. This, along with "Pause on uncaught exceptions", should reveal the issue. Over to you Developer feedback at this point is crucial. It lets us catch not only issues with our implementation, but also design issues with ServiceWorker itself. File bugs against Chrome, bugs against the spec, or leave a comment below. Let us know what works & what doesn't!

Minimising font downloads

Optimising fonts is pretty difficult for larger sites. There's an easy solution, although only some browsers support it. Translations Français Fonts can be big Really big. They can be anywhere from 70k to many megabytes (compressed of course, because why wouldn't you?). You want bold? Well, you just doubled the size. Italic? Tripled. Bold-italic? Quadrupled. This is a huge deal if those fonts are blocking the rendering of primary content, which they often are. Sub-setting the font helps a lot by getting rid of all the characters that aren't needed. That usually brings the font under 50k. But what characters do you keep? Pretty tough to answer if your site has a lot of content, or if users can create content. If characters outside the subset are used, they'll fallback and look pretty ugly. Cohérent Note how the 'é' is a different typeface to the 'e' You either live with the occasional ugliness, or be conservative with your sub-setting which lands you back with a heavy font download. But there's a better solution! Just let the browser deal with it Take a rough guess at which chars most pages will use, create a subset, but create another resource containing additional characters: /* Small subset, normal weight */ @font-face { font-family: whatever; src: url('reg-subset.woff') format('woff'); unicode-range: U+0-A0; font-weight: normal; } /* Large subset, normal weight */ @font-face { font-family: whatever; src: url('reg-extended.woff') format('woff'); unicode-range: U+A0-FFFF; font-weight: normal; } /* Small subset, bold weight */ @font-face { font-family: whatever; src: url('bold-subset.woff') format('woff'); unicode-range: U+0-A0; font-weight: bold; } /* Large subset, bold weight */ @font-face { font-family: whatever; src: url('bold-extended.woff') format('woff'); unicode-range: U+A0-FFFF; font-weight: bold; } /* And use the font: */ p { font-family: whatever, serif; } The above contains multiple definitions for the same font-family name but with different font-weight & unicode-range combinations. The unicode range U+0-A0 covers basic letters numbers and punctuation. This tells the browser that a bold 'e' glyph can be found at bold-subset.woff, whereas a normal weight 'é' can be found at reg-extended.woff A browser can be smart about this and fetch the minimum required to render the page. If the page uses characters outside of that range, the extra font is downloaded in parallel. You should still provide a suitable fallback in case the font doesn't download in time or the browser doesn't understand the format. Demo Take a look. Optimally, the browser should only download the normal sub-setted resource. If you edit the text to include, say 'ö', the browser should fetch an additional font. Browser support Ok, here's the bad news: Safari: Downloads all the fonts Internet Explorer: Also downloads all the fonts Firefox: Only downloads one font, but the wrong font, leaving rendering broken Chrome: Does the right thing, only downloads the normal subsetted font Opera: Same as Chrome This is pretty bad new for IE and Safari, they end up downloading 300k rather than 30k in this example. It gets much worse if you add in italic & bold-italic, as IE & Safari will download those too, even if they're not used. What's going on with Firefox? Incorrect Firefox rendering Firefox does the right thing with font-weight, it only downloads what the page uses, unfortunately it ignores unicode-range. The "reg-extended" font declaration overwrites the "reg-subset" one, but only because of the source order. "reg-extended" is used to render the page, but it doesn't contain all the characters, so fallbacks are used. Of course, "sans-serif" would have been a better fallback, but I wanted to highlight the problem. But if it's only using the extended font, how are most of the characters are rendering correctly? You wouldn't expect 'o' to be part of the extended set, but it is. In order to be efficient within the font, ö is a combination of the o and ¨ glyphs. Although we didn't want to keep 'o' in the extended font it's retained because other glyphs depend on it. Einstein's theory of glyph reuse No glyphs reuse 'F' or 'b', so they're not in the extended font. Firefox isn't paying attention to unicode-range at all. As well as providing a hint for downloads, unicode-range dictates which characters the browser should use from the font, even if it contains others. You can work around the Firefox issue by including all glyphs in the final fonts listed for your subsets ("reg-extended" and "bold-extended" in this case), or separate your subsets into different font families: /* Small subset, normal weight */ @font-face { font-family: whatever; src: url('reg-subset.woff') format('woff'); unicode-range: U+0-A0; font-weight: normal; } /* Large subset, normal weight */ @font-face { font-family: whatever-extended; src: url('reg-extended.woff') format('woff'); unicode-range: U+A0-FFFF; font-weight: normal; } /* And use the font: */ p { font-family: whatever, whatever-extended, serif; } Here's a demo of that. Firefox downloads both fonts regardless of whether it needs them, but at least the rendering is correct. Push for this to be fixed! When it works, this is a great feature, especially for sites that handle a variety of locales, sites that allow users can submit their own content, or even just for downloading that fancy-ampersand font only when it's needed. If it's useful to you, tell browser vendors (including your use-case): Safari: Download only needed weights, Download only needed ranges Internet Explorer Download only needed weights, Download only needed ranges - last year they indicated they have no intention of implementing this, hopefully they'll change their mind Firefox: Support unicode-range In the meantime If you're already serving a large font, consider splitting it into multiple files with different unicode-ranges. Give each of them a different font-family name to work around Firefox issues. IE, Firefox & Safari will download more than they need, but only the equivalent of the one large font they had before. Chrome and Opera users will get a faster experience, and hopefully this enhancement will land in other browsers. Further reading WOFF2 - supported by Chrome & Opera, reduces font resources by a further ~20%

What happens when you read a response?

There's a bit of disagreement over the behaviour of requests and responses in the fetch API, curious to know what you think… Setting the scene The new fetch API gives the web proper Request and Response primitives. fetch('/whatever').then(function(response) { return response.body.asJSON(); }).then(function(data) { // fetch gave us a response & we // successfully read it as JSON }).catch(function(err) { // either fetch had a network failure, // or parsing as JSON failed }); Fetch takes a URL or a request object: var request = new Request('/whatever', { mode: 'POST', body: formData // or a string, or a blob }); You can store responses in caches: caches.get('cache-name').then(function(cache) { cache.put(request, response).then(function() { // response is now fully cached }); }); You can use responses in ServiceWorkers to respond to requests: this.onfetch = function(event) { event.respondWith( fetch('/whatever') ); }; The question Take this bit of code: fetch('/whatever').then(function(response) { response.body.asJSON().catch(function() { // parsing as JSON failed, let's get the text response.body.asText().then(function(text) { // ... }); }); }); Or this bit: // assume request & cache are in scope fetch(request).then(function(response) { response.body.toJSON().then(function(data) { // make sure the JSON isn't an error response if (!data.err) { // put it in the cache cache.put(request, response); } }); }); Or this: this.onfetch = function(event) { event.respondWith( caches.get('content').then(function(cache) { // fetch the resource return fetch(event.request).then(function(response) { // put it in the cache cache.put(event.request, response); // and send it to the browser return response; }); }) ); }; Would it suprise you if all those were 'wrong'? Each example involves reading the body of the response more than once, and there's a proposal that multiple reads should fail. The first example tries to read as JSON then text. The second example reads as JSON then reads into a cache. The third reads into a cache then hands it back to the browser to read & parse. Why fail on multiple reads? Memory efficiency. The idea is to treat the request/response body as a stream, when you call cache.put(request, response) the response stream is piped to the cache. This means large responses can be handled without buffering them into memory. fetch('/whatever').then(function(response) { return response.body.asJSON(); }).then(function(data) { // ... }); With the above, we don't need to keep the original response in memory as well as the JSON representation. You can stringify the JSON of course, but you'll have lost parts of the original response that mean nothing to JSON, such as whitespace. Because reading means the original stream is gone, there's no opportunity to do a second read, unless… Opting into buffering There are cases such as above where you want to read twice. The solution is to allow cloning of the stream: fetch('/whatever').then(function(response) { return response.clone().body.asJSON().catch(function() { // parsing as JSON failed, let's get the text response.body.asText().then(function(text) { // ... }); }); }); The above works, because .clone is called on the response before the first read. This means the original response can still be read. By doing this, you opt into the memory overhead of retaining the original stream. this.onfetch = function(event) { event.respondWith( caches.get('content').then(function(cache) { // fetch the resource return fetch(event.request).then(function(response) { // put it in the cache cache.put(event.request, response.clone()).then(function() { // done! }); // and send it to the browser return response; }); }) ); }; In the above we send a clone into the cache, and the original back to the browser. Although we're cloning the stream, it's still memory efficient as both streams are being consumed, so we don't need to hold the original in memory. However, if we also .cloned the stream being sent back to the browser, we may be in trouble, as the original response would be unread and in scope. An alternative solution The other proposal is to have all body-reading methods automatically clone, so all the examples above just work. However: fetch('/huge-resource').then(function(response) { cache.put('/huge-resource', response).then(function() { console.log('It worked!'); }); }); This could be problematic as response stays in scope, meaning it can be read again after consuming the whole thing. The browser would have to store the original response in either memory or more likely disk. It's already on disk in the cache of course, but that may not be true in all cases (imagine streaming a video response to a video element). In this model, if you want memory efficiency you interact directly with the low-level stream response.body. That low-level access won't be there at the start, as the streams spec is still in-progress. Over to you! Did it surprise you that multiple reads would fail? Does it make sense after my attempt to explain it? Perhaps it makes sense but you still think it's the wrong thing to do? What do you think? Help us do the right thing!

Service Worker - first draft published

The first draft of the service worker spec was published today! It's been a collaborative effort between Google, Samsung, Mozilla and others, and implementations for Chrome and Firefox are being actively developed. Anyone interesting in the web competing with native apps should be excited by this. Update: Is ServiceWorker ready? - track the implementation status across the main browsers. So, what is it? If text isn't your thing, you can see/hear me spout a lot of this stuff in a 15 minute intro video, or this full-fat 50 minute Google I/O talk. No? Ok, on with the text… The service worker is like a shared worker, but whereas pages control a shared worker, a service worker controls pages. Being able to run JavaScript before a page exists opens up many possibilities, and the first feature we're adding is interception and modification of navigation and resource requests. This lets you tweak the serving of content, all the way up to treating network-connectivity as an enhancement. It's like having a proxy server running on the client. Browsers don't yet support all of the features in this post, but there's some stuff you can play around with. Is ServiceWorker ready tracks the implementation status & the flags you'll need to activiate in order to use them. Making something work offline-first Trained To Thrill is a silly little demo app that pulls pictures of trains from Flickr (although Flickr's search is super broken right now, so sometimes it displays nothing). I built it in a way that uses a service worker to get to first render with cached trains without hitting the network. If there's no cached trains or the browser doesn't support service workers (which is all of them at the moment), it works without the offline enhancement. The service worker code is pretty simple, as is the in-page code that races the network and the cache. Registering a service worker if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/my-blog/sw.js', { scope: '/my-blog/' }).then(function(sw) { // registration worked! }).catch(function() { // registration failed :( }); } If registration worked, /my-blog/sw.js will begin installing for URLs that start /my-blog/. The scope is optional, defaulting to the whole origin. Registration will fail if the URLs are on a different origin to the page, the script fails to download & parse, or the origin is not HTTPS. Wait, service workers are HTTPS-only? HTTP is wide-open to man-in-the-middle attacks. The router(s) & ISP you're connected to could have freely modified the content of this page. It could alter the views I'm expressing here, or add login boxes that attempt to trick you. Even this paragraph could have been added by a third party to throw you off the scent. But it wasn't. OR WAS IT? You don't know. Even when you're somewhere 'safe', caching tricks can make the 'hacks' live longer. We should be terrified that the majority of trusted-content websites (such as news sites) are served over HTTP. To avoid giving attackers more power, service workers are HTTPS-only. This may be relaxed in future, but will come with heavy restrictions to prevent a MITM taking over HTTP sites. Thankfully, popular static hosting sites such as github.io, Amazon S3, and Google Drive all support HTTPS, so if you want to build something to test/demo service workers you can do it there. Browser developer tools will let you use service worker via HTTP for development servers. Messing around with the network /my-blog/sw.js will run in a worker context. It doesn't have DOM access and will run on a different thread to JavaScript in pages. Within it, you can listen for the "fetch" event: self.addEventListener('fetch', function(event) { console.log(event.request); }); This event will be fired when you navigate to a page within /my-blog/*, but also for any request originating from those pages, even if it's to another origin. The request object will tell you about the url, method, headers, body (with some security-based restrictions). It won't fire for the page that registered it, not yet anyway. Pages continue using the service worker they're born with, which includes "no service worker". You can change this behaviour, which I'll come to later. By default, if you refresh the page, it'll use the new service worker. Back to the "fetch" event, like other events you can prevent the default and do something else. self.addEventListener('fetch', function(event) { event.respondWith( new Response('This came from the service worker!') ); }); Now every navigation within /blog/* will display "This came from the service worker!". respondWith takes a Response or a promise for a Response. The service worker API uses promises for anything async, if you're unfamiliar with promises, now's a good time to learn! Making a site work offline The service worker comes with a few toys to make responding without a network connection easier. Firstly, the service worker has a life-cycle: Download Install Activate You can use the install event to prepare your service worker: self.addEventListener('install', function(event) { event.waitUntil( caches.open('static-v1').then(function(cache) { return cache.addAll([ '/my-blog/', '/my-blog/fallback.html', '//mycdn.com/style.css', '//mycdn.com/script.js' ]); }) ); }); Service worker introduces a new storage API, a place to store responses keyed by request, similar to the browser cache. You can have as many caches as you want. waitUntil takes a promise, this defines the length of the install process. Pages won't be controlled by this service worker until it fulfils. If the promise rejects, that indicates install failure and this service worker won't be responsible for further events. cache.addAll([requestsOrURLs...]) is an atomic operation and returns a promise. If any of the requests fail, the cache isn't modified, and the promise rejects (failing installation). Lets use that cache: self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) ); }); Here we're hijacking all fetches and responding with whatever matches the incoming request in the caches. You can specify a particular cache if you want, but I'm happy with any match. caches.match(requestOrURL) returns a promise for a Response, just what respondWith needs. Matching is done in a similar way to HTTP, it matches on url+method and obeys Vary headers. Unlike the browser cache, the service worker cache ignores freshness, things stay in the cache until you remove or overwrite them. Recovering from failure Unfortunately, the promise returned by caches.match will resolve with null if no match is found, meaning you'll get what looks like a network failure. However, by the power of promises you can fix this! self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request).then(function(response) { return response || event.default(); }) ); }); Here we've caught the error, and attempted something else. event.default() returns a response-promise for the original request. You could also call fetch(requestOrURL) to get a response-promise for any URL. Of course, if the item isn't in the cache and the user has no network connection, you'll still get a network failure. However, by the power of promises etc etc… self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request).then(function(response) { return response || event.default(); }).catch(function() { return caches.match('/my-blog/fallback.html'); }) ); }); We can rely on /my-blog/fallback.html being in the cache, because we depended on it in our installation step. This is a really simple example, but it gives you the tools to handle requests however you want. Maybe you want to try the network before going to a cache, maybe you want to add certain things to the cache as you respond via the network (which Trained To Thrill example does). I dunno, do what you want. You can even respond to a local URL with a response from another origin. This isn't a security risk, as the only person you can fool is yourself. Response visibility is judged by it's origin, rather than the original URL. If you give a local XHR request an opaque response (as in, from another origin with no CORS headers), it'll fail with a security error, as it would if it requested the cross-origin URL directly. This is a low-level API. We don't want to make shortcuts until we know where people want to go. AppCache made this mistake, and we're left with a terse easy-to-make manifest that doesn't do what we want and is near-impossible to debug. Although service workers require more code for similar things, you know what to expect because you're telling it what to do, and if it doesn't do what you expect you can investigate it like you would any other piece of JavaScript. Updating service workers If you just need to update a cache or two, you can do that as part of background synchronisation, which is another spec in development. However, sometimes you want to change logic, or carefully control when new caches will be used. Your service worker is checked for updates on each page navigation, although you can use HTTP Cache-Control headers to reduce this frequency (with a maximum of a day to avoid the immortal AppCache problem). If the worker is byte-different, it's considered to be a new version. It'll be loaded and its install event is fired: self.addEventListener('install', function(event) { event.waitUntil( caches.open('static-v2').then(function(cache) { return cache.addAll([ '/my-blog/', '/my-blog/fallback.html', '//mycdn.com/style-v2.css', '//mycdn.com/script-v2.js' ]); }) ); }); While this happens, the previous version is still responsible for fetches. The new version is installing in the background. Note that I'm calling my new cache 'static-v2', so the previous cache isn't disturbed. Once install completes, this new version will remain in-waiting until all pages using the current version unload. If you only have one tab open, a refresh is enough. If you can't wait that long, you can call event.replace() in your install event, but you'll need to be aware you're now in control of pages loaded using some previous version. When no pages are using the current version, the new worker activates and becomes responsible for fetches. You also get an activate event: self.addEventListener('activate', function(event) { var cacheWhitelist = ['static-v2']; event.waitUntil( caches.keys(function(cacheNames) { return Promise.all( cacheNames.map(function(cacheName) { if (cacheWhitelist.indexOf(cacheName) == -1) { return caches.delete(cacheName); } }) ) }) ); }); This is a good time to do stuff that would have broken the previous version while it was still running. Eg getting rid of old caches, schema migrations, moving stuff around in the filesystem API. Promises past into waitUntil will block other events until completion, so you know all the migrations & clean-up have completed when you get your first "fetch" event. This lets you perform updates without disrupting the user, something Android native apps don't do awfully well at. Performance Memory Service workers have been built to be fully async. APIs such as synchronous XHR and localStorage are banished from this place. A single service worker instance will be able to handle many connections in parallel, just as node.js can. The service worker isn't constantly needed, so the browser is free to terminate it and spin it up next time an event needs to be triggered. High memory systems may choose to keep the worker running longer to save on startup cost for the next event, low memory systems may terminate it after each event is complete. That means you can't retain state in the global scope: var hitCounter = 0; this.addEventListener('fetch', function(event) { hitCounter++; event.respondWith( new Response('Hit number ' + hitCounter) ); }); This may respond with hit numbers 1, 2, 3, 4, 1, 2, 1, 2, 1, 1 etc. If you want to retain state, you'll have to save that data in browser storage such as IndexedDB. Latency Running JS per request may have a performance impact. We have ideas to solve that, eg declarative routes such as addRoute(urlRe, sources...), where sources is a preference-ordered list of where the browser should look for a response. However, we don't want to add workarounds to performance issues until we have performance issues to work around. Without an implementation we don't know the size and shape of those issues, or if we have any at all. This is just the start The above is a brief look at some of the API. For full details, see the GitHub repo. If you spot bugs, file issues. If you have questions, ask away in the comments. I expect the API to shift around as bugs are spotted and various browser vendors spot better ways of doing particular things, I'll keep this post updated as that happens. Excitingly, the service worker context may be used by other specifications. This is great as many features the web is missing need to run code independent of pages. Things like: Background synchronisation (specification on GitHub) Reacting to a push message (latest draft uses service workers) Reacting to a particular time & date Entering a geo-fence These will be developed independently to the core service worker spec, but if we can nail this, we get many of the features that make native apps a more attractive option, and that makes me happy. Further reading The spec Is ServiceWorker ready? - track the implementation status across the main browsers JavaScript promises, there and back again - guide to promises ES7 async functions - use promises to make async code even easier The browser cache is vary broken - some research that went into the ServiceWorker cache

Improving the URL bar

iOS has hidden the pathname of URLs for some time now, but recently Chrome Canary introduced something similar behind a flag. I'm not involved in the development of Chrome experiment at all, but I've got more than 140 characters worth of opinion on it… We have a real security problem I recently received an email from my domain name registrar telling me spritecow.com was soon to expire. I followed the link, entered my username and was about to enter my password. I glanced up at the URL, it was HTTPS, great, had the registrar's URL in it, fine. Then I looked closer and realised it wasn't the registrar's URL at all. I was very nearly phished. I get phishing emails all the time, but this one nearly got me. It was well written, it used all the same logos. When I followed the link my URL bar filled up, which is expected, the domain registrar I use doesn't have fantastic URLs. It did the usual trick of front-loading the URL. If I nearly got caught out, surely less savvy users are pretty screwed. Real URL vs phishing URL On top, a page from my bank's website, on the bottom, a phishing equivalent. Part of this is my bank's fault. It's taught me not to expect HTTPS on all of its pages (thankfully the online-banking bits are), and to expect terrible URLs, but the browser could be doing a better job to save me. Find someone who doesn't work in tech, show them their bank's website, and ask them what about the URL tells them they're on their bank's site. In my experience, most users don't understand which parts of the URL are the security signals. Browsers have started to make those parts of the URL bolder, but as you can see from the above screenshot, that isn't enough. Hiding the cruft iOS7 Safari introduced something interesting: Real URL vs phishing URL in iOS7 …and Chrome Canary does something similar behind the flag chrome://flags/#origin-chip-in-omnibox: Real URL vs phishing URL in Chrome Canary Edit: I want to point out that this is an experiment. It's behind a flag in Canary, that's as experimental-yet-public as it gets. It's by no means the final design, and existing in Canary behind a flag does not mean it's certain to appear in stable releases. For me, this is much more obvious (as long as this bug is fixed) and becomes more obvious still with extended validation certificates. HTTPS vs HTTPS + extended validation certificates Browsers stopped showing the username/password part of URLs because it made phishing too easy. This is a natural progression. The death of URLs? No, this isn't URL death. The URL is the share button of the web, and it does that better than any other platform. Linkability and shareability is key to the web, we must never lose that, and these changes do not lose that. The URL is still accessible in iOS by tapping the URL bar, or in the Canary experiment by clicking the origin chip or hitting ⌘-L. Well-designed URLs are more aesthetically pleasing to share, so the requirement for meaningful URLs doesn't go away. To the average user, the URL is noise. It's a mix of protocol, origin, and path. It's a terminal command, echoing it back to the user is weak UI. We should focus on the security of the URL, without harming shareability.

visibility: visible undoes visibility: hidden

html.show-only-the-button { visibility: hidden; } html.show-only-the-button .the-button { visibility: visible; } If you set an element to display: none the browser ignores all of its children, if a child sets itself to display: block it will remain hidden. This isn't true of visibility. Serious? Serious. html.show-only-the-button { visibility: hidden; } html.show-only-the-button .the-button { visibility: visible; } Toggle everything but this So says the spec: hidden: The generated box is invisible (fully transparent, nothing is drawn), but still affects layout. Furthermore, descendants of the element will be visible if they have 'visibility: visible'. — CSS 2.1 I've found this useful when testing paint-related issues, as you can isolate a particular element without disrupting layout. Further reading Having fun with <image> - another spec oddity Animated line drawing in SVG document.querySelector('.the-button').addEventListener('click', function(event) { document.documentElement.classList.toggle('show-only-the-button'); event.preventDefault(); });

ES7 async functions

They're brilliant. They're brilliant and I want laws changed so I can marry them. Update: This feature is now shipping in browsers. I've written a more up-to-date and in-depth guide. Async with promises In the HTML5Rocks article on promises, the final example show how you'd load some JSON data for a story, then use that to fetch more JSON data for the chapters, then render the chapters in order as soon as they arrived. The code looks like this: function loadStory() { return getJSON('story.json').then(function(story) { addHtmlToPage(story.heading); return story.chapterURLs.map(getJSON) .reduce(function(chain, chapterPromise) { return chain.then(function() { return chapterPromise; }).then(function(chapter) { addHtmlToPage(chapter.html); }); }, Promise.resolve()); }).then(function() { addTextToPage("All done"); }).catch(function(err) { addTextToPage("Argh, broken: " + err.message); }).then(function() { document.querySelector('.spinner').style.display = 'none'; }); } Not bad, but… This time with ES7 async functions… async function loadStory() { try { let story = await getJSON('story.json'); addHtmlToPage(story.heading); for (let chapter of story.chapterURLs.map(getJSON)) { addHtmlToPage((await chapter).html); } addTextToPage("All done"); } catch (err) { addTextToPage("Argh, broken: " + err.message); } document.querySelector('.spinner').style.display = 'none'; } With async functions (full proposal), you can await on a promise. This halts the function in a non-blocking way, waits for the promise to resolve & returns the value. If the promise rejects, it throws with the rejection value, so you can deal with it using catch. Edit: I originally used await within an arrow function, apparently that's not allowed so I've replaced it with a for loop. Domenic gave me a knowledge smack-down on why await can't be used in arrow functions. loadStory returns a promise, so you can use it in other async functions. (async function() { await loadStory(); console.log("Yey, story successfully loaded!"); }()); Until ES7 arrives… You can use async functions and other ES6/7 features today using the Traceur transpiler. Also, you can use ES6 generators to create something akin to async functions. You need a small bit of library code, a spawn function. Then you can use generators similar to async functions: function loadStory() { return spawn(function *() { try { let story = yield getJSON('story.json'); addHtmlToPage(story.heading); for (let chapter of story.chapterURLs.map(getJSON)) { addHtmlToPage((yield chapter).html); } addTextToPage("All done"); } catch (err) { addTextToPage("Argh, broken: " + err.message); } document.querySelector('.spinner').style.display = 'none'; }); } In the above, I'm passing a generator function to spawn, you can tell it's a generator because for the asterisk in function *(). The spawn function calls .next() on the generator, receives the promise at the yield call, and waits for it to resolve before calling .next() with the result (or .throw() if it rejects). ES7 brings the spawn function into the spec and makes it even easier to use. Having a standard way to simplify async coding like this is fantastic! Further reading JavaScript promises, there and back again - guide to promises Promise.resolve() is not the opposite of Promise.reject() - a common misunderstanding with promises

Promises: resolve is not the opposite of reject

When I first started working with promises I had the overly simplistic view that passing a value into reject would mark the promise as "failed", and passing a value into resolve would mark it as "successful". However, the latter isn't always true. new Promise(function(resolve, reject) { resolve(something); }).then(function() { console.log("Yey"); }, function() { console.log("Boo"); }); Even though we aren't calling reject(), the rejection callback console.log("Boo") will be called if either: something is not defined, resulting in an error being thrown, which is caught by the promise and turned into a rejection, or something is a promise that rejects So: new Promise(function(resolve, reject) { resolve(Promise.reject()); }).catch(function() { // This is called }); This is a good thing, as it behaves the same as Promise.resolve() and the return value from callbacks: var promise1 = Promise.resolve(Promise.reject()); var promise2 = Promise.resolve().then(function() { return Promise.reject(); }); var promise3 = Promise.reject().catch(function() { return Promise.reject(); }); All promises above are rejected. When you resolve a value with a "then" method, you're deferring the resolution to the eventual non-promise value. In Practice You can resolve a value without worrying if it's a value, a promise, or a promise that resolves to a promise that resolves to a promise etc etc. function apiCall(method, params) { return new Promise(function(resolve, reject) { if (!method) { throw TypeError("apiCall: You must provide a method"); } var data = { jsonrpc: "2.0", method: method } if (params) { data.params = params; } resolve(postJSON('/my/api/', data)); }); } Now apiCall will reject if method isn't provided, or if postJSON rejects for whatever reason. You've safely handed off the resolution of the promise onto postJSON. Further reading JavaScript promises, there and back again - guide to promises ES7 async functions - use promises to make async code even easier

The browser cache is Vary broken

Jake, why are your blog posts always so depressing? — Domenic Denicola (@domenic) Well, I wouldn't want to disappoint… TL;DR If you use "Vary" to negotiate content, the responses will fight for the same cache space. Additionally, IE ignores "max-age" and Safari is buggy. Content negotiation using "Vary" An item in the HTTP cache is matched by its URL and method (GET, POST etc), but you can specify additional constraints via the "Vary" header. You can use this to serve different content… depending on language via Vary: Accept-Language depending on supported types, eg WebP for images via Vary: Accept when client hints ship, depending on screen DPR and render-width via Vary: CH-DPR, CH-RW Some people are against content negotiation, but I thought it worked well for the cases above. However if you mix it with caching, browsers barf in your face. In your face. Serving WebP conditionally Put your eyes on this: The above is a 90k PNG, unless you're in a browser that supports (and advertises) WebP, then you get a 44k WebP equivalent. At the moment you'll get the WebP in Chrome and Opera. The server looks to see if the "Accept" request header contains 'image/webp', then it serves the relevant file, instructs it to cache for an hour via Cache-Control: max-age=3600, and uses Vary: Accept to indicate the response differs depending on the "Accept" header. IE fails us All versions of IE ignore the max-age=3600 bit when "Vary" is present, so they go back to the server for revalidation. For the above image, that means downloading the whole thing again, although from IE7 onwards you can reduce this to an HTTP 304 by using an ETag. MSDN has more details on IE's caching of "Vary" responses. To avoid this, don't send the "Vary" header to IE, and prevent intermediate caches storing it with Cache-Control: max-age=3600, private. Ilya Grigorik covers the server setup for this. And the other browsers? In this case, Firefox, Chrome, Opera and Safari do pretty well. Although there are some caching oddities lurking underneath that you'd only see if you XHR fetched the image with a different accept header. For example… Serving a language pack conditionally Pick a language English French Spanish German Italian Scottish Re-request Greeting: Loading… var greet = document.querySelector('.greeting'); var langSelect = document.querySelector('#lang-select'); function update() { greet.textContent = "Loading…"; fetch("/demos/conditional-lang-cached/lang-pack", { "Accept-Language": langSelect.value }, function(response) { greet.textContent = JSON.parse(response).greeting; }); } langSelect.addEventListener('change', update); document.querySelector('.lang-rerequest').addEventListener('click', update); update(); }()); When you change the select above, it makes a GET request for "/demos/conditional-lang-cached/lang-pack" with the "Accept-Language" header set to whatever you select. The response is set to cache for an hour via Cache-Control: max-age=3600. Many responses, only one gap in the cache Unless you're using IE, pressing "Re-request" doesn't result in a request to the server, max-age=3600 is respected and the cache is used. However, if you switch to another language then back again, you get two full server requests, ETags don't even help. I assumed the browser would cache both responses independently, but no, the "Vary" header is used for validation, not keying. Here's what the browser does: Request "English" Look for an entry in the cache for the URL+method, none found Request from the network Cache it against the URL+method Request "Scottish" Look for an entry in the cache for the URL+method, "English" entry found "English" response has Vary: Accept-Language, but the "English" request has a different "Accept-Language" header to this request, so the cache item doesn't match Request from the network Cache it against the URL+method, which overwrites the "English" entry So, if you want to get the most out of caching, use different URLs rather than content negotiation, else your responses will be fighting over the same space in the cache. I hope we can fix this before Client Hints ship. Changing API response type based on Accept Pick content-type: JavaScript JSON Re-request Loading… var output = document.querySelector('.content-type-output'); var langSelect = document.querySelector('#content-type-select'); function update() { output.textContent = "Loading…"; fetch("/demos/conditional-content-cached/code", { "Accept": langSelect.value }, function(response) { output.textContent = response; }); } langSelect.addEventListener('change', update); document.querySelector('.content-type-rerequest').addEventListener('click', update); update(); }()); When you change the select above, it makes a GET request for "/demos/conditional-content-cached/code" with the "Accept" header set to whatever you select. The response is set to cache for an hour via Cache-Control: max-age=3600. This is similar to the WebP example, except this time the browser can make multiple requests with different headers, as we did with "Accept-Language". Most browsers exhibit the same caching issues we saw with the previous example, except… Safari isn't listening The example above doesn't work in Safari. No matter what you select, it'll fetch the previous response from the cache, meaning it comes back with the wrong content. You ask for JSON, it fetches the JavaScript response from the cache. It's ignoring the "Vary" header in this case, despite getting it right in the "Accept-Language" example. Once again, having different URLs would have avoided this, and given us better caching in other browsers, but this is a bug in Safari (bug ticket). Why don't browsers cache these things independently? It wasn't clear to me until I tried to write an implementation. Let's say we make a GET request to "/whatever/url": # Request: Accept: application/json # Response Cache-Control: max-age=3600 Vary: Accept …some JSON… Now let's make another request: # Request: Accept: */* # Response Cache-Control: max-age=3600 …some HTML… Say we cached both, what happens if we made this request?… # Request: Accept: application/json Unfortunately the above request matches both entries in the cache. The first entry matches because we vary on "Accept", but the "Accept" header is the same. The second entry matches because it doesn't care about the "Accept" header, it has no "Vary" header. It feels like the first entry should be the match because it's more specific, but HTTP caching has no concept of specificity. Having different "Vary" headers for the same URL messes things up. I recently drafted a programmable HTTP cache for the ServiceWorker, the rule we use here is "first match wins". But when adding to the cache, anything the request matches is overridden. So if we added a new entry: # Request: Accept: application/json # Response Cache-Control: max-age=3600 Vary: Accept …some newer JSON… …that would overwrite both previous entries because the request matches. Otherwise, we'd be adding a request to the cache that cannot be reached, because the Accept: */* entry would always match first. Hopefully that'll work!

Don't use flexbox for overall page layout

When I was building this blog I tried to use flexbox for the overall page layout because I wanted to look cool and modern in front of my peers. However, like all of my other attempts to look cool and modern, it didn't really work. Why? Well, take my hand and follow me into the next section… Update: Don't let this post scare you off flexbox, it's one of the best layout systems we have on the web today. However, there's a growing problem on the web when it comes to content shifting around during loading. For large amounts of content, flexbox can cause this, whereas grid is less likely to, but more commonly content-shift is caused by JS modifying the DOM. "Tools not rules" - test your layout with a 2g connection & large amounts of content, and ensure things are stable during loading. Flexbox vs Grid Here's a basic three column design: The Holy Grail, apparently Here it is mocked up using flexbox (works in all modern browsers), and here it is using grid layout (works in IE10+). Both look the same, the difference is in how they load: Flexbox vs Grid Browsers can progressively render content as it's streamed from the server, this is great because it means users can start consuming content before it's all arrived. However, when combined with flexbox it causes misalignment and horizontal shifting. It's difficult to spot too, you're unlikely to notice it while developing locally, or via a super-fast connection. In those cases the page loads too quickly to notice. Here's a demo that displays the columns on a delay, similar to how they will appear on a slower connection. Flexbox: content dictates layout Here's a simplified version of the layout: .container { display: flex; flex-flow: row; } nav { flex: 1; min-width: 118px; max-width: 160px; } .main { order: 1; flex: 1; min-width: 510px; } aside { flex: 1; order: 2; min-width: 150px; max-width: 210px; } The container declares itself as a flexbox, and child elements declare how they'd like to interact with one another within the flexbox. As the page loads, the container starts to receive the first child, the main content. At this point it's the only child and it has flex: 1, so it gets all of the space. When the nav starts to arrive, the main content has to resize to make room for it, which causes that ugly re-layout. Grid: container dictates layout (to some extent) Here's a simplified version of the same layout: .container { display: grid; grid-template-columns: (nav) minmax(118px, 160px), (main) minmax(612px, 1fr), (aside) minmax(182px, 242px); } nav { grid-area: nav; } .main { grid-area: main; } aside { grid-area: aside; } Note: The code above is based on the latest spec and isn't implemented in any browser, yet. You should bother your favourite browser vendor about this. Here the layout is defined in the container, so the nav is rendered into the middle column as soon as it starts to arrive. But grid can load poorly too... To load nicely, you need to restrict yourself to configurations that can be predetermined by the grid container. Here are some examples that break that: .container { display: grid; grid-template-columns: /* Size defined by content, so will change with content */ (foo) max-content, /* Same again */ (bar) min-content, /* Computes to minmax(min-content, max-content), so same again */ (hello) auto; } aside { /* This column isn't defined by the container, so one is created dynamically. This will cause content to shift as 'aside' appears in the container */ grid-column: 4; } But don't write-off flexbox! Flexbox is great, it just isn't the best thing for overall page layouts. Flexbox's strength is in its content-driven model. It doesn't need to know the content up-front. You can distribute items based on their content, allow boxes to wrap which is really handy for responsive design, you can even control the distribution of negative space separately to positive space. This nav bar by Chris Coyier is a great example of something that makes more sense as a flexbox than grid. Flexbox and grid play well together, and are a huge step forward from the float & table hacks they replace. The sooner we can use them both in production, the better. Further reading Solving rendering performance problems Progressive enhancement is faster - the benefits of progressive rendering

Progressive enhancement is faster

Progressive enhancement has become a bit of a hot topic recently, most recently with Tom Dale conclusively showing it to be a futile act, but only by misrepresenting what progressive enhancement is and what its benefits are. You shouldn't cater to those who have deliberately disabled JavaScript …unless of course you have a particular use case there. Eg, you're likely to get significant numbers of users with the Tor Browser, which comes with JS disabled by default for security. If you do have that use case, progressive enhancement helps, but that's not its main benefit. You should use progressive enhancement for the reasons I covered a few weeks back. However, my article was called out on Twitter (the home of reasonable debate) as just "words on a page" and incompatible with "real-life production". Of course, the article is an opinion piece on best practice, but it's based on real examples and actual browser behaviour. However, I wanted to find more tangible "real-world" evidence. Turns out I was staring at it. I love Tweetdeck I don't have Chrome open without a Tweetdeck tab. I use it many times a day, it's my Twitter client of choice. I manage multiple accounts and keep an eye on many searches, Tweetdeck makes this easy. It does, however, depend on JavaScript to render more than a loading screen. Twitter used to be the same, but they started delivering initial content as HTML and enhancing from there to improve performance. So, they deliver similar data and cater for similar use cases, this makes them perfect for a real-world comparison. The test To begin, I: Set up a Tweetdeck account with six columns Closed all other apps (to avoid them fighting for bandwidth) Connected to an extremely fast wired network Used the Network Link Conditioner to simulate a stable 2mbit ADSL connection Cleared the browser cache I recorded Tweetdeck loading in a new tab, then cleared the browser cache again and recorded six Chrome windows loading the equivalent content on Twitter (here's the launcher if you're interested). 2mbits may sound slow, but I've stayed at many a hotel where I'd have done dirty dirty things for anything close to 2mbits. Broadband in the area I live is 4mbit max unless you can get fibre. On mobile, you'll be on much lower speeds depending on signal type and strength. The results Here are the two tests playing simultaneously (note: they were executed separately): Tweetdeck vs Twitter - Empty cache 02.080s: Tweetdeck renders loading screen. So Tweetdeck gets past the blank screen first. 02.150s: Twitter renders three "columns" of tweets, that's only 70ms later than Tweetdeck shows its loading screen. 02.210s: Twitter renders six columns of tweets. Twitter has delivered the core content of the page. 04.070s: Tweetdeck renders six empty columns. Twitter is downloading background images. 05.180s: Tweetdeck renders its first column of tweets. 06.070s: Tweetdeck delivers another column. 06.270s: …and another. 08.030s: …and another. 08.230s: …and another. 10.120s: …and another, and that's all the core content delivered by Tweetdeck. Tweetdeck is fully loaded at this point, whereas Twitter continues to load secondary content (trends, who to follow etc). 14.210s: Twitter finishes loading secondary content. So Twitter gets the core content on the screen 7.91 seconds earlier than Tweetdeck, despite six windows fighting for resources. For the first bit of core content, Twitter gets it on screen 3.03 seconds sooner. Twitter takes 4.09 seconds longer to finish loading completely, but this includes a lot of secondary content and heavy background imagery that Tweetdeck doesn't initially show. On Twitter, the core content is prioritised. That's with an empty cache, but what about a full one? Here's the same test, but ran a second time without emptying the browser cache: Tweetdeck vs Twitter - Full cache 00.060s Tweetdeck renders loading screen. So Tweetdeck gets past the blank screen first, again, but much sooner. 00.090s Twitter renders the first "column" of tweets. 01.010s Twitter renders second "column". 01.110s Twitter renders third "column". 01.190s Twitter renders fourth "column". 01.200s Tweetdeck renders six empty columns. 01.230s Twitter renders fifth "column". 01.240s Twitter renders sixth "column". Twitter has now delivered all core content. 02.100s Tweetdeck renders first column. 02.160s Tweetdeck renders second column. 02.260s Tweetdeck renders third column. 03.030s Tweetdeck renders fourth column. 04.010s Tweetdeck renders fourth and fifth columns. Tweetdeck has now delivered all core content. 04.050s Twitter finishes downloading secondary content. So, with a full cache, Twitter beats Tweetdeck to all core content by 2.77 seconds, and first core content by 2.01 seconds. Which test represents the "real-life" case? Well, something in between the two. As you browse the web you'll knock resources out of your cache, but also the site will cause cache misses through rapid deployments which change the urls of resources. Current best practice is to combine & minify your CSS/JS files & give them a unique url, so whenever you need to make a change, no matter how small, the url changes and the client has to download the new resource. Roll on HTTP2, I say. Is this test fair? Well, no. It's real-life, and as such it has the uncertainty of real-life. But those are two expertly-built sites that offer similar content. The test is unfair to Tweetdeck because: A lot of the XHR requests they make could be rolled into one, improving performance despite JS reliance (assuming this wouldn't have large overhead on the server) The test is unfair to Twitter because: Six separate requests are made for markup, whereas a true progressive competitor would use one. This incurs the HTTP overhead and repetition of common markup. It undergoes 6x the style calculations and layouts compared to Tweetdeck (because Twitter is running in six separate windows). I zoomed out all the Twitter windows so more content was visible, so the six windows have the paint overhead of scaling. Of course, Tweetdeck gets away with it because it's a tab I always have open, so I don't go through the startup cost often. After that startup cost, Tweetdeck updates all columns in real-time & interactions feel as snappy as they do on the enhanced Twitter site. It's extremely rare for a website to become a perma-tab in users' browsers, and certainly something you shouldn't bet your performance on. Twitter did this, but it turned out people shared & followed links to individual tweets and timelines, and the startup cost made them feel incredibly slow. They fixed this with progressive enhancement. The results aren't surprising Here's what a progressively enhancing site needs to download to show content: some HTML CSS …and those two download pretty much in parallel. The HTML will get a head start, because the browser needs to read the HTML to discover the CSS. The whole of the CSS will block rendering to avoid FOUC, although if you want to put your foot to the floor you can inline the CSS for top-of-page content, then include your link[rel=stylesheet] just before the content that isn't covered by the inlined CSS. Rendering isn't blocked by all your HTML, the browser can update the rendering as it receives more markup. This works with gzipped HTML too. Check out the full WHATWG spec (warning: it's 2.6mb), although the markup is huge, it gets to first-render really quickly. Ideally you'd have your script in the head of the page loading async, so it gets picked up by the parser early and enhances the page as soon as possible. If a site does its content-building on the client based on API calls, such as Tweetdeck, here's what it needs to download: All HTML (although small) CSS JavaScript JSON (API call) The HTML, CSS and JavaScript will download pretty much in parallel, but the JSON download cannot start until the JavaScript has downloaded, because the JavaScript triggers the request. The JavaScript on one of these pages can be significant, for instance Tweetdeck's scripts total 263k gzipped. As Adam Sontag points out (using Mike Taylor as his medium), that's the size of a few images we'd use on a page without thinking twice. But we would think twice if those images blocked core content from downloading and displaying, which is the case with the JavaScript on these sites. Hey, images even progressively render as they download, JavaScript can't do anything until the whole file is there. Getting something on screen as soon as possible really improves the user-experience. So much so that Apple recommends giving iOS apps a static bitmap "Launch Image" that looks like the first screen of your app. It's a basic form of progressive enhancement. Of course, we can't do that on the web, we can do better, we can show actually core content as soon as possible. I'm open to examples to the contrary, but I don't think a JS-rendered page can beat a progressively enhanced page to rendering core content, unless the latter is particularly broken or the content can be automatically generated on the client (and generating it is faster than downloading it). I'm keen on the progressive enhancement debate continuing, but can we ditch the misrepresentation and focus on evidence? That goes for both sides of the debate.

Having fun with

Did you know that this works in every browser? <image src="f1.jpg"> Look, here's one: An <image> You might think it's leaking from SVG, but SVG images don't use src, they use xlink:href. Let's all take a moment to laugh at xlink. Done? Ok… In the first age of the web, some people accidentally typed <image> instead of <img>. Browsers decided they should error-correct for it and we've been stuck with it ever since. Update: As Elijah Manor points out, there's a comment hidden in the WHATWG spec suggesting a study was done in late 2005 showing ~0.2% of pages used the <image> element. But what about: document.querySelector('image').src = "kettle.jpg"; Well, that throws an error in Chrome, Safari and Firefox, because querySelector('image') returns null. Whereas it works fine in IE. Try it: Change the image above How about: document.querySelector('img').src = "kettle.jpg"; That works in all browsers, including IE. In fact, querySelector('img') and querySelector('image') are completely interchangeable in IE. Try it: Change the image above How about: var image = document.createElement('image'); console.log(image.tagName); Well, it's "IMG" in Chrome, Safari & IE, but it's "IMAGE" in Firefox. How about: var image = document.createElement('image'); image.src = 'f1.jpg'; document.body.appendChild(image); This works in Chrome, Safari & IE, but Firefox treats it as an unknown element and doesn't load the image. Try it: Add image What's going on? It seems: Firefox aliases 'image' to 'img' at parse-time Chrome & Safari alias 'image' to 'img' at element-creation time IE aliases 'image' to 'img' throughout the runtime <image> isn't defined in any spec…. Update: As Mathias Bynens points out (who's this blog's unofficial error-spotter), <image> is specced by the WHATWG. It's only mentioned as part of token parsing, so although IE's handling of <image> feels more complete, Firefox seems to be the most spec-compliant.

Solving rendering performance puzzles

.curved-text { position: relative; padding-top: 97.8%; } .svg-demo { padding: 10px; background: #f3f3f3; border: 1px dotted #BDBDBD; position: relative; } .svg-demo .btn { position: absolute; left: 10px; top: 10px; } .wobbly-text { position: relative; padding-top: 17.55%; margin: 1em auto; } .curved-text svg, .wobbly-text svg { position: absolute; top: 0; right: 0; bottom: 0; left: 0; width: 100%; height: 100%; } .toggler { cursor: pointer; } .toggler.on { color: green; fill: green; } .inline-svg .no-svg-support, .slow-line-anim, .slow-line-anim-stop, .tspans-line-anim, .tspans-line-anim-stop, .fast-line-anim, .fast-line-anim-stop, .go-fixed, .go-textpath, .make-browser-bleed { display: none; } .curved-text.slow, .curved-text.tspan { visibility: hidden; } .critical-path { position: relative; } .critical-bubble { position: absolute; width: 100%; } .critical-bubble img { width: 307px; margin-top: 16px; } var supportsInlineSvg = (function() { var div = document.createElement('div'); div.innerHTML = ''; return (div.firstChild && div.firstChild.namespaceURI) == 'http://www.w3.org/2000/svg'; }()); var supportsInputRange = (function() { var input = document.createElement('input'); input.setAttribute('type', 'range'); return input.type != 'text'; }()); document.documentElement.className += (supportsInlineSvg ? ' inline-svg' : '') + (supportsInputRange ? ' input-range' : ''); window.requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame; You're missing demos in this post because JavaScript or inline SVG isn't available. The Chrome team are often asked to show the process of debugging a performance issue, including how to select tools and interpret results. Well, I was recently hit by an issue that required a bit of digging, here's what I did: Layout ate my performance I wanted to show a quick demo of text on a path in SVG, then animate it appearing character by character. Although we're dealing with SVG, the process of finding and fixing the issues isn't SVG-specific. It's a heart breaker, she's a heartbreaker. Tell your friends that that girl is a heartbreaker. Don't go near her. She's a male undefined species. She hunts for him, just to get a whiff of his fame. I can't take it. Tell'em she's a heartbreaker. Oh, she's telling me to get it. But, girl I don't want it. I don't want you no more. That's why I'm saying. I can't take it no more. All these paps. all these girls. knocking on my door. Heartbreaker, you didn't get the best of me. I hope you're happy now. You made me cry, but do you know the real reason why? girl. It's a heart breaker, she's a heartbreaker. Tell your friends that that girl is a heartbreaker. Don't go near her. She's a male undefined species. She hunts for him, just to get a whiff of his fame. I can't take it. Tell'em she's a heartbreaker. It's a heart breaker, she's a heartbreaker. Tell your friends that that girl is a heartbreaker. Don't go near her. She's a male undefined species. She hunts for him, just to get a whiff of his fame. I can't take it. Tell'em she's a heartbreaker. It's a heart breaker, she's a heartbreaker. Tell your friends that that girl is a heartbreaker. Don't go near her. She's a male undefined species. She hunts for him, just to get a whiff of his fame. I can't take it. Tell'em she's a heartbreaker. Oh, she's telling me to get it. But, girl I don't want it. I don't want you no more. That's why I'm saying. I can't take it no more. All these paps. all these girls. knocking on my door. Heartbreaker, you didn't get the best of me. Bring joy to my eyes! Argh, that's not joy! Make it stop! = fullText.length) { play = false; } requestAnimationFrame(frame); } function end() { startButton.style.display = 'inline'; stopButton.style.display = 'none'; } startButton.onclick = function(event) { charsShown = 0; play = true; startButton.style.display = 'none'; stopButton.style.display = 'inline'; container.style.visibility = 'visible'; requestAnimationFrame(frame); event.preventDefault(); }; stopButton.onclick = function(event) { play = false; event.preventDefault(); } }()); As the animation continues it gets slower and slower. I opened up Chrome Devtools and made a timeline recording: Layout costs increasing over time The animation starts at a healthy 60fps, then falls to 30, 20, 15, 12, 10 and so on. When we look at an individual frame we can see the slowness is all to do with layout. To render web content the browser first parses the markup, then performs "Recalculate style" to determine which styles apply to each element after the CSS cascade, style attributes, presentational attributes, default styles etc etc. Once this is complete, the browser performs a "Layout" to determine how these styles interact to give each element their final x, y, width and height. At some point later, the browser will "paint" to create pixel data for an area of the document, then finally "composite" to combine parts that were drawn separately. You'll see all these events (and more) fly past in Chrome Devtools' timeline. It's pretty rare for the critical path to be majorly disturbed by layout, but that's what's happening here. The critical path So why is layout having a tantrum? Let's look at the code: SVG <svg viewBox="0 0 548 536"> <defs><path id="a" d="…" /></defs> <text font-size="13" font-family="sans-serif"> <textPath xlink:href="#a" class="text-spiral"> It's a heart breaker… </textPath> </text> </svg> JavaScript var spiralTextPath = document.querySelector('.text-spiral'); var fullText = spiralTextPath.textContent; var charsShown = 0; function frame() { spiralTextPath.textContent = fullText.slice(0, charsShown); charsShown++; // continue the anim if we've got chars left to show if (charsShown != fullText.length) { window.requestAnimationFrame(frame); } } // start the animation window.requestAnimationFrame(frame); Each frame we're changing the value of textContent to include an extra character than the frame before. Because we're replacing the whole text content every frame, the browser lays out every letter despite most of the letters being the same as the frame before. So, as we get more letters, layout takes longer. Update: As pointed out by Mathias Bynens, .slice isn't a safe way to split a string if it contains chars from the astral plane. Thankfully Justin Bieber thinks an astral plane is a space shuttle so it's ok. But if you're dealing with a string containing unicode surrogates, MDN has a workaround. Avoiding multiple layouts Before we animate the text, let's wrap each character in it's own tspan (much like HTML's span). Then we can show each tspan frame-by-frame rather than replacing the whole text every frame. Using the same SVG as before, we change our JavaScript to: var spiralTextPath = document.querySelector('.text-spiral'); var fullText = spiralTextPath.textContent; // empty the text path, we're going to rebuild it spiralTextPath.textContent = ''; // loop over every char, creating a tspan for each var tspans = fullText.split('').map(function (char) { // I love having to use namespaces to create svg // elements, it's my FAVOURITE part of the API var tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan'); tspan.textContent = char; // hide the element, we don't want to show it yet tspan.style.visibility = 'hidden'; // add it to the text path spiralTextPath.appendChild(tspan); return tspan; }); // and now the animation var charsShown = 0; function frame() { // show a char tspans[charsShown].style.visibility = 'visible'; charsShown++; if (charsShown != tspans.length) { window.requestAnimationFrame(frame); } } window.requestAnimationFrame(frame); Update: For the same reasons as str.slice(), str.split('') fails on strings containing unicode surrogates. Unlike display: none, elements with visibility: hidden retain their size and position in the document. This means changing their visibility has no impact on layout, only painting. Job done. It's a heart breaker, she's a heartbreaker. Tell your friends that that girl is a heartbreaker. Don't go near her. She's a male undefined species. She hunts for him, just to get a whiff of his fame. I can't take it. Tell'em she's a heartbreaker. Oh, she's telling me to get it. But, girl I don't want it. I don't want you no more. That's why I'm saying. I can't take it no more. All these paps. all these girls. knocking on my door. Heartbreaker, you didn't get the best of me. I hope you're happy now. You made me cry, but do you know the real reason why? girl. It's a heart breaker, she's a heartbreaker. Tell your friends that that girl is a heartbreaker. Don't go near her. She's a male undefined species. She hunts for him, just to get a whiff of his fame. I can't take it. Tell'em she's a heartbreaker. It's a heart breaker, she's a heartbreaker. Tell your friends that that girl is a heartbreaker. Don't go near her. She's a male undefined species. She hunts for him, just to get a whiff of his fame. I can't take it. Tell'em she's a heartbreaker. It's a heart breaker, she's a heartbreaker. Tell your friends that that girl is a heartbreaker. Don't go near her. She's a male undefined species. She hunts for him, just to get a whiff of his fame. I can't take it. Tell'em she's a heartbreaker. Oh, she's telling me to get it. But, girl I don't want it. I don't want you no more. That's why I'm saying. I can't take it no more. All these paps. all these girls. knocking on my door. Heartbreaker, you didn't get the best of me. Hurrah! We've fixed it! Wait, no we haven't, wtf, stop! You're missing a demo here because JavaScript or inline SVG isn't available. = fullText.length) { play = false; } requestAnimationFrame(frame); } function end() { startButton.style.display = 'inline'; stopButton.style.display = 'none'; } loopyTextPath.textContent = ''; var tspans = fullText.split('').map(function(char) { var tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan'); tspan.textContent = char; tspan.style.visibility = 'hidden'; loopyTextPath.appendChild(tspan); return tspan; }); startButton.onclick = function(event) { charsShown = 0; play = true; startButton.style.display = 'none'; stopButton.style.display = 'inline'; container.style.visibility = 'visible'; requestAnimationFrame(frame); event.preventDefault(); }; stopButton.onclick = function(event) { play = false; event.preventDefault(); } }()); I was really hoping to finish this blog post here. I mean, we've definitely improved things in Chrome, but it's still getting slower as the animation continues. Also, the performance in Firefox and IE has dropped significantly. I have no idea where Firefox is losing speed because it doesn't have a timeline tool like other browsers (c'mon Firefox, we need this). Anyway, what's the timeline showing now? Increasing gap between paint & composite We've lost the huge layout cost, but something is still increasing over time. The timeline is being extremely unhelpful here. Nothing's taking long, but there's an increasing gap between painting and compositing. In fact, looking back at the first timeline recording, the issue is there too, I missed it because I got distracted by the huge layouts. So, something else is getting in the way of pixels hitting the screen. Time to bring out the big guns. About about:tracing Chrome has a lower-level timeline tool hidden in about:tracing. Let's use that to identify the cause. Debugging with about:tracing TL;DW: about:tracing shows the majority of each frame to be taken up by calls to Skia, the drawing API used by Chrome. This tells us the slowdown is paint-related. Turning on paint rectangles reveals the whole SVG canvas appears to be redrawn on each frame. I'm a good web citizen, so I opened a ticket about the lack of SVG paint tracking in devtools. Turns out this is a regression in Chrome Canary, the stable version of Chrome gets it right and shows increasing paints. If only I'd checked there first, I wouldn't have needed to throw myself into the scary lands of about:tracing. The critical path is happy with layout, but it's awfully concerned with all the painting. The critical path, again When does SVG redraw? I've seen enough complex SVG examples to know Chrome's renderer isn't always that dumb. It usually only repaints the updated area. Let's experiment with updating various elements: .redraw-test text { font-size:40px; font-family: sans-serif; }Hello WorldHelloWorld Hello World You're missing a demo here because JavaScript or inline SVG isn't available. Turn on "Show paint rectangles" and click the words above. The first "Hello World" is made of two tspan elements. The second is two text elements. The third is two HTML span elements. From this we can see that updating a tspan causes the parent text to fully redraw, whereas text elements can update independently. HTML span elements also update independently. The same thing happens in Firefox & Safari (unfortunately there isn't a show-paints tool in IE), but I don't think this over-painting is required by the SVG specification. I've made a ticket about it, hopefully an optimisation can be made. Update: Although IE11 doesn't show painted areas live, it will show the size of painted regions in its timeline. Unfortunately the Win7 platform preview of IE11 crashes when I try the timeline. Will update this when I get it working. So there's our answer, we need to ditch the tspan elements and go with a text element per character so they can be painted independently. From tspan to text Unfortunately, SVG doesn't allow text elements inside textPath. We need to get rid of the textPath, then position & rotate each character's text element into a spiral shape. Thankfully, we can automate this. SVG gives us text.getStartPositionOfChar(index), which gives us the x & y position of the character and text.getRotationOfChar(index) which gives us the rotation. We can use this to convert text on a path to a series of absolutely positioned text elements: var svg = document.querySelector('.text-on-path'); var fullText = svg.querySelector('.text-to-convert'); fullText.textContent .split('') .map(function (char, i) { var text = document.createElementNS('http://www.w3.org/2000/svg', 'text'); var pos = fullText.getStartPositionOfChar(i); var rotation = fullText.getRotationOfChar(i); text.setAttribute('x', pos.x); text.setAttribute('y', pos.y); text.setAttribute('rotate', rotation); text.textContent = char; return text; }) .forEach(svg.appendChild.bind(svg)); // get rid of the old text fullText.parentNode.removeChild(fullText); Note that I'm looping twice, once to create all the text elements, and another to add them to the SVG document (using a nifty bit of function binding). This is to avoid interleaving layout-reading operations with layout-changing DOM modifications. If we interleave layout reads and writes the browser needs to relayout the SVG element on each loop, which causes major slowdown, we're talking eleventy-billion times slower. We call this layout thrashing. Anyway, let's take some text on a path and convert it to a series of text elements. Clicking the button below shouldn't effect the rendering, but look at the DOM to see the difference: .wobbly-text text { font-family: sans-serif; font-size: 30px; }Ohh it's some wobbly text, how exciting! Make the text fixed Nah, put it back how it was You're missing a demo here because JavaScript or inline SVG isn't available. That feels like the answer, but unfortunately doing this for all characters in the spiral takes upwards of 20 seconds, except in IE which nails it almost instantly. The vast majority of this time is spent in calls to getStartPositionOfChar and getRotationOfChar (I've opened a ticket to investigate this). If you want to benchmark your particular browser, go for it, but it may make your browser bleed out: Make. Browser. Bleed. You're missing a demo here because JavaScript or inline SVG isn't available. However, we could do this processing once, locally, save the resulting SVG, & serve that to users. This takes our SVG data from 1k to 19k (gzipped), and text position will be based on the machine we generated it on, which may not look right on a machine with a different "sans-serif" font (in which case we could switch to an SVG font). But hey, it works: Move it! That'll do, pig. You're missing a demo here because JavaScript or inline SVG isn't available. r.text()).then(text => { container.innerHTML = text; startButton.style.display = 'inline'; texts = Array.prototype.slice.call(document.querySelectorAll('.curved-text.fast text')); }); function frame() { if (!play) { end(); return; } texts[charsShown].style.visibility = 'visible'; charsShown++; if (charsShown == texts.length) { play = false; } requestAnimationFrame(frame); } function end() { startButton.style.display = 'inline'; stopButton.style.display = 'none'; } startButton.onclick = function(event) { charsShown = 0; texts.forEach(function(text) { text.style.visibility = ''; }); play = true; startButton.style.display = 'none'; stopButton.style.display = 'inline'; requestAnimationFrame(frame); event.preventDefault(); }; stopButton.onclick = function(event) { play = false; event.preventDefault(); } }()); Job done, for real this time Ahh the sweet smell of 60fps. Hardly touching the CPU. The critical path now has very little to complain about. Oh you! In summary... If layouts are causing slowness: Remember that things like innerHTML & textContent will destroy and rebuild the entire content of that element Isolate the thing you're animating (give it its own element) to avoid impacting other elements When showing/hiding elements, visibility: hidden allows you to pay the layout cost earlier If an element is frequently changing size, try to prevent it being a layout dependency for other elements, eg make it position: absolute or use CSS transforms If paint is causing slowness: Use paint rectangles to see if too much is being drawn Avoid complex CSS effects Can you achieve the same or similar effect with the GPU's help? Eg using CSS transforms & transitions/animations. If devtools isn't giving you the answer: Take a deep breath & give about:tracing a try File a bug report against devtools! Image credit: Forest path

Animated line drawing in SVG

.full-sketch-container { padding-top: 35%; position: relative; margin: 25px auto; } .full-sketch-container svg, .squiggle-container svg, .jb-container svg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } .full-sketch-container, .squiggle-sliders, .squiggle-go { display: none; } .squiggle-container { padding-top: 16.87%; position: relative; margin: 25px auto; } .squiggle-container path { stroke: #000; stroke-width: 4.3; fill: none; } .jb-container { padding-top:76.1%; position: relative; margin: 25px auto; } .inline-svg .no-svg-support { display: none; } .input-range .no-range-support { display: none; } .input-range.inline-svg .no-range-svg-support { display: none; } var supportsInlineSvg = (function() { var div = document.createElement('div'); div.innerHTML = ''; return (div.firstChild && div.firstChild.namespaceURI) == 'http://www.w3.org/2000/svg'; }()); var supportsInputRange = (function() { var input = document.createElement('input'); input.setAttribute('type', 'range'); return input.type != 'text'; }()); document.documentElement.className += (supportsInlineSvg ? ' inline-svg' : '') + (supportsInputRange ? ' input-range' : ''); path { fill: none; stroke: #000; stroke-width: 2.7; } .shade { stroke-width: 20.7; stroke: #e5e5e5; } .letter { stroke-width: 3.2; } .bubble { stroke-width: 5; } There's a demo you're missing here because JavaScript or inline SVG isn't available. I like using diagrams as a way of showing information flow or browser behaviour, but large diagrams can be daunting at first glance. When I gave talks about the Application Cache and rendering performance I started with a blank screen and made the diagrams appear to draw themselves bit by bit as I described the process. Here's how it's done: Paths in SVG Paths in SVG are defined in a format that competes with regex in terms of illegibility: <path fill="none" stroke="deeppink" stroke-width="14" stroke-miterlimit="0" d="M11.6 269s-19.7-42.4 6.06-68.2 48.5-6.06 59.1 12.1l-3.03 28.8 209-227s45.5-21.2 60.6 1.52c15.2 22.7-3.03 47-3.03 47l-225 229s33.1-12 48.5 7.58c50 63.6-50 97-62.1 37.9" /> I use Inkscape to create the non-human-readable bits of SVG. It's a tad clunky, but it gives you an SVG DOM view as you edit the document, rather than using its own format and only offering SVG as an export format like Adobe Illustrator. Each part of the d attribute is telling the renderer to move to a particular point, start a line, draw a Bézier curve to another point, etc etc. The prospect of animating this data so the line progressively draws is, well, terrifying. Thankfully we can cheat. Along with things like colour & width, you can make an SVG path dashed and control the offset of the dash: <path stroke="#000" stroke-width="4.3" fill="none" d="…" stroke-dasharray="0" stroke-dashoffset="0" /> dasharray: dashoffset: There's a demo you're missing here because JavaScript, inline SVG or input[type=range] isn't available. node.nodeValue === '0', }); var dasharrayCode = nodeIterator.nextNode(); var dashoffsetCode = nodeIterator.nextNode(); function updateCode(dasharray, dashoffset) { dasharrayCode.nodeValue = dasharray; dashoffsetCode.nodeValue = dashoffset; } function updateSvg(dasharray, dashoffset) { path.setAttribute('stroke-dasharray', dasharray); path.setAttribute('stroke-dashoffset', dashoffset); } var arraySlider = document.querySelector('#array-input'); var offsetSlider = document.querySelector('#offset-input'); function change() { var arrayVal = (Math.pow(arraySlider.value, 2) * pathLen); var array = ''; if (arrayVal && arrayVal 0) { arrayVal = arrayVal.toFixed(2); array = arrayVal + " " + arrayVal; } var offset = (offsetSlider.value * pathLen).toFixed(2); updateSvg(array, offset); updateCode(array, offset); } if ('oninput' in arraySlider) { arraySlider.oninput = change; offsetSlider.oninput = change; } else { arraySlider.onchange = change; offsetSlider.onchange = change; } change(); }()); stroke-dasharray lets you specify the length of the rendered part of the line, then the length of the gap. stroke-dashoffset lets you change where the dasharray starts. Drag both sliders up to their maximum, then slowly decrease the dashoffset. Voilà, you just made the line draw! 988.01 roughly the length of the line which you can get from the DOM: var path = document.querySelector('.squiggle-container path'); path.getTotalLength(); // 988.0062255859375 Animating it The easiest way to animate SVG is using CSS animations or transitions. The downside is it doesn't work in IE, if you want IE support you'll need to use requestAnimationFrame and update the values frame by frame with script. Avoid using SMIL, it also isn't supported in IE and doesn't perform well in Chrome & Safari (I'll do a separate post on this). I'm going to use CSS transitions, so the demos won't work in IE. Unfortunately I couldn't come up with a solid feature detect for transitions on SVG elements, IE has all the properties, it just doesn't work. In the first example I used SVG attributes to define the dash, but you can do the same thing in CSS. Most SVG presentational attributes have identical CSS properties. var path = document.querySelector('.squiggle-animated path'); var length = path.getTotalLength(); // Clear any previous transition path.style.transition = path.style.WebkitTransition = 'none'; // Set up the starting positions path.style.strokeDasharray = length + ' ' + length; path.style.strokeDashoffset = length; // Trigger a layout so styles are calculated & the browser // picks up the starting position before animating path.getBoundingClientRect(); // Define our transition path.style.transition = path.style.WebkitTransition = 'stroke-dashoffset 2s ease-in-out'; // Go! path.style.strokeDashoffset = '0'; Make it so! There's a demo you're missing here because JavaScript or inline SVG isn't available. Using getBoundingClientRect to trigger layout is a bit of a hack, but it works. Unfortunately, if you modify a style twice in the same JavaScript execution without forcing a synchronous layout inbetween, only the last one counts. I wrote about this issue in more detail & how the Web Animations API will save the day over at Smashing Magazine. I usually trigger a layout by accessing offsetWidth, but that doesn't appear to work on SVG elements in Firefox. More fun with dashes Lea Verou used a similar technique to create a loading spinner similar to Chrome's. Josh Matz and El Yosh expanded on this to create this funky cube animation. So far we've been using stroke-dasharray to create a bit of line followed by a gap, but you can create more complex patterns by adding more numbers. For instance, here's Justin Bieber's autograph, where the line is morse code for "Christ our saviour": Further reading visibility: visible undoes a parent element's visibility: hidden Don't use flexbox for overall page layout

Progressive enhancement is still important

About 5 years ago it felt like the progressive enhancement battle had been won, but after watching the reactions to Nicholas Zakas' "Enough with the JavaScript already" it seems all the old arguments are back with reinforcements. Well, I'm wearing my angry-pants and I'm ready for a jog down ranty lane. This is not about users without JS If you turn JavaScript off, you're going to have a tough time on the web. If you remove the steering wheel from your car you're going to have a tough time going round corners. My advice: Leave the steering wheel where it is. Progressive enhancement has never been about users who've turned JavaScript off, or least it wasn't for me. Elevators vs escalators Christian Heilmann re-purposes a Mitch Hedberg observation to relate to progressive enhancement. Basically, when an elevator fails, it's useless. When an escalator fails, it becomes stairs. We should be building escalators, not elevators. Given the diversity of user agents, your JS failing isn't an edge case. Sometimes it'll be the browser's fault, sometimes yours. For example, a few months ago the JavaScript on the Download Chrome page failed. Y'know what happened when you clicked the "Download Chrome" button? Nothing. A dash of progressive enhancement would have allowed people to continue downloading Chrome while the problem was fixed. Reduce your testing efforts in older browsers A couple of years ago I was working on an intranet site for a large company. I built it with progressive enhancement out of habit. The users were mostly on IE7, it was a tightly controlled environment. Then, at the 11th hour, the client asked for the site to work on Blackberrys, and not the shiny WebKit ones, some of them were using the old Blackberry browser. The site was a disaster on those phones. However, the ancient Blackberry wasn't too bad at the ol' HTML and CSS, but when it came to JS it was a random error generator. A little (ahem) UA-sniffing later and we weren't serving JS to the Blackberrys. This got us 90% of the way there, instantly. The rest was just minor CSS tweaks. This was only possible because the site worked without JS. Sure, there were some full-page refreshes that newer browsers did quicker with XHR, and some nice transitions were missing, but it worked. We took this idea further for Lanyrd's mobile site where a basic feature detect was used to decide if scripts should be loaded. We didn't use JavaScript for anything that couldn't handle ApplicationCache, which was what most of the JS was handling. The BBC call this basic feature test "Cuts the mustard". Do yourself a favour, save your JS for the cutting-edge browsers, then you only have to drag the older browsers through HTML & CSS. It's important to do this at the script-loading stage rather than just before execution. Not only do you avoid downloading the JS in browsers that don't need it, but you save on parsing too. On bockety old mobiles, such as the Blackberry, parsing can take many orders of magnitude longer than the downloading, and the UI is blocked while this happens. Reduce your testing efforts in general When I make something work on the server, it has to work in Django 1.5.1 running on Python 2.7.3 served through Nginx 1.1.19 etc etc etc. I control the lot, if I change one of the dependencies I can test it before deploying. Code running on the client is more of a testing effort due to the diversity of interpreter vendors and versions. Unless your server architecture is frequently changing, having most of your logic on the server is easier. Be faster JavaScript is more powerful than HTML & CSS, it's like a Formula 1 car whereas HTML & CSS is a bicycle. However, in a race, the bicycle will get off the line first. An F1 car has a complex start-up procedure and requires a team of people to get it going. If the race is short enough, the bicycle will win. Here's how most pages load: HTML downloads CSS downloads CSS fetches additional assets JS downloads JS executes JS fetches additional assets JS updates DOM This is the order in which they generally start, 1-4 can happen in parallel to some degree. In a progressively enhanced page, the whole of step 2 blocks rendering, but other than that the HTML can be parsed and rendered in chunks as it downloads. This is assuming your scripts are async or are at the bottom of the document, which they should be (more on script loading). In this case, step 6 probably isn't necessary and step 7 is minor. Also, most modern browsers will take a peek at pages you might open and scans them for things it needs, so there's a chance the browser already has a head start on downloading the CSS and JS. In a page that's entirely JS-driven, as in <body></body>, your first render is blocked by all 7 steps. Sure, your HTML download is tiny, but your JS isn't, and you don't get any progressive rendering. If your JS needs to download additional assets, this also blocks rendering and cannot be seen by the look-ahead scanner that can pick up CSS & JS downloads. JS dependent pages that aren't simply <body></body> may be able to get some stuff onto the screen sooner, but the page cannot be interacted with until all 7 steps are complete. But yes, if the race is a bit longer, the F1 car will win. This is why you progressively enhance. You're off the line straight away on your bicycle, but as the F1 car comes past to overtake, you jump off the bike, do a jaw-dropping backflip in the air, land in the F1 cockpit, and accelerate into the distance. A great example of this is Twitter, their JS-driven site was painfully slow and clunky. They're fixing this by switching to progressive enhancement. Fans of progressive enhancement had a "told you so" moment when Airbnb's improved performance by delivering requested content as HTML (and sold it as a new idea). And don't get me started on blogger.com. It's not doubling up on work There's a perception that progressive enhancement means building everything on the server then building it again, like for like, on the client. This is rarely the case. Lean on the server as much as possible. If you want to dynamically update a part of the page, that's great if it's actually faster, but do you need client-side templates? Could the server just send you the new HTML for the element? Often, yes. Remember, HTML is a semantic data format just like JSON, and if you're simply going to convert that JSON to HTML on the client, just do it on the server. If you need client-side templates, use a format that can be shared with the server, such as Mustache. Even then, consider compiling the templates to JavaScript functions on the server and serve those, saves every client having to do the parsing & compilation. Work with the browser, not against it Lanyrd's mobile site is built the progressive enhancement way, but we used JavaScript to handle all page-to-page navigation (similar to using XHR to bring the new content in). This was to hack around limitations in AppCache, and it came at a cost. So, you click on a link, JS changes the content. At this point, the URL isn't reflecting the content. That's ok, we have history.pushState(). Then the user clicks the back button, we pick up the URL change and switch the content back. However, this doesn't feel natural because the user is sent back to the top of the page, whereas expectation is to restore the scroll position. Ok, so now we have to record scroll positions before we change content, and work out the conditions for when scroll position should be restored. The more you take over from the browser, the more complex yet expected browser behaviour you have to reimplement in JavaScript. And if different browsers do different thing, you pick one behaviour & it feels odd to users who aren't used to it. "App" is not an excuse "Yeah, but I'm building a webapp, not a website" - I hear this a lot and it isn't an excuse. I challenge you to define the difference between a webapp and a website that isn't just a vague list of best practices that "apps" are for some reason allowed to disregard. Jeremy Keith makes this point brilliantly. For example, is Wikipedia an app? What about when I edit an article? What about when I search for an article? Whether you label your web page as a "site", "app", "microsite", whatever, it doesn't make it exempt from accessibility, performance, browser support and so on. If you need to excuse yourself from progressive enhancement, you need a better excuse. There are, of course, exceptions …but they must be exceptions. SpriteCow, Jank Invaders and Request Quest all depend on JS. SpriteCow could be done server-side, but it's a rare case where the client can totally outperform the server due to the transfer of image data. Jank Invaders is basically a demo of a JavaScript feature, and like many games simply doesn't have a sensible server-side fallback. Request Quest depends on JavaScript because it's… well… it has to process… yeah ok, I just got lazy. Don't do that! One of the best things about the web is it can rival native applications without a hefty initial download, without an install process, and do so across devices old and new. Let's keep it that way. Edit: Emil Björklund wrote a similar post yesterday that's worth reading. Another edit: Emil's server appears to be down at the moment, not even progressive enhancement can save us from that.

I've only gone and done a blog

I've been promising myself I'd start a blog for about 100 years now, but it's finally here! I decided to build everything from scratch to force myself to learn Vagrant, Puppet and other general sysops stuff that was on my "to explore" list. This was dumb in terms of how long it took me to get everything up & running, but I have no regrets. To bless the blog's launch, may I present… Foreword by Alex Russell It was a dark, gloomy summer day -- as you expect in London -- when Jake mentioned his blog to me. In the hushed tones one reserves for the death of a loved one or the news that a friend has acquired a 3rd cat, he unveiled the boxes (if not the text) that would become his tragic last forray into relating to developers. It wasn't the fitting end his all-too-short career deserved. But an end it was. What could one say to someone driven so mad by perfection that they had to re-build a shit WordPress with all new, custom security vulnerabilities? Alas, the light that shone so brightly was now just a wisp of smoke and an ember, all but dark. The insightfulness...the wit...the studied humor. All erased by an act of hubris so shocking, so pedantic that it could scarcely be discussed in polite company. To contemplate how far he had fallen from dick jokes and genius comic timing is to reflect on tragedy itself. No fearlessly donned mankini, this, but a horrid, distended mangnum opus of self-indugence and self-loathing. The hours of self-reflection his ascendence to a life of luxury at Google were likely the torturous conditions that his previous hard-scrabble days as a minor-leauge webdev had deprived him of, to his former benefit. This...this corpuscle of self regard, too grotesque even for the world "blog" was surely the predictable outcome. What madness had preceeded it? How many OKR discussions and pointless meetings...how much selling and shilling...had led to this? It's comforting, then, to think back on his early work. The fart noises, the tales of debauchery and tongue-in-cheek debasement. Simpler, better times. Let us remember his work as it was: vibrant, in a pink mankini. Erected, if you will, as a statement that even northerners can make it big.