 Okay. Right. Do I have everything plugged in? Now, I've got two click-its here. Okay. So a lot of times when people do these kind of polls of an audience that they ask people to raise their hand, it's kind of totally meaningless. It's just to kind of make sure you're all awake. But I'm going to ask you a question and it's actually going to decide the talk I give. So here goes, who here has sort of seen a talk about service workers before or kind of know roughly what they are? Okay. Right. So I'm not going to do the intro talk then. So if you don't enjoy the talk that I'm about to give, it's actually your fault. You picked it. So you should have picked the other one. So firstly, I'm going to do a little bit of a recap. I built a little web app called Emojoy, which you can find at this URL. It started just as a website, but I turned it into a progressive web app. And if you're looking to do the same thing, the first thing you need to do is to integrate the site with the operating system and the UI. And one of the ways of doing that is in the head of the page, you can use a theme color, and Chrome will use that to kind of change the color of the location bar there. So it's a small quick win for integration. But the main part is this web app manifest. So inside the manifest, it looks like this. It's got details on the name of the app, the icon, all that sort of stuff. And once you have this, you will be able to add the site to their home screen, and they get the icon and the name that you specified. But not only that, when the user taps the icon, they get a splash screen. This is reasonably new in Chrome. And this is where the browser is booting up and while your page is getting to first render. And so the icon, the text there and the background color and the sort of the purple strip across the very top there, that's all taken from your manifest, so you can configure all of that. And once that goes away, you get your site, but without the URL bar, that's an option you can have. So here it just feels like a native app. However, this whole illusion of native comes crashing down when you're offline. So this is pretty bad, right? The user has launched our app, they've come back to our app, they've added it to the home screen, that's how much they like it. But we have failed them to such an extent that the browser has had to step in and defend us and they do this by blaming the user saying you are offline. This is your fault. If we're going to compete with native, this is like an operating system level error message, like a full crash. So this is not good enough. But things get worse. So offline isn't the worst thing that we face. This is, I call it, li-fi. This is when your phone says it has connectivity, but it doesn't really. I think we've all experienced this. The li-fi experience is just that. So that the splash screen goes away when the site gets to its first render. But with li-fi, that never happens. This is worse than offline. With offline, you get a quick answer. The answer is no, but it is a quick answer. Here, the user is just left waiting. You're forcing the user to stare at this or give up. And with every passing second, they hate the user experience a little bit more. And this is why we should avoid treating online and offline as these kind of binary states. Because if we cater for online and we cater for offline as separate things, this situation goes completely unsolved. And that's why the gold standard here is offline first. Offline first solves these problems. With offline first, we assume offline and do as much as we can with local content than try and get content from the network. And the more we get to render without a connection, the better. So you should think of the network as a piece of progressive enhancement that might not be there. And here's how we do it. So to begin with, register for a service worker. And this isn't some kind of magic manifest format. It's just JavaScript. Because we thought, why do we invent a whole new thing when we've got a room full of JavaScript developers, a world full of JavaScript developers, and loads of existing tooling? Of course, we should write this register call here in a simple feature detect. Because, you know, there are older browsers out there that don't support service worker. And this feature detect prevents them from hurting themselves and others around them. Anyway, in that script, I'm just going to register for this event, this fetch event, so I'm going to have a debugger statement there. And when I do that, now if I refresh the page, I hit a break point. And you get this event object, and it has a request property. And this is the request for the page. And you get things like the URL, headers, the type of request. But you also get one of these events for every request that the page makes, the CSS, the JavaScript, the fonts, the images. I get the event for these avatars even though they're on origin. So it's not just your own origin. So by default, requests will go from the page to the network. And there's not a lot you can do about it. But once you introduce a service worker, it controls pages, and requests will go through it. But like other events on the web, you can prevent the default and do your own thing. And that's where things get really interesting. So let's use that to go offline first. So the first thing we need is an application shell, which is just the site but without the messages in this case. We want to serve this and the CSS it requires and the JavaScript it requires before we even try a network request, before we even think about going to the network. Because even if you have a great network connection, serving this locally is going to be faster. And that's the offline first approach. So we need to cash this ahead of time. And the service worker has an event for doing this. It's the install event. This is fired when the browser sees this version of the service worker for the first time. And it's your opportunity to go and fetch everything you need to work and store it in a cache. So here I'm opening a cache called static V1. And then there I'm adding the HTML for the shell, the CSS, and the JavaScript that it needs. But these won't be used by default. Service worker doesn't do anything for you. It kind of, it waits to be told what to do. So over in the fetch event, I'm going to start by parsing the URL so we can look at the component parts of it. And then if the request is to the same origin, and it's just the home page, we're going to respond with the app shell from the cache. So calling respond with here is my indication saying, like, I'm going to handle this request, I'm going to take over here. And we pass in either a response object or a promise that resolves to a response. Caches.match that returns a promise which resolves to a response from the cache. So these can pose it together really well. Otherwise, we're going to try and respond with cached content that matches the current request. So that's how we're going to pick up the CSS and the JavaScript when the shell page requests those. If there's no match, it's going to resolve with undefined. So as a fallback, if response is falsely, we'll make a network request. So this is the offline first approach. We're treating the cache as our priority, as our primary source, and then falling back to the network, whatever connection the user has. So using this pattern, you know, the page goes to the service worker, and the service worker just pulls the shell from the cache and the CSS and the JavaScript it needs. So that gives us our shell render without going to the network. And now we're running JavaScript on the page so we can display even more content. So with a Mojoyer, in this case, I actually went to index DB and pulled out the past messages, the last messages the user saw, and rendered those on the page as well. So the user's looking at messages without having to go to the network. And then we can go to the network for newer messages if we can, and we can show updated content and additional content. So if this network request fails, it doesn't matter because we're displaying content. That's a good offline experience. If the network is slow, then that's maybe okay, because the user might just be looking at past messages and we've given them that. So to see the benefits of this, let's pit the original online only site against this new shiny offline first version. We must go to the comparinator. There was going to be sound there. Never mind. So first up, the online experience. Go. So we get instant full content, whereas the original lags behind having to fetch stuff from the network. What about the offline experience? Instead of complete failure, we have instant full content. What about li-fi? Instant full content rather than the white screen of death. In fact, with our progressive web app, the experience is the same no matter what connection you have. And that's the offline first goal. The network only matters when it comes to fetching new content. And this is how we compete with native. And compete with native, we can. I decided to pit the emoji progressive web app against a native app. Google photos in this case, because it's a well-built, well- refined native app. I wanted to launch them at the same time and see what happened. So press them at the same time. And it's really close. This is native versus web. In fact, the emoji is about 0.2 seconds slower to show content. That's 200 milliseconds, not a lot. And that's compared to a well-built native app. But that's starting from cold. The browser isn't in memory. If the user who's used the browser some time recently, and let's face it, the browser is a pretty popular app, so it's likely to be in memory, this happens. This is a progressive web app beating a native app to content render by almost half a second. Now, a few people said to me, it's like, well, what about Android instant apps? What about those? Well, my answer to that is progressive web apps are shipable today. And they're not Android only. You can build one app across thousands of devices, operating systems and browsers, and it can beat a native app to content render. So ServiceWorker is in the stable versions of Chrome, Firefox, Opera, Samsung browser as well. And it's coming to Microsoft Edge. It's a high-priority implementation for them. It's under consideration by Apple. But progressive enhancement means you can use it today. You just don't get the offline features in Safari. But if you use ServiceWorker and suddenly your stuff has more features or becomes faster in Chrome and Firefox than it does in Safari, then that will give Apple more reason to implement ServiceWorker. As web developers, you are in control of the future of the web here. But not enough about the things that I supported today. I want to talk about the future, this is what I wanted to get onto. The idea was to build something like Wikipedia, but Wikipedia that ran offline first. So it's something that was very much less of an app, because the emoji is very much built like a native app, it looks like a native app. But is this stuff applicable to sites? Is it something that you will be able to use for your blog? And we thought Wikipedia is a really good demo of that. So the idea was, like, when you're online, you're going to get a server render, because that's how Wikipedia works. But with a ServiceWorker that we can start doing different things. So the first thing I did when I built the Wikipedia demo is to create an app shell as well. That's right. So I built an app shell for the article pages that would load this generic shell without the network, and then the page's JavaScript will handle affection is the appropriate thing from the network. So without ServiceWorker, it was kind of just a standard website. But with ServiceWorker, I'd rearchitected it to be more like a single page app. So I was dead excited about what I built. So I ran straight to the comparinator to see how everything changed. So I'm going to set the normal website here against my shiny ServiceWorker version. And I'm going to load it over a frottled internet connection, or as most of the world call it, their internet connection. Three, two, one, go. And I watched this, and I thought, shit. It got slower, like way slower. Here it is again. First render is actually quite fast. But the content, which some people would say is the important bit, is over two seconds slower on 3G. I had a mild panic, not as much as the panic I just had two minutes ago, but a mild panic that we'd really messed up the ServiceWorker thing. But it wasn't ServiceWorker that was the problem. See, I was using this app shell and letting JavaScript do the rest. Which is fine if your design already sort of dictates that you do something like this, but it wasn't the case here. I'd rearchitected a site into a single page app. And single page apps are incredibly slow, and this is why you'll see me moan about frameworks all the time on Twitter. Here's what a website does to get to first render. HTML starts downloading, CSS downloads, and now we start rendering content, and we render more as more HTML downloads. We might download JavaScript as well, but it should be async, so it's not going to block render. Contrast this to single page apps, web apps, where HTML downloads, usually instantly, because it's normally really small, CSS downloads, and then we get our first render, assuming the JavaScript isn't render blocking, which it often is, but let's be kind. Anyway, this is just a basic UI render, no content. And then JavaScript downloads, it pauses and executes, and then it goes off to fetch the content. And once it has all of the content, it adds it to the page and we get to render, and this is how slow happens. And in my case, I wasn't losing too much time on the CSS, the HTML, and the JavaScript download because they were all cached in the service worker, but the problem here is at the bottom, the JavaScript has to download all the content before showing any of it. The more I think about load time performance, the more I realize it's not about the size of your CSS, the size of your JavaScript, it all comes down to being progressive. And this is related to progressive enhancement, but not quite the same. I mean, show them what you've got. Like, all the time you're loading, the user is watching and waiting. Don't make them wait until you have everything before showing them anything. Prioritize the first render, the first interaction. Show the user what you've got as you get it. And that's where I was failing. I was hoarding all the content and only showing it once I had all of it. In retrospect, it seems kind of stupid to cram a site into this single page app model. But for me, this was my app cache hangover. You see, app cache nearly, nearly lets you use the page shell pattern, but not quite. But it lets you so close, like you can almost smell it, but it just sort of bats you away at the very last moment. So once app cache was out of the way, I was like, really? App cache is gone. Does that mean I can have all of the page shells I want and nothing is going to stop me? But I didn't stop to think that the best answer wasn't the one that app cache nearly let me have. It was the thing that it didn't let me anywhere near. What I wanted was streaming. And with streaming, you get to finish the simulation here, and you can see the final render is somewhat quicker than the non-streaming version. But the important bit is that render happens ages beforehand in the non-streaming model. So this is the full HTML spec loading here over a frottled internet connection. It's a 3 megabyte document. It's still downloading, but it gets on screen after only 20K is fetched, which is great. That's streaming. But for a long time we had no access to streams and JavaScript, but that is all starting to change. So we designed the fetch API alongside service worker, mostly because we needed a lower-level representation of requests and responses. But it was also a good opportunity to kind of look at XHR and make something that's a bit easier to use, because XHR is almost 18 years old. I don't want to be using XHR when it's legal to drink. It's bad enough when it's sober. So this is fetch. It returns a promise for a response. There it is. But that only resolves when we've received headers. We haven't received the body yet. You have to choose how you want to read it, and that gives you a promise as well, and then you get your data adjacent in this case. This compresses really nicely using arrow functions and even better using async functions, so that the keyword await there, it pauses execution of the function until the promise resolves. And it's pausing in async way, so it's not blocking the thread, it's not blocking interaction. I think Edge has this in their preview builds. I think we've got stuff in Canary as well, but Babel lets you use it in other browsers as well. And it produces much easier to read code. I think it's great. So if you haven't encountered it before, you can pretend that async code is sync. It makes it a lot easier to read. Anyway, you don't have to read the responses, Jason. We added a load of different methods for common types that you'd want to read. We added these for two reasons. They're nice convenience methods, but we wanted to ship fetch before streams were ready, so we needed a way to read the body. But we did reserve response stop body for streams when they landed. And they landed in Chrome and Opera around a year ago, and they've been actively developed by Firefox, Edge, and even Safari as well, they're working on them. Here's how you use them. So you fetch something, say the HTML spec, and we're going to fetch it as a stream, and we called GetReader, so I'm saying I want to open the stream, I want to get a lock on the stream. And then we can read some of the data, read returns a promise, result is an object, result.done is true when there's nothing left in the stream. So if result.done isn't true, result.value is a chunk of the data, and that's a Uint8 array of bytes that you've received. It's not the whole response, it's just the first part of it. When we want the next part, we can call read again. So if we wanted to get the length of a whole response, we could do this. We're going to have a variable there to keep a running total, and then we're going to create a loop. I'm just going to keep calling read until result.done is false. And in there, get the value, add the length to the total, and then log it out. And that's it done. We should be able to see that working. I don't know. It feels really risky doing life demos after what happened before, but let's give it a go. Let's see. Here we go. So this is, let's see if I can make this bit bigger. Oh, wrong window. Maybe I can't make it bigger. Where's my mouse pointer? There we go. So if we refresh the page here, we can see the sort of log at the bottom there, and it's reading the HTML stuff. And we can see that the, we're inspecting parts of the response here, but we're not having to keep it all in memory. You can see the chunks are of different sizes, so that's something you have to deal with. They're kind of around about 32K, but not always. So here we're reading the full size. It's eight megabytes, I think, the uncompressed size right down at the bottom there, but we don't have to have the whole thing in memory. We're just sort of reading bits and then there can be garbage collected straight away. So a more practical use of something like this would be to search a stream for a given string. So say we wanted to search the HTML spec for the word horse. Here's how we do it. So this is fairly a simple way of doing it. We're kind of fetching it, reading it as all this text and then searching. But this means we have to load that full eight megabyte document into memory and then search across it. We can do better with streams. Instead of fetching all the text, I'm going to loop over the stream as before, but the problem here is that result.value is in the array of bytes, whereas we need a string to do the matching. In future, this will be really easy. You'll just be able to pipe the stream through like a text decoder and that will kind of read in the bytes, convert it to text and pass it out at the other end. But transformed streams haven't been fully specced yet. They're all coming soon, but at the moment you can do things a little bit manually with text decoder. So here we fetch, get a reader, but also creates a text decoder. And then we want to loop through the stream as we did before. But on result.value, I'm going to call decoder.decode and that will take the byte and give us a string back. It decodes it assuming UTF-8. There are other options as well. The stream through there, that's actually really important. That changes how things work. So let's show you that. So here's a simple demo. So this is the code you saw before. I've got three chunks of text there, and for each one I'm going to decode each part. And if I run that, everything is broken. But if I add that stream option in and run things again, right, we get the poo emoji, the toilet emoji in an exclamation mark. So the reason this happens is that emoji are four bytes long in UTF-8. So with stream true, it tells it that it might be receiving more bytes later as part of the same message. So with stream true, it receives the first three bytes and it goes, this is part of the character. This is not the whole thing. So it just returns an empty string and it holds those three bytes. The next time we call it, it adds the next three bytes on. So it adds the 169, the first byte, and it goes, ah, right, I've got enough now to show the poo emoji. So I'm going to pass that back and I'm going to hold on to the last two bytes. And then we add the next chunk on, the next three bytes, and it goes, ah, right, I've now got enough for the toilet emoji, send that back, and also 33 at the end there is the character code for an exclamation mark. So you can send that back as well. So now we can check these chunks as they come along. Now we've got them as text to see if it contains the word horse. And not only that, we can also cancel the download when we find it. And this is really efficient. So it's an eight megabyte document, but if we find a match in the first 20k, that's great. We can stop the download and save all that bandwidth. But there is actually a bug with this implementation. It works fine if our chunks are like, my lovely, and then horse running through the field, oh, there's the match in the second chunk, great. But what if the chunks are my lovely huh, no match, horse running through the field, no match? And we miss it because the match was across two chunk boundaries. So when you're doing things like searching or filtering streams, you need to defend against this. And the way we do it is by keeping a buffer that is the size of the thing we're looking for, minus one. So in our case, horse, five characters, we need a buffer of four. So that means that we could see my lovely h, fine. And then we keep those last four characters and add them onto the next chunk and then we see horse. So the code for that, my slides have gone off again. Isn't that great? Whatever you did, oh, it's back, excellent. So the code for that, same as before, but we create a buffer. My heart rate is going so fast. So we're creating a buffer. We're going to loop through the stream. We're going to decode it, add it onto the buffer. And if it includes horse, great. And then we cancel. Otherwise, we reduce the buffer down to those last four characters. So now we're searching large documents, we're keeping the memory usage down and we can stop the download early. So I'll show you an example of that as well. So we're actually going to run this on the HTML spec. It's a slightly longer piece of code because I'm going to keep a larger buffer so I can show the search term. So with horse, which I didn't expect to be in the HTML spec, but if we run it through, there's a match. And it's because Tommy Fawson is one of the contributors and he's got a horse in his name. Let's run a different example. Someone could be an example of something that probably won't be in the spec but might. Who? Do you know what that means? You are so much more polite than the meet-up in London that I gave this same talk to very recently. Because when I asked for a suggestion, there was actually a lot of silence for and then someone at the back of the audience just sort of stood up and pointed at me and went shit. Okay, I'm going to pretend that's not a heckle. And I'm going to do that sort of nervously. Okay, this will be fine. Obviously, you're not expecting anything to happen. But as it searches, it's like, whoa, does it match? Oh, it's because it's a canvas shit region. A canvas hit region. Okay. Where are my slides? So, oh, yeah, I was talking to some developers in India and where connectivity is quite poor. And they were sending this blob of JSON down for some search results. And they wanted to detect 2G versus 3G. So the idea was like, oh, if they're on 2G, we're going to send fewer results because it will get on screen faster. You'll get a faster render. But detecting connectivity is really unreliable, especially in India, where you can have 3G or 4G packaged, but it will be the speed of 2G. So it's much better to do something else, like serve something like this, where it's not JSON, but every line is a JSON object. And this is something you can stream then. You can parse each line separately as JSON and deal with the data as it arrives. And that means you can deal with the first result before dealing with the rest. And there's a real, there's a huge performance benefit to this. So here I've got an example of that. I'm going to throttle the network down to 2G. There we go. So a regular fetch of this JSON will take, well, it depends on the connectivity, but it will take around about, okay, so it's all about five seconds. And with regular JSON, it has to download the whole thing before it gives you any of it. So the first bit of JSON arrives after five seconds, and so does the second. But with a streaming solution, getting the last bit of data is going to take roughly the same amount of time. Yeah, so sort of five seconds. But we get that first bit after 364 milliseconds. So there's a huge benefit here for slower connections. We can start showing results before we've received the entire file. So you might be wondering how I can use this to speed up my Wikipedia demo. Well, I can't, because although you can use fetch to read a response in little chunks, JavaScript has no access to the streaming HTML parser. It might look like here I'm sort of happily adding HTML into an element, but plus equals is the same as getting then setting. So you're asking the browser to serialize the DOM and then you're asking it to parse some more DOM, like you've added some more to the end. This is a performance disaster. Don't do this, because the elements at the start of your string get created like hundreds of times. I'm hoping we can get like a streaming HTML parser a document fragment or something in future, but it's not there yet. But in Chrome beta and Chrome canary, we can create our own readable streams now. And this is something that's also being developed in other browsers too. It looks like this. You just pass in an object with these methods. And so say we wanted to create a stream of random numbers. We're going to store an interval. I'll show you why in a moment. I'm going to create a stream and then we're going to pass in a start method. This is called straight away. So in here, I'm going to set up an interval that pushes a random number to the stream every second. The controller has other methods as well, such as like close to signal the end of the stream, but we're not using that here. I'm also going to add a cancel method. This is when the user cancels the stream after they're done reading it. You get this cancel method so I can use this to abort the interval. So this is just like fetch streams now. I'll show you an example of that running. I think it's this one. So yeah, here we go. So this is just the code you saw before, but I'm going to read the value three times and then cancel and then try and read it again. But what we'll see if the page loads should be great. You can see this log down at the bottom here and it's getting those numbers. It gets three of them, but then it's had enough of it. So this is a push based stream because we're pushing data into it. So if no one's reading from the stream, those numbers are going to build up and up in memory and eventually you'll get memory problems. The opposite of this is a pull stream. The pull method is called when a reader is waiting on a value and we don't have anything left in the buffer. So when pull is called, I'm going to return a promise. That waits for a second and then pushes the random number on. And this is much better because you're not generating random numbers if nothing's waiting for them. Your streams can be of anything. With fetch, we saw there are a UNS8 array of bytes here. It's numbers. It could also be objects. It's one interface for all of those things. The designers of web streams, they talked a lot to the node folks because they have had like four, three or four versions of streams and they've made mistakes along the way. So there's a really good source of like what did you do wrong so we can avoid making the same mistakes. So you might be wondering how I can use this to speed up my Wikipedia demo. Well, I can't because not just using streams on their own anyway, because things get much more interesting when they're combined with a service worker. So this already streams. Fine. It just does it automatically. The browser connects the dots. Same with this. The browser will stream it from disk, from the cache. But in Chrome beta and in Chrome Canary, you can create responses from streams. You can create your own streams and stream those into the browser. So in my fetch event, I'm going to create an encoder. So this is the opposite of before. It takes a string and turns it into bytes because response streams must be bytes. We saw that before. Then I'm going to create a stream with a pull method which waits a second and then pushes a paragraph onto the page. So you can see that I've got encoded or encoded to turn it into bytes. And then I create a response, give it the stream, and then say this is HTML, give it the HTML header. So if we run that, it should be here, we can see random numbers appearing on a one-second interval. But as far as the browser is concerned, it is just receiving text very slowly. And you can see that the spinner at the top there, as far as the browser is where it is just loading a page very slowly. So you might be wondering how I can use this to speed up my Wikipedia demo. Well, I can. I actually can this time. Before we were piping content straight into the browser streaming HTML parser, and that is the key to it all here. This is what AppCache wouldn't let us anywhere near. So instead of turning a perfectly capable server-rendered site into a JavaScript-driven single-page web app, I can use ServiceWorker more like my server. So here's what I did for Wiki offline. I fetch the article header, the body, and the footer. So the header and the footer are coming from the cache. The body is coming from the network, but it will fall back to the something in the cache if it fails. And then I combine those streams together into one single stream. And combine as a function I wrote, it's about ten lines. It's kind of a manual process right now. It's like creating a single stream that reads the first stream into it, then the second, then the third, and so on, until it's run out and then it ends. And then we create a new response using that stream. So one response made of multiple different parts and places, just like what you do on the server. You get some stuff from a database, some stuff from a template, et cetera, et cetera. Treating your service worker to a server maps really well to content sites like blogs and Wikipedia which are server driven. You see the result of this for one final time, we go back to the comparinator. Oh, there's sound this time. Great. You'd normally be bored of that sound by now, but that was the first time. So here's how it compares. So on the left, the app shell render. And on the right, the streaming service worker, both over 3G, so launch them at the same time. So the difference is absolutely massive, but the app shell was a performance regression anyway, so let's compare it to the original server render. So we get the benefit of that quick cached first render, which happens without the network. But that allows us to get network stuff on screen quicker because there's less to fetch. We've already sent the header and stuff down. I'll play it again. So the first render is almost like a second faster, but content render is half a second faster, even though it's just coming from the network, the same network in each example. So needless to say, I'm quite excited about streams, but that doesn't mean the app shell model is bad. You saw it being used with Emojoy earlier and the result was a progressive web app which launched faster than the native app. But a streaming solution will be faster and easier for you if you use server rendering, say, like switching to an app shell model here could be a lot of work and could land you with a performance regression. If you're already a single page web app, you have that performance regression already, so knock yourselves out. Carry on doing that. Consider streams if your initial content may come from the network. The benefit of streams is being able to build content from multiple sources. If your initial content comes from the cache or database entirely, then you don't have so much to gain from streams. So that was the case with Emojoy. I was getting all the first render content from caches and databases, so streaming wasn't a whole lot of use there. Consider streams if a partial content render is filable to you. And that, I think, is in most cases, but especially written content. So if you're looking to build something like an offline first, news sites like the Guardian or a shop like Amazon or your blog, streams could be the faster and easier way to do it. And they're landing in Chrome in the next few weeks and they're being actively developed in Firefox and Edge. If you want to know more, I've written an article about streams, a gushing praise on my blog. In this post, I do some sillier stuff as well, like kind of transcode and an MPEG to a GIF on the fly or transform content. I actually did this with the I ran a transform stream on the Wikipedia page for cloud computing. This is it. But every time it sees the word cloud, it's replacing it with but, which is great to read. Where is that? I've got a favorite bit in here. See if I can find it. There it is. Oracle announced the Oracle But. While aspects of the Oracle But are still in development, this but offering is poised. I love the term but offering. That's great. So service worker is this is Bruce Lawson's logo for service worker. I like it. It kind of makes me feel slightly drunk. It's great. You can probably fill new network features with it. You can become a faster network resilient, more network resilient. We've gone through a lot of stuff here at Lightning Speed and that monitor has gone off and I've got one slide left. So let's rush. There's also a free Udacity course where you can take a website from online only to offline first to use it over a series of examples. Covers the App Shell model in index DB. And with that before, everything just sets on fire and breaks. Thank you very much. Cheers.