Architecting Mobile Web Apps (Google I/O'19)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
[MUSIC PLAYING] MICHAEL BLEIGH: Good morning, everyone. And welcome to Architecting Mobile Web Apps with Firebase. [APPLAUSE] Thank you. That's exciting. This is very early for that kind of applause, so I appreciate it. My name is Michael Bleigh. I'm an engineer on the Firebase team. And before we dive in, first, I would like you to think of a number between 1 and 10. All right? Picture it in your mind. Now, multiply that number by 2. You've got a new number. Multiply that new number by 5. Now divide that by your original number and now subtract 7. Through my magical, mentalist powers, I can tell you that the number you are now thinking of is 3. So now I either just blew your mind, or you have some basic idea of how arithmetic works. Either way, what I actually did is something else. I distracted you for about 45 seconds. And in that time, you could have downloaded 2 megabytes over a 2G connection. So that's about the size of the average website. Yikes. Because in the real world, we don't get 45 seconds of distracting patter when users are loading our websites. Instead, we get a tiny loading bar at the top of our browser and a glaring white expanse of blankness. So for my next trick, it'll be a little bit different. I'm going to make a web application appear. It'll be great. Let's switch over to the demo. So here you can see I have a blank white screen. I'm on about:blank. This is the web equivalent of nothing up my sleeve. And we're going to visit my web app, which is called TaskMagician. And, oh, I should note that I have my network throttled to 3G, so this is at least a simulated mobile environment here. So you can see, it loaded pretty quick. I've got a nice little landing page. Oh, I've got a little checklist application. That seems neat. Let's go ahead and try signing in. That pops up and asked me to choose a Google account using Firebase Auth. So I sign in with my Google account. And it drops me into my main application. And I have a few different task lists here, but the one that seems kind of relevant is speak at Google I/O. So let's see what's on the list. We've got perform goofy icebreaker magic. Done. We have sign into demo app. Also done. Check off this task. Done. Now the-- oh, go offline. That's rough. This a live demo. Going offline is never fun in that kind of environment. But you know what? It's Google I/O. I'm feeling good. Let's do it anyway. So we're going to go offline. And to give ourselves a little extra challenge, let's go offline and edit this task. So far so good. My application is still working even though my network is completely disconnected. You can see because I've made sure that my app tells you in big letters that you're offline. I'm also going to say, yeah, I've gone offline, and I've edited this task. But, of course, this isn't that tough because I've gone offline, but I'm still on my initial page load. The really rough thing is what happens if I try to refresh right now. Oh, scary. So I'm going to go ahead and do that. And look at that. Everything came back. Actually, almost everything came back. My user profile image did not load, which shows you that this is a live demo and not me completely fooling you. So not sure why that happened. But there's a little bit more here than meets the eye as well because I have open in another window the same application opened on a different account that is sharing the same speak at Google I/O task list. So you can see that this task has been checked off because I did it while I was still online. This one is still the same as I left it before I went offline. So, well, OK, I've shown that the online version hasn't been updated. But really to bring this magic trick home, we're going to need to bring this back online and hope that we see what we're hoping to see. So now I've brought this back online. And in the background, there's a little bit of magic happening because the Cloud Firestore SDK is reconnecting to the network and syncing the changes that I made offline back online. And so you can see that the changes that I made in my offline tab have now synchronized with the changes that I made in my online tab and both are working together. Whoo, yeah, magic. Let's go back to the slides. [APPLAUSE] So great web applications are magical. A great web app feels fast. It loads almost instantly. Every tap elicits an instantaneous reaction. You never have to worry about whether switching off of Wi-Fi as you leave the house is going to make everything slow and janky or completely broken. But the reality is web apps aren't magic in the supernatural sense. There's no wizards or prophecies or dragons. No. Web applications are magic in the close up magic sense. They're card tricks. They're the kind of magic that makes you think you're seeing one thing, and then you're seeing something else. Great web apps aren't real magic, but they are carefully crafted illusions. Now, you may not think of them this way, but web apps are actually all about sleight of hand. As web developers, it's our job to trick the user into thinking they're seeing a full-fledged application well before we've actually had time to fetch, parse, and load all of the different pieces of our web app. So how do we do it? How do we build an illusion so convincing that it becomes indistinguishable from the real thing? Well, it's time to reveal web developments biggest secrets. And, of course, like any good magician, I'm going to need an assistant. Oh sorry, not this assistant, although it is very helpful. This one. Firebase provides tools and cloud services that can help you craft your illusion and even pull off some feats that are really difficult otherwise. We've already seen this in action from our demo earlier. And we had four assistants from Firebase in that demo. We had Firebase Hosting, which provides automatic, zero configuration, HTTPS web hosting for your app. It also serves content over a global CDM so that you know it's fast. And newly launching at Google I/O this year, every Firebase Hosting website gets a free web.app subdomain. So now you can go figure out the cleverest name you can think of to claim your .web.app. Next, we have Firebase Auth, which you also saw, which provides identity as a service. It gives you a variety of mechanisms from email and password to phone number to authenticated linked accounts like Google that you saw in the demo. We also have Cloud Firestore, which provides a real-time synchronization to a cloud database from clients across the world. This works, not only online, but offline as well, as you saw. And I want to explain a concept that might be a little confusing at first. It's something called latency compensation. Now, in reality, the offline scenario is the worst case, but it's not the most common case. Lots of times, you're using an application, and it's not that your network is totally gone, it's just bad. You're going through a tunnel, or you're in an area that just doesn't have very good data coverage. And, in that case, the data is slow, but it's still going online. And the Cloud Firestore SDK implements something called latency compensation, which is that when you make changes, it applies them locally before it waits for the server to acknowledge the change. So if I write to a document in Cloud Firestore, it will immediately reflect that in my UI and then send it off to the server assuming that everything's good. Now, if the server rejects that, the Firestore SDK is smart enough to say, oh, that change was rejected, so I'm going to revert back to the previous version. But most of the time, it succeeds. And so most of the time, your users experience instantaneous reactions to their writes without having to wait for the server. And finally, you didn't really see it in action. But behind the scenes, Cloud Functions is also working here. Cloud Functions allows you to run server-side trusted code without having to run the server yourself. And there are two ways that it was working in this demo. The first is there's a scheduled execution. So this little app has the concept of scheduled task lists that recur every day. We recently launched the ability to schedule functions on a Cron-like basis. And this will automatically create new lists every day or on a time period that I've programmed. In addition, Cloud Firestore can be great for data normalization. So, in this case, whenever I complete or uncomplete a task, there's a Cloud Function that will update my list with the number of complete tasks from that list. That allows me to have a really quick view of the number of tasks that were complete without having to manually sync that from the client every single time. So these are the ways that Firebase was assisting us. But again, we're here to talk about magic. And when you're building a web application, it's actually not that different from performing a magic trick. And Penn and Teller have something that they call the seven principles of magic. And these are essentially seven moves that magicians will do that pile up together to create most of the illusions that you'll see from magicians. I'm going to go through them now. The first is one that you're probably all familiar with-- misdirection. That's to lead attention away from a secret move. That's, hey, look over here when something's happening over here. Next is palm, to hold an object in an apparently empty hand. You've probably seen that with card tricks when there's a card hidden behind the magician's hand. Ditch-- to secretly dispose of an unneeded object, like, oh, here, let me see that coin. It's over my shoulder. It's gone, disappeared, yay magic. Steal-- to secretly obtain a needed object. So that's I pull something from my pocket when it doesn't look like I'm doing that so that I have it for later. And load is related to move that hidden object to where you need it. Simulation-- to give the impression that something that hasn't happened has. So that's I throw an object, but I actually keep it in my hand. Your eye travels with the motion of my throw, but actually, it's still in my hand. And finally, switch-- to secretly exchange one object for another. So I've done a lot of talking about magic, and I better actually pay this off by tying it back to web development. So let's make some magic and talk through the steps of my demo but this time in the context of the seven principles of magic and the actual code that I wrote. We start with some misdirection. We display a static landing page with all critical styles inlined. We show a loading indicator for non-functional UI. Now the first time that a user visits your page is pretty much the most critical moment in their experience because that's going to decide whether they wait for it to load or whether they abandon it completely. And the best way to make sure that they stick around at least long enough to find out what your app is about is if you don't need anything fancy to get to that first paint. And so what I like to do is use completely static content that's just right in the HTML. There's no JavaScript. There's no server requests. There's no SDKs or libraries or frameworks. It's just good old HTML and CSS. So this is the rough structure of my demo app simplified a little bit. And you can see that I have three main HTML sections-- one called lander, one called loader, and one called app. So the lander is where I put all of my static content for the landing page. The loader is just a full page spinner. And app is where I actually use JavaScript to render my application once everything is ready. But for the first load, these are the only parts that actually matter. I have a style tag in the head of my HTML that contains all the styles to display that static landing page. Now sometimes, your landing page is actually pretty complex and figuring out what the critical style to inline is can be difficult. So I'm not saying that this is just, oh, just cram the entire thing in the head, and you'll be fine. There is some nuance here. But the idea is that you have as few network round trips as possible to get something displayed on the page. Next, we perform a steal. We fetch a slim JavaScript control bundle immediately when the page loads, and we preload our full application JavaScript and CSS. So how does this work? Well, here in my head, you can see that I have essentially four things that I'm pre-loading. First, I have a script-type module that points to a module/main.js. Next, I have a script nomodule pointing to nomodule/main.js. If you aren't aware of this pattern, this is actually a really important one to learn because it's developing quickly into a great way to minimize your JavaScript bundle size. Script type module is only recognized by browsers that support ES modules. So they can import modules using the ES module syntax. But importantly, the only browsers that understand ES modules also understand a whole lot of other things, probably async await, probably all kinds of things that you might be loading polyfills for in your application normally. So if you use script type module, you can omit all of those polyfills that you were using that don't apply to browsers that support modules. Now, you might be thinking, OK, that's great, but I need broader browser support than that. I can't just support browsers that have modules. And that's why the browser technology has a really clever hack of this nomodule. So script nomodule will be completely ignored by browsers that understand ES modules. But browsers that were created before that don't recognize this at all, so they just load it like normal JavaScript. So this sort of works like the various hacks that you used to use to do differential CSS loading or other kinds of techniques where the new browser understands the new thing, the old browser understands the old thing, and so you have the ability to sort of fork the path that you go on and really save in byte size on your JavaScript bundles. Now most JavaScript bundlers have some way to compile multiple targets. And so if you can configure your bundle to point to both a module version and a no module version, then you're going to save some bytes. Next, we have a few preload links. Link rel preload tells the browser, I'm going to need this very soon, so go ahead and start fetching it, but don't actually do anything with it yet. And we do that here with our application CSS and with some additional JavaScript that we're going to need that is loaded by the main JavaScript bundle. So that's the way that we are stealing all of the secret items that we're going to need later to make our application work. Next, we do a switch. Oh, sorry, one more tip on this. There is actually a style sheet link right in the middle of the page, which you may not have seen a bunch. This is a nifty thing that I just learned about recently. Modern browsers will work by rendering the entire page up to that point then blocking and loading the CSS before it renders additional elements. That's really useful to us here because that means our entire landing page content can be loaded before we ever even try to load the application CSS. Older browsers won't necessarily do that. They might block on the entire render, but it still will end up with the same effect. It'll just take a little bit longer. So this is sort of safe to do and works better for modern browsers. Next, we perform a switch. We attach interactivity, remove the loading indicator, and we removed the loading indicator from the static HTML over landing page. So this is just a really simple JavaScript. We have essentially a sign in button that we're going to grab. We're going to remove the disabled attribute. We're going to change the text from loading to sign in with Google. And we're going to add a click listener to it that will perform our actual sign in action. And our sign in action is done using Firebase Auth. And it's very simple here. We just say auth.signInWithPopup, and we provide a Google Auth provider. But the important thing to note here is that I am using an asynchronous import to pull in that module. And by doing that, that means that I don't have to have the entire Firebase Auth SDK loaded in my initial bundle. It can be fetched asynchronously when this loads. And that is the extra chunk that I was pre-loading in my HTML. And so we do this sort of progressive loading. And that's sort of the idea here. You're trying to keep just ahead of your user. You're trying to have just enough of your application working so that they don't notice that you're still loading other parts. Next, we do a simulation. Now you didn't see this in the demo because I talked long enough that it didn't matter. But sometimes, you might click that sign in button before the Firebase Auth SDK has fully loaded. And again, one of the most critical things that you need to do when building a web application is make sure that it is immediately responsive to user interaction. So if a user taps a button, the worst thing you can do is have nothing happen. And so, in this case, rather than have nothing happen, if the Firebase Auth SDK hasn't loaded yet, we transition to a full page loading screen, which at least lets the user know, hey, I got your interaction, I'm working on it, just give me a second. So the way that we do that, in my case, is really simple. I just add a loading class to the document body, and I remove it when I'm done with my work. And then I have some CSS that just displays a full page spinner essentially on top of everything else. Now, this is a pretty crude method, but it's actually really effective. And at least when you're starting out, this can be a good way to just have, OK, I need to just indicate that I'm loading for a minute. Let me stick this in there. And if you want to do sort of complex page transitions and animations and things like that, you can build to that, but you can start from here. Next, we perform another switch. The main bundle is now loaded, and it takes over seamlessly enabling full interaction, at least for Firebase Auth. And so Firebase Auth SDK takes over and pops the user out to the Google sign in. Now, I want to talk a little bit here about sort of the meat of the state machine of your web application. So we're using Firestore as our main data source for our web application. And you've probably heard of the idea of a central state store where you change the state store and then that causes things to render. And adding Firebase can work really well in that mix. It just goes above everything else. So what we're going to do is, first, we're going to attach listeners to a Firestore collection, in this case. But essentially, that can be a collection or a document in your database. And that listener will get fired every time the document changes or the query changes. And so we listen to that, and then we set our central state store based on the results of those queries. And so I take the data from Firebase, and now I set it in my central store. Then, in my central store, I subscribe to changes in that, and I trigger rendering. So, in this application, I used lit-html as my rendering engine. But this is really framework agnostic. You can do this with pretty much any of the modern frameworks because all of them pretty much support some kind of central state store that triggers rendering. And it's just a pretty good model because it's easy to reason about. You have state changes. State changes trigger rendering. Finally, the question is, what happens when the user interacts with my application? And the temptation is, well, OK, that changes the application state as well. But because of Firebase's real-time aspects and because Firestore has that latency compensation I talked about, the best thing to do is actually have user interactions talk directly to the Firestore SDK. And so when the user does an action, like submits a form, I just immediately go straight to the Firestore SDK, and I say, OK, well, now I'm going to add a new list here. And because all of my listeners are real time and because latency compensation will apply those changes before it waits for the server, that will immediately trigger my listener to say, oh, this query has changed, and so here's a new document, which then pipes into my state, which then flows down to my UI. And so you have this nice circle where it goes Firebase to state to UI and then back to Firebase. And that's sort of the core loop of the application. So that's a quick aside. I just wanted to talk it through because that's something that I think is just a useful mental model for how to work with this. So now, we're done. Right? Our application is loaded. The user is signed in. The full experience is at the user's fingertips. But what happens if our user refreshes the page? We spent all of this effort making sure that this landing page appears instantaneously as soon as the page is loaded, which means that now that the user is signed in, when they refresh, isn't that landing page just going to show up again and then flash into the application when it's loaded? That's the worst. I hate that. And so now we're going to perform a ditch. We know the user is signed in, so we don't show the landing page. And this is something where if you're sort of familiar with the different web APIs, you might cringe a little because I'm going to use local storage to do this, which is a synchronous API that lots of people hate for very good reasons. But it's also really good to do this specific thing, which is at the very beginning of my page before I've done anything else, I start my static HTML, and the body has a class pending on it. And before I've done anything else, I check if there is an item in local storage called signed in. If it's there, I add a class to the body that says, hey, I'm signed in, and then I remove the pending class. So it's just a couple lines of JavaScript. But what it does is say, before you've done anything else, before you've rendered any of the landing page HTML, before you've done literally anything, check to see if we're signed in. And if we are signed in, add a class. Now in my main app bundle, I just listen to the Firebase Auth state. And if there's a user, I set that item. If there's not a user, I remove it. And then in my critical inline CSS, I just make sure that body.pending, which is something that only happens for a split second when the page first loads, hides the entire page. So this is very hacky and very crude but also very effective. I don't get a flash of my landing page when I'm trying to load my signed in application. Again, it's all about sleight of hand. So, OK, now, our application is loaded. The user is signed in. The full experience is at our fingertips. But, oh no, the user has lost their internet connection. Surely, our illusion is doomed. Well, not quite. Let's rewind back to the very beginning of our user's journey because there was actually a little bit more going on than meets the eye. We did a palm before anything else was happening. We used two technologies to do this-- Service Workers and the Firestore SDK. Now Service Workers are a browser technology that allows you to intercept network requests and behave differently based on any number of factors, including whether the user is offline or not. So we use a Service Worker to cache all of the static assets, our HTML, JavaScript, CSS, and images, and we use the Firestore SDK to cache all of our fetched application data. And I'm going to take that second one first because enabling offline persistence in Firestore is like so easy that I'm still amazed by it. It's this. It's literally one call, and now your Firestore app will work offline. So using the Firestore SDK, you just say enablePersistence. In this case, I passed an option that makes it work from across multiple tabs as well. And you're done. That's literally all you have to do. And now all of the data that you fetched from Firestore will automatically be cached offline in an IndexedDB in your browser, so whatever you fetched will be available. And you still use the same exact SDK calls that you used when you were online to interact with this offline data. So basically, this one little switch can just turn your application into something that's capable of working offline. That's really impressive. And I'm really proud of all of my teammates that have made that work. We also need a Service Worker because if you refresh the page, it doesn't matter if you have all this data in IndexedDB if you don't have the HTML and JavaScript that reads the data. And so we just look to see if the browser supports a Service Worker. If it does, we wait until the page is loaded, and then we register our Service Worker. But, of course, we have to actually create the Service Worker that's going to do all the caching. And this is, again, why I'm glad I have teammates and other teams that do all kinds of cool things because we can use Workbox for this. Workbox is a library that's published by Google that provides you a number of tools for creating and utilizing Service Workers to do common tasks. In this case, what we want to do is cache all of our static assets so that they can be used offline. You can use Workbox as sort of a programmatic JavaScript API in your Service Worker for advanced use cases, or you can actually use Workbox to generate your Service Worker from scratch, which is what I did for this demo. So here is my Workbox configuration, and it has just a few small parts to it. First, we have the directory that we're going to glob. So this is essentially, where are all of your static files? And it's just going to go through, find all your static files, and add them into the cache. Next, we say, where do we want to generate the Service Worker? So obviously, I put that into the directory where my static files are. Next, we have the navigationFallback and the navigationFallbackBlacklist. The navigateFallback is essentially your single page application routing solution. So you say, whenever we're offline, if you go to a URL that I don't recognize, instead just serve index.html. So it's the same thing that you've used for any kind of single page routing on Firebase Hosting or anywhere else as well. One other thing that we want to add is we actually want to add a blacklist to the navigateFallback of any URLs that start with slash double underscore because Firebase and Firebase Hosting reserve that namespace to do some things like interacting with Firebase Auth for the redirects out to Google. And so you don't actually want your Service Worker serving up the index.html because sometimes that cache can trample over the user interactions that you need with Firebase. The last thing that we do is some runtime caching, which is where we define a URL pattern and say, anything that you load from these URLs, I want you to automatically cache. And then, I want you to serve the stale version, the cached version, even when I'm online while you go and fetch a fresh version and put it into the cache. So we use this for Google Fonts. And we also use this for our profile image, which is the thing that didn't work in the demo, and I'm not entirely sure why. But it's definitely worked most of the time. Next, we perform a ditch. We hide interactive elements that aren't able to work offline. Now I didn't really demo this as much, but in this application, creating a list is a little bit more complicated than just interacting with tasks. And there's an Add List button on the Home page, and that just fades out when you go offline. And there's also an Archive List button that fades out when you go offline. The way that this works-- this is like one of my favorite things to do that I just put in every application almost right away because it's surprisingly easy, and it gives you just a lot of flexibility. You can listen to events on the browser that tell you when you're offline. And you can also use this navigator.online with horrible capitalization-- that's like the worst web standard right there-- to tell whether or not the browser is currently online. And so the first thing I do is I just add these event listeners to online and offline, and I make sure that I do two things. First, I add a class to the body that says, I'm offline. Which means using only CSS, I can just say, well, if it's body.hidden and it's something with a class that's say hidden while offline, then just display none. That's the easiest way to just make things disappear off of your site when you're offline. That's also what triggered the transition from the purple background to the gray background with the offline banner is just using the CSS of, well, the body says offline. I also put that into my state so that I can actually understand throughout my application whether or not I'm offline. Next, we perform a simulation. So when you actually refresh the page while you're offline, the Service Worker intercepts the network requests and responds to them. It simulates like you're online. So the browser, the actual page itself, it just gets the same response as it normally gets. And it just chugs merrily along its way. Oh, I've got this HTML. I've got this JavaScript. I've got the CSS. I've got all the things I need to make a page. And the Firestore SDK simulates you being online by reading from the IndexedDB cache locally instead of from the network. So you have these two helpers in Service Worker and Firestore that are assisting you in simulating an online experience even when you're offline. Next, what happens when you go back online. Firestore automatically syncs when the connection is restored applying local rights and receiving the changed data. And so your offline goes back to your online. And that's what we saw when the data synced back up when we went online. So this gives us the ability to work offline even across multiple browsers, come back online, and everything will just sync back. Now, there is a small caveat in that the last write wins with the offline mode. So whoever essentially comes back online last, their changes are going to get applied as if they had just happened. And so you need to think about that when structuring your data model to make sure you don't have offline trampling happening, where somebody works offline and then just blows away the changes that someone else made online. There are a variety of ways to tackle that, and it's not the topic here, but it is something to call out. So with all of that, we have performed a number of tricks. We have continuously tried to fool the user into thinking that everything is working, everything is perfect and slick and polished and functional, even when behind the scenes, we are scrambling trying to keep ahead of the user, loading JavaScript and changing page state and making sure that we're saving everything. But, in the end, we did it. We have a fast-loading web application that syncs in real time and keeps working even when we're offline. And that's pretty magical. But, of course, this isn't the end of the road because magic takes practice. Over time, you will hone your craft and learn ever more convincing ways to trick people into thinking that your web app is awesome. And maybe over time with enough iterations, it actually will be awesome because an illusion that is utterly convincing is indistinguishable from the truth. So every day, you have an opportunity to find a new rough spot in your app, the little slip where the audience can see the card up your sleeve, and you can polish it. But how do you know where to look? And that's where one last assistant comes into play. Here at Google I/O, we've brought Firebase Performance Monitoring to the web. Firebase Performance Monitoring measures the performance of your application in the hands of real users on real devices. Here's just a screenshot. That's an example of what it looks like. It can show you the distribution of how real users are experiencing your app in the wild. And Firebase Performance Monitoring measures many of the most important metrics like First Contentful Paint and time to interactive automatically just by dropping in the SDK. It can tell you exactly the way that your audience is experiencing your illusion of a web application. And critically, it can show you how an illusion that's utterly convincing to someone on the latest pixel phone on a 4G network might not work so well for someone on an older device and a slower network. You can break this data down by connection type or device type. And you can see, oh wow, my first page load is horrible on this specific type of phone. And maybe my JavaScript is consuming too much CPU, or maybe I'm just loading too many things, and the network connectivity here is really bad, and it can't do parallel requests. Firebase Performance Monitoring is completely free. And it gives you the access to real user monitoring, which is really important if you want to craft that convincing illusion. So what I hope I've done today is spark some ideas as to how you can make better web applications by focusing on how your application appears and feels to users because ultimately, it doesn't matter how messy your application is under the covers. What matters is the real experience of your users when they visit the application. And so by using these tricks and other ones that you'll invent and other ones that other people will invent and stringing them all together, you can craft this convincing illusion of an amazing web application. Thanks for your time. And if you'd like to chat more, I'll be heading over to the Firebase Sandbox area right now. Enjoy the rest of Google I/O. [MUSIC PLAYING]
Info
Channel: Firebase
Views: 17,039
Rating: 4.9697733 out of 5
Keywords: type: Conference Talk (Full production);, pr_pr: Google I/O, purpose: Educate
Id: NwY6jkohseg
Channel Id: undefined
Length: 35min 10sec (2110 seconds)
Published: Thu May 09 2019
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.