[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]