Sessions is temporarily moving to YouTube, check out all our new videos here.

Building the Progressive Web App for HackerNews.io in Ember

Ivan Vanderbyl speaking at EmberFest in October, 2017
188Views
 
Great talks, fired to your inbox 👌
No junk, no spam, just great talks. Unsubscribe any time.

About this talk

If you're an avid reader of HackerNews you may have seen the HNPWA, an attempt to implement a HackerNews client in every major JavaScript framework. There's only one problem: they forgot Ember. So Ivan set out to change this. As it turns out, Ember requires a lot of optimisation to reach speed even close to React on mobile, and in this talk, he'll show us how we did it.


Transcript


G'day, good afternoon. My name's Ivan Vanderbyl, I'm the engineering lead on a team called Flood. We're a distributor team that build the performance testing tools that allow you to measure the performance of your application under extremely large load. But, that's not what I'm going to be talking to you about today. Instead we're going to go on a journey, to building a progressive web application. So we're going to cover the fundamental aspects of building a progressive web application, which will be a simple HackerNews reader. And we'll pay specific attention to optimising the performance and the first experience on the device, the first time an application is loaded. And I'll share some of the tips and tricks, that we've picked up along the way. So, this is a photo I took in Greece earlier this year. My girlfriend and I were working remotely from the island of Mykonos. And we got there early May so it was kinda before the tourist season, we had almost the whole island to ourselves, there was just nobody else there. But it was still lovely and warm and nothing says productivity like sitting by the pool with your laptop, a cold beer and a Souvlaki. So, on a Thursday afternoon I happened to be reading HackerNews as I usually am. And I came across this post, about HackerNews readers as progressive web applications. And it was put together by Addy Osmani who was the original creator of TodoMVC and I think he's also an evangelist on the Google Chrome development team. And I was looking through and thinking "Oh Great. There's a reactive implantation, pre active implantation, angular implantation, probably another geometrically named framework implementation. But I got to the end and there was no Ember implementation. And I knew this was a challenge that I had to solve. So, what is the HackerNews PWA? Whoa. So the requirements are that the application displays all the pages that you would expect to see on HackerNews, that's the top stories, new stories, show HackerNews, ask HackerNews, jobs page, and all the threaded comments. The cool thing is though, these requirements map very nicely to the Progressive Web Application requirements. So, like I said ... I'm skipping ahead. Each page is to obviously have it's own URL. We need, like I said, 30 items per page. One thing is we need to be able to load in under five seconds as a Progressive Web Application and we should use the Application Shell pattern. So, yeah. And the application should be responsive on desktop and mobile. And lastly it should aim to work cross browser, which quite frankly, I don't really care about because if you're not using an evergreen browser these days and you're reading HackerNews, this is kind of your own problem. If you're reading HackerNews on iA6 I kind of question your life choices. So, what is a Progressive Web Application. Here is some fancy Google Marketing copy I stole from the developers.google website. So, Progressive Web Application are reliable. They load instantly on unreliable network connections and never show the Downasaur. Their fast, they respond quickly to user interaction with silky smooth animations and no janky scrolling. And they're engaging. They feel like a native application on the device, with an immersive user experience. So this is the Downasaur. Next time your Internet's out and your waiting for it to come back, you can press space and play this dinosaur game. I don't think this is what Google had in mind when they were talking about engaging user experiences. So some of the checklists for what is a Progressive Web Application. First requirement is that its served over HTTPS. This a requirement in order to enable service workers, for obvious security reasons. Pages are responsive on desktop and mobile devices. All app URLs work while offline. We provide a bit of metadata so that the device knows how to instal applications, you know, which icons to use, what the description is and what the title is on our home screen. It should be fast, even on a 3G connection. It obviously works cross-browser. Page transitions don't feel like they block on the network. And obviously, were in with developers so we get out of the box and every page has its own URL. So let's build the Progressive Ember App. So I thought, it's Thursday afternoon. Challenge accepted, and I set to work. Generated the new Ember App, this was Ember 213 at the time. And set out to build what would become this application. So, I reorganised the Progressive Web App checklist, to kind of align with a couple of technologies that we're going to be using. So, first of all, in order to get our offline functionality, and the metadata provided on the home screen, we just have to instal two add-ons. Which is ember-cli-service-worker and the additional plug-ins like ember-cli-service-worker-asset-cache or index-cache. And then an aptly named ember-web-app add-on, which basically makes our application register as a web app on the device. So if you're using an Android device, you'll be able to instal it on the home screen and you can do it through iOS, but you've got to do it through Safari and it's a bit clunky still. Hopefully, Apple fixes this in the future. Secondly, in order to get our cross-browser functionality support and responsiveness, we pretty much just rely on postCSS to organise our CSS for us and handle all the little browser tweaks, or quirks, I should say and browser targets in ember-cli to give us the necessary packs to support the browser we're after, which I guess, should be IE11 or something, but I just made the last edition of all the major browsers, so sorry IE. And lastly, each page has a URL and doesn't block on the network. Well, we get this out of the box with Ember Router, so there's not much to talk about there. So a little bit about the application shell. This is everything except for the content in your application. So, that's JavaScript, CSS, images, logos, icons, fonts. Everything that we can store, you know, in a service-worker-cache, aside from the content of the application. So, the idea is that after you've loaded the application once, all these assets get stored and every time you come back to the application it's instant, because it just has to pull like the latest new stories off the HackerNews API. You can also cache everything, you have a full-back-cache for the API itself, but I won't go into that just yet. So, we needed a data source and I went looking around thinking originally that I might have to build some sort of scraping interface for HackerNews because I wasn't aware there was an API, but it turns out going to GitHub updated three years ago there is a HackerNews API provided by Firebase. And I knew that there was a Firebase Ember add-on. And I've never used it before and it sounded like a fun thing to try out, so looking at the data source that we got back I realised that the HackerNews feed is essentially a link and and a comments and all the child comments and they're all represented as an item, which just basically has a type attached to it, which maps quite nicely to an Ember data model with those tributes, so, you know, who it was by, the child elements as kids, the parent element if it has a parent, obviously the time it was posted, the text and the type either to the link or comment. And setting up our routes, pretty straight-forward. I would create a route to each page and additionally one last route for the item content itself. So here's a big blob of code, which is pretty ugly. Don't look to closely at it. Basically the annoying thing with Firebase is that it just returns all the IDs. It doesn't have the ability to return the actual item content. So we first request all the IDs and then basically await 20 promises coming back with all the content. Which is a bit slow, but it works. And I didn't wanna put too much effort into that one just yet. So, the other details, which I won't go into cause we're kind of short on time with this talk, there's a lot of other things I wanna cover. We put in service-workers like I said. We use Firebase and Ember Data to load the data. We're using moment.js to format timestamps. A couple other useful add-ons in there, and lastly the Ember App Manifest comes from ember-web-app. This was the first implementation I put together in an afternoon. It's useful, you know. You've got your top couple posts there. It's readable. Each page works. Definitely needs a bit of work. But, I was feeling pretty good about it. So we need to talk a bit about the performance though, because, you remember one of the requirements was that on a slow 3G device, which they classify as a Motorola G4 on a 3G connection, which is 1.6 megabits. It has to load and be interactive in under five seconds. And there's really two times we care about here with the performance. There's the first time you load the app and then there's every other time that you load the app. And the first time you load the app, we call, that's basically the Time To Interactive. So, that's defined as the point at which the main thread is basically idle enough that the browser can process user interactions. So, React did it in 4.1 seconds. What do you think Ember did it in? And keep in mind that we get a lot of functionality out of the box of free. We get service-workers, just instal an add-on. We get basically two add-ons and you've got a Progressive Web Application. So, you'd expect that we should get performance out of the box as well. So first cut ... Like Ember, what where you doing that whole time? What's going on? And so, as far as Progressive Web App goes, remember that's one of the requirements, it has to be a Progressive Web Application and we measure this using a tool called Lighthouse. Lighthouse gives us these measurements. The most important one we care about is how much of a Progressive Web App it is, which we're not even half way there, so it's not even going to be called a Progressive Web App yet. And that's because of a lot of other reasons that come down to the deployment and the performance. By the end of this talk I'm going to get that to 100 and some respectable marks on performance and so forth, but that's a very hard metric to increase unless you get your app down to under one second. I'm also going to get the load time parallel with React at 4.1 seconds. And here's kind of the timeline of how it went. You can see initially first deploy 12 and a half seconds. Shaved off a few low hanging fruit and things start to taper off very, very quickly, but then we hit this point where it's more magic than science for a little while and eventually hit a point where it kind of starts to make sense and we could identify that certain changes are actually having an effect on the load time. And, look, initially, I was just stabbing in the dark though. Oh, that little tribute, I wonder what it does. You know, see what happens. Deploy it, test it, see what happens. All these benchmarks, we're using Webpagetest.com, basically in the same configuration which is just using the easy test. Motorola G3, fast 3G connection, I think. So, I hope no body here is too intimidated by network graphs. First sight they can be pretty hard to interpret. This is everything that basic took place on the wire. This is a really important thing to look at to get an idea of what's going on. So, you'll see we're loading 32 resources. And at the very top here, which is kind of the critical part to our application running, we're spending like five seconds for each of those resources, just to get them on the device, so there was definitely some optimization we could do there. You'll also notice that down in the bottom right here, this section which is highlighted in red, that's basically pegging the CPU. And there's no way the device can process any easier at that point. But after that point is where we want to get to. So, we need to move that as far to the left as possible. So the first thing to do is figure out why everything that was sending over the wire was taking so long. And I came across this add-on that Stephen Penner wrote called Block linking cat analyzer, which basically gives you this kind of block graph thing of everything that's in your vendor and app JS files. So initially we can see that by adding Firebase and Ember data we added like 130 kilobytes that probably, you know. That's expensive for just making a couple of API calls. Also jQuery, we're not using that anywhere. So that's kind of 30 kilobytes we don't need. All these numbers are the are the G zipped size of the wire so the actual amount is probably like three times the size. I know that the vendor.js at this point was 1.1 megabyte uncompressed. Which is huge, I mean that's basically a freshly generated ember app with a couple of add-ons installed. You also see that in this area like here all these little boxes are all little helpers and everything included from a couple of other add-ons like the moment.js and ember composable helpers which all just end up in the app tree even if you're not calling them. And like I said this was Ember 213 and even today we don't quite have you know, a way of shaking this stuff out. So what we need to do is figure out a way to slim this down a little bit. But first we need to understand why. So there's really two things going on here, there's in terms of the performance, there's what's on the wire and there's what's on the browser. And when I say on the wire, I don't mean the epic TV show shot in Baltimore, starring agent McNulty, I mean everything on the layer fore between your server and, sorry my server and your browser. So once we get to the browser then we can care about the pass time and execution time of the actual code which on a slow device actually adds up pretty quickly. So I said about removing all of these low hanging fruit and I thought removing jQuery is honestly something that if you're not targeting old browsers kind of may as well just do because it's a bit of a waste of time having in there and it does have a certain cost, especially on a mobile device. This is my five to six step guide on removing jQuery, I'll post these slides up afterwards if you wanna copy them. But basically it was a similar story, once I removed jQuery it was a process of removing Ember data, removing Firebase but the one thing about removing jQuery is you no longer have Ember Ajax so we need an alternative and the great thing about this was it meant that I had to search around for a new data source and there is a third party node HackerNews unofficial API running on Heroku which is actually really cool because it gives us only one request for each page containing every item. So it's also cached so I put it behind a CDN and it absolutely flies, give us the first 30 articles. But we no longer have Ember Ajax so we have to look at something different which is Ember Fetch. And Ember Fetch greatly simplifies loading this data in so a couple of things I did here, because I realised that loading each page is simply just appending in from page to the URL so we just have the API hosters as a property on our model, I'm sorry on our route and the page itself as a property on the route. And I concatenate the two together and make a fetch call and that's the whole model hook, just returns a bunch of pojos. So for all the other pages all I have to do is extend the index route and specify what pages it needs to load and they all use the same template from index and away it goes. There's very little code duplication. And if you wanna see how that is implemented, you can find the repository on my GitHub. So these little changes had a huge impact. So we get the initial load time down to 6.8 seconds but there's a couple of other funky things going on, you'll notice that it starts loading the API pretty late in the game. And so I'd kind of like to get all the little stages there out of the way because it's kind of just wasting time really. It's slowing everything down for no real reason. So first you need to understand a couple of fundamental truths about browsers we have a single thread for our JavaScript execution, on HTTP 1 we can only open about six to eight connections to each domain. Depending on the browser. If you're using HTTP 2 it's apparently unlimited and in practise it seems to be fairly parallel. Resources get assigned a priority which is how important they are to load against everything else that has to happen in the browser. So that actually is defined more or less by type but there's a couple of tweaks that we can do to kind of optimise that. And also there's a thing called the Script Streamer Thread which we can offload some of our parsing work to which will happen in parallel to loading. Should shave off the parsing time because on a mobile device parsing time can actually get pretty expensive. Parsing a standard vendor.js bundle of one megabyte on a mobile device takes about 150 milliseconds. On the test device that we're using. So that 150 milliseconds is a lot I mean on desktop you wouldn't even notice it'd be you know a couple of milliseconds. So this is a normal network request, assuming that we're making a request to a new domain that we haven't seen before so we've got to do a DNS look up we've got to do a TCB handshake which is the connect, we've gotta do a TSL TLS negotiation if we're using HTTPS and then then we download the content, then the browser parses it. And then it compiles and then it runs. So those are a lot of steps and it'd be great if we could somehow do a couple of those things ahead of time so that the browser doesn't have to do them when we actually ask for them. And luckily using async or defer attributes in Chrome since about a year or two ago, it will automatically move the parsing step in line onto another thread. And we can see this in practise in a moment but I should mention, there's just one problem with this, we can't async both vendor and app js because they will as the name suggests become asynchronous and be loaded in any uncontrollable order. So you could end up with a situation where app.js which is more likely to happen because it's usually smaller will load before vendor.js and then it won't be able to find Ember and then your application won't load. So you could async just app.js but there wouldn't be so much benefit. To solve this we can concatenate the tow together using ember-cli-concat which has the additional benefit that it will shave off the extra network requests for app.js which on our test device is about 500 milliseconds. So that was a pretty low hanging piece of fruit to fix and if you might be thinking well and then we lose the benefits of having vendor.js cached because it's gonna get expired every time we deploy and that's true except that if you're building a progressive web app, you're already caching most of this stuff in a service work afterwards so it's not really a problem. So once we have that we can actually use async and one thing to be aware of is that async will decrease the load priority order of that script. So usually the browser comes across the first piece of JavaScript that says this must be important I'm going to block everything until this finishes loading and then I'll continue. However when you use async the browser says oh this is super low priority, I can load all this other stuff first. And if there's a lot of stuff you gotta load ahead of that, there's basically no benefit to be gained but if there's nothing that gets loaded ahead of that then there's a lot to be gained. And here is it in action. So we'll see that at the top here, we've got our concatenated bundle, hn.js and before that finished loading, we start parsing it on another thread which would usually happen after it finishes loading. So again here is our normal request with the usual network components in there. And we can do a few other little tricks here to speed it up because wouldn't it be cool if for when we get around to requesting the API which happens to be on a different domain name, then we don't have to do a DNS look up. Because each domain name in the browser has to do a DNS look up the first request it makes. So we can use rel=dns-prefetch link in the header and the browser will establish a connection to this. Give it fairly low priority but it will usually happen before you ask for the resource. And that's pretty cool, like DNS might say be 30 milliseconds or so. But wouldn't it be great if we could just do the entire connection ahead of time so that when we ask for the data, it just has to pull it over the wire. And we can actually as the browser to basically preconnect to the server. Simply by saying rel=preconnect this link. And most modern browsers support this but as of two weeks ago, Firefox and Chrome's had this for a while but you know now that Firefox supports it, we can use a thing called rel=preload which will just load the entire resource ahead of time when we ask for it. It's worth pointing out, there's also a thing called rel=prefetch which isn't supported by any browsers yet and the difference between the two is that rel=preload is given a very high priority and rel=prefetch is given a very low priority. And sometimes low priority might actually be what you need here. It's additionally worth pointing out that the connection times there, there's no guarantee of when they're going to happen so the browser may decide to do the DNS and then the TCP and the TLS or it might do them all together or all at the start and then download, you've got no say in that and it usually shouldn't matter but it's something to be aware of. So that's our network optimizations out of the way. And now it comes to getting stuff onto the page. And the first thing I hit was this thing called Blocking CSS. And this is basically that as soon as a browser comes across a style sheet, it says wait, this is gonna be critical to making this page look good, I'm gonna stop everything I'm doing, download this file, create a CSS object model, render and then continue what I was doing. So it basically blocks your page from loading for a good amount of time like half a second on a mobile device, maybe longer. So to get around this, I found this MPM package called Critical which I made an ember-cli add-on for which runs it so you can use this with ember-cli-critical and what it does is it spins off the build of your app on deployment in phanton.js, it extracts all the style sheets that we use to render everything above the fold and then pulls them out of your style sheet and basically puts them as an inline style tag within the index HTML which saves you having to do a whole request just to get that style sheet for the initial render. And then you've still got, you know, truncated style sheets of everything else. So that's, that actually saves quite a bit of time so the initial cost of that was like 450 milliseconds per style sheet and it's worth noting that none of my add-ons had style sheets so I just basically don't load the vendor CSS. So the last thing is serving everything to you customers, basically you should have a CDN. This rule I think should be pretty obvious, it's kind of like breathing air and drinking water, it's basically critical, if you're serving your app from one server in a hire somewhere, that's really really far from Berlin and that will be really really slow but if you're serving it off a CDN and you know they're all pretty reasonable options, the hackernews.io app uses S3 + CloudFront and that's been pretty good for the webpage test scores. I don't know if it was the best option, I don't know if there is a best option, I think the answer to that is well it depends, your mileage may vary basically. But a CDN is definitely the way to go. So after all of those tweaks, we get our, the point at which our main thread is idle, here, is like 4.1 seconds. And you'll notice we're loading considerably less resources. So the cool thing is we did get to our target of under five seconds. This was the final application you can view it on your phone or your laptop, hackernews.io One little feature I added was that when you're offline it goes grey. And then turns red again when you go back online. That's meant to be URL at the bottom of that page but apparently it's cropped off. But hackernews.io not in anyway affiliated with the real hackernews but it was a pretty fun weekend hack project. So quick recap, use ember service worker plus ember web app to get yourself a progressive web application. Concat you assets and async them to get them on page faster. Preconnect if you need or if possible. Inline your critical CSS which is gonna have a pretty big impact on the initial render time. And inline the service worker registration which is an option for ember-cli-serviceworker which will save the request for the initial JavaScript just to get the register script. Use a CDN and remember that you can't improve anything that you can't measure. So looking forward there was a blog post about some Glimmer improvements recently, Glimmer is starting to get stupidly fast. It's really really cool what's going on there. Huge props to the team that's working on Glimmer at the moment. Because basically a fresh Glimmer app will render in under five seconds without a fault which is a fantastic place to be in. One thing I didn't mention was fast booth. I did try deploying this with fast booth but it dramatically increases the time to first byte because it has to basically request the page and render it. So hopefully the future Glimmer service site rendering is sufficiently faster and the rehydration doesn't have an impact on the interactivity of the page when it comes up. Hopefully also future Ember automatically removes the unused modules so that you don't have to do that manually. And maybe Ember will consider dropping jQuery I know there was an RFC for that about two years ago but it never went anywhere. But it's pretty easy to fix. And one last thing to consider is spriting your assets because the service worker will cache everything that is in the assets folder. So those are all individual requests you'll have to make. So One last thing, there is a Glimmer implantation of this app. Here are the scores. It does reasonably well, I think it could do better. Could probably get it around to the two second mark with a few more optimizations. But still, on a mobile device like that, that's crazy fast, that's like desktop speed. So I've been Ivan Vanderbyl, you've been an awesome audience, thank you so much for coming along to my talk.