Instant Loading with Service Workers (Chrome Dev Summit 2015)

Instant Loading with Service Workers (Chrome Dev Summit 2015)


So, hi, I’m Jeff Posnick from
the Chrome Developer Relations Team. We’ve heard a lot about the
benefits of progressive web apps, and we’ve also heard
a lot about Service Workers and we’ll be hearing more about
those in sessions to come. But I want to take the
time to talk about one approach to structuring your
progressive web app using what we’re calling the App Shell. The App Shell
architecture works really well with a Service
Worker caching as progressive enhancement. So during the session we’ll
talk in detail about the SW pre-cache and SW tool
box libraries, which make it easy for
developers to add Service Workers to their web apps. Web apps that use the App
Shell plus Service Worker model effectively can feel
like they load instantly. And before we go
any further, let me demonstrate what I mean
with a real world example. So I built a web app to browse
iFixit repair guides using iFixit’s excellent public API. The iFixit API returns
a mix of text and images like you’d find in many
content browsing applications, and this the screencast
we’re using the iFixit API to load in a list
of featured guides and then browsing
through a few of them. So you can imagine
each of these pages being something a user discovers
on the web as a search result or finds a link to
from some other site. It’s the web so there’s nothing
special to install in advance. So loading obviously
looks fast, but let’s put some hard numbers behind a few
different loading scenarios. So all of these examples
were run on a simulated 2G mobile network against
the App Engine instance we’ve deployed on, and
we’re showing off top level navigation’s here. So what you see on the
screen is experience loading the web app
for the first time with nothing in the cache and
no Service Worker in place yet. We’re able to get all the
above-the-fold content on screen in a very
respectable 650 milliseconds. So let’s compare
that first visit to what your users will
experience the next time they visit the same page. With a Service Worker in place
and your cache populated, we’re able to get the
above-the-fold content on screen in around
200 milliseconds. As a point of
comparison, the same page loaded with a populated cache
but without a Service Worker, maybe because your browser
doesn’t support it, takes around 560 milliseconds. And let’s take a look at the
learning experience for a page that we haven’t
previously visited. So because a web app Service
Worker is already installed and the App Shell is cached, new
pages load instantly as well. So we can get
above-the-fold content on the screen in
225 milliseconds. With the same cache content
but without a service worker, we’re looking at
about 650 milliseconds to get the content
on the screen. You can see that even on
a slower mobile network, great load times are
possible across the board. So regardless of how you
structure your web app, that initial request for
the HTML landing page can take time. On the left is your
network traffic requesting our iFixit demo
app for the first time with no Service Worker
and nothing in our cache. It takes 423 milliseconds before
we get a full response back. And this includes the
time it takes the server to make the iFixit
API request for us. On the right, we’re serving
the same page directly from our cache. It only takes five milliseconds
once the Service Worker gets involved. So with a Service Worker
and with the right caching strategy, the network
no longer stands in the way of getting your
app on your users’ screens. So we strongly
feel that adopting the model we’ll be
talking about today is the surest way to build web
apps that consistently load in under a second, which is a
target we recommend in the Rail performance model. You’ll hear more about Rail
during tomorrow’s sessions, but keep that in mind. And as my colleague
[? Paul ?] pointed out, loading instantly can
make a big difference in a web app’s success. Studies have shown that an
additional one second delay in page load times
can lead to 11% overall fewer page views as well
as a 16% decrease in customer satisfaction. So instant loading
isn’t just nice to have, there’s a direct impact
on your bottom line. So hopefully
everyone out there is ready to go and adopt this
model in your own web apps. But before you can
do that, let’s run through an explanation
of the key concepts you’ll need to understand. So first, what’s an App Shell? So your shell is the
HTML, JavaScript, css, and any other static
resources that provide the structure for your
page minus the actual content specific to the page. So the shell should
be cached, obviously, and should load quickly. Think of your shell
as being kind of similar to the native
code that would get uploaded to an
app store if you were writing a native application. But you don’t have to
sacrifice the linkabilty and discoverability of the web. And unlike with native
apps, you deploy updates to App Shell in a second. The concept of an
App Shell is going to come up again and again
throughout this presentation. Anytime you see
something colored green, note that we’re talking
about the App Shell. So once the shell
content loads, we fetch dynamic content, in
our case from the iFixit API, to populate the view. The App Shell plus
our dynamic content equals the complete
rendered page. Any time you see
something colored blue, know that we’re talking
about the dynamic content. So now that we’ve talked
about the App Shell, we can turn to
the next question, what’s a Service Worker. Technically speaking,
a Service Worker is a network proxy
written in JavaScript and intercepts your HTTP
requests made from your web pages. But an analogy
that I like to use is to think of a Service
Worker as being an air traffic controller. So let’s see how this
analogy plays out. You can think of your
web app’s HTTP requests as planes taking off. With a Service Worker you now
have an air traffic controller making sure that your requests
load quickly and efficiently. So the request plans might
land using the network runway, or they might land using the
cache runway in which case the request can bypass
the network completely. So it’s up to the
Service Worker to decide how to handle each request. And here’s a quick
visual walk-through of the life of a Service Worker. We start out with a page
that isn’t yet controlled by a Service Worker
but contains code to kick off the registration. So that creates a new
Service Worker instance and fires the install
event that the Service Worker will respond to. And now is our chance to
add our App Shell’s content to the cache. Let’s dig in deeper. So while every setup
may be different, in our iFixit sample we’re
serving the HTML for our App Shell from the /shell URL. Keep that in mind throughout
this presentation when you’ll see a /shell
pop up again. So to populate our cache, the
service worker makes an HTTP get request for /shell
during the install events. And let’s walk through
the contents of our shell. So we’re including some
site-wide inline styles that’ll be shared by all of our pages. And next we have a
template insertion point within our body element. So this doesn’t contain
any real content but it serves kind of as a
placeholder for the client side templating that will take place
when our content is loaded. And finally, in this
example, we have a reference to an external file app.js
that contains all the site wide JavaScript
that our page needs. So back to the life
of the Service Worker. The next event you’ll get a
chance to handle is activate. So here you normally
perform cache cleanup of any out-of-date resources you
no longer need in your shell. And once those
handlers complete, your Service Worker
enters into an idle state. So it’s running, but
it’s not doing anything until there’s a network request
that fires off a new event. And in response to
a network request, a fetch event handler will get a
chance to intercept the request and respond as you see fit. So that happens after
a period of idleness, your service worker script
is stopped automatically. So when the next
network request is made when your page is loaded
again it’s started back up and can immediately
respond to fetch events. So the air traffic controller
analogy breaks down a bit here because you
probably wouldn’t want to tear down your
air traffic control tower every time a plane lands
and wait for another one, but bear with me here. OK, so let’s take a look at
how our fetch handler serves up our App Shell. Our Service Worker can intercept
any requests for any URL under its scope, including
pages that haven’t been previously visited. So in our case, we want to
respond to any navigation requests with the /shell
response that we’ve previously cached. And this will bypass
the network completely. So this is a model that you
might already be familiar with. It’s what happens with server
side rendering when there’s a URL pattern that’s routed to a
matching handler, which is then responsible for returning
the full page to the browser. And this is the Service Worker
equivalent of that routing. And it’s code that runs
client side on the App Shell that ends up populating the
content in the scenario. And if you’re using a
universal JavaScript framework it might end up being the
same templating code that runs on both the
server and the client, but that’s not a requirement
for using this model. So at this point, I hope you see
the advantages of structuring your web app using the App
Shell plus Service Worker model. And while you could write
your own service worker code, we have two libraries
that will take care of many of
the details for you while following
the best practices and avoiding common gotchas. So, going back to our
analogy for a second, you may be an awesome
web developer, but you might not
feel comfortable writing the code responsible
for landing an airplane and that’s fine. These libraries will
handle that code for you. So the first library that
we’re going to talk about is sw-precache. So it takes care of
caching and maintaining all the resources
in your App Shell. Second library we’ll
talk is sw-toolbox. It provides a canonical
implementation of all the runtime caching
strategies that you need for your dynamic content. So we built these libraries
to go together hand-in-hand. And using them
together gives you a Service Worker that can
load your App Shell instantly while giving you
fine-grained control over how your dynamic content is cached. So both of these libraries
are battle tested and ready for production. They’ve powered the Service
Worker for this year’s Google I/O web app, so chances
are you’ve already visited a page that uses them. I was a member of the team
that built the I/O web app and we specifically
designed these libraries to handle the real
world needs that came up while building the site. I wrote up a case study
about our experiences which you can read at
the URL on the slide. And we’re also really
excited to announce the flipkart is using the
sw-toolbox library directly as part of their new
progressive web app. You’ll hear more
about that tomorrow. All right so let’s dive into
the first library, sw-precache. So most web apps already have
some sort of build process in place, and sw-precache
drops right into your existing build process and uses wildcards
to identify all the resources that make up your App Shell. And we’re using Gulp
in the iFixit demo app, but sw-precache works
equally well with Grunt, and we have a command
line version as well. All right so let’s take a look
at what happens when your build process includes sw-precache. So here we have a set
of local files in our build directory that
make up our App Shell. We perform a Gulp build,
which includes a step that runs sw-precache for us. sw-precache is configured
with some wildcards to match all those local files. And what it does is calculate
a hash of each of the files that it finds that
matches the pattern, and generates a service
worker code for you that contains a mapping of the
URL and the current hash. So this Service Worker
JS file that it generates contains all the code
that you need to maintain the cache of those files. You don’t have to write a thing,
you can just use it as is. And the initial visit to a web
app that uses a Service Worker triggers a pre-caching of all
the URLs that were identified. And you can think
of this as kind of being equivalent to like
a native app store install process. OK? So your App Shell changes
over time and sw-precache will make sure that your
cache changes along with it. So let’s assume you make
some updates to two files and you’re ready to push those
changes out to your web server. When you run Gulp as
part of your deployment, it will regenerate the
service worker JS file. Since two of the files
now have new contents, the associated hash
will change as well. And there’s a logic in the
generated service worker file that will detect
those changes for you and automatically update
the cache entries just for the files that have changed. When the users
return to your app, only new or modified
resources are downloaded. You can kind of think of
this as a native app store’s incremental updates. So those who prefer a
command line build process to [? Gulp or Grunt ?] can
install the sw-precache module globally from NPM and
then run it directly. So remember to rerun
the command prior to deploying an
update to your site to ensure that the generated
service worker picks up on all the local changes. All right, let’s get
our hands dirty a bit and take a look at how I’m using
sw-precache in the iFixit web app demo that I showed earlier. So this snippet is
part of the Gulp build strip for the
project which gets run through Babel, so it can use all
soft of [? ES2015 ?] goodness. And first and most importantly,
we have the dynamic URL to dependencies
option and this is how we keep an always up-to-date
copy of the application shell cached. So we use the URL /shell as
a key and the value here is an array of all the files that
uniquely define the inline contents of /shell. Next we set the navigate
fallback option to be /shell again. Our new service worker file
will automatically serve the contents of /shell whenever
there is a navigation to any URL on our site. It will serve out the
application show even if the actual navigation for
the request is something like /guide123, and client side URL
routing will handle populating content. Next, static file globs
is also really important. This is where we list all
the individual App Shell resources that might be
requested directly as opposed to those that are included
in-line within the shell. And finally we have the
import scripts option. And here’s where we can
extend the generated service worker to include additional
logic that we might need. In this case, we’re
pulling in a configuration file for sw-toolbox which is the
next library we’ll talk about. All right so sw-toolbox
is the answer for caching your web
apps dynamic content. So everything that we’ve colored
blue in this presentation basically. And while sw-precache integrates
with your build process, sw-toolbox is loaded by your
Service Worker at run time. And there are number of very
common caching strategies sw-toolbox provides a canonical
implementation of each. So let’s take a look at some
of those caching strategies. And one approach is to have
the service worker check the cache for a response. If it’s there, great,
return to the page. If it’s not there in
the cache already, then return a response
from the network instead. So this is a good
strategy to use when you’re dealing with
remote resources that are very unlikely to change
like static images or something like that. And it corresponds
to sw-toolbox’s cache first handler. The converse of that approach
is to check the network first for a response, and
if that’s successful, return it to the page. If the network
request fails, you can fall back to a previously
cached entry instead. And this is a good
strategy to use when you’re dealing
with data that needs to be as
fresh as possible, like, say, a real-time
API response. But you still want to display
something as a fallback when the network is unavailable. And this corresponds
to sw-toolbox’s network first handler. So finally, here’s an
interesting strategy– The Service Worker fires
off both network requests and requests it goes against
the cache at the same time. Cache response will
normally come back first, and that will get returned
directly to the page. But in the mean time, the
response from the network is used to update the
previously cached entry, and that keeps things
relatively fresh. This updates from the
background without blocking rendering of the cache content. And you can get this
for free just by using the toolbox.fastest strategy. It’s pretty powerful. So you do need to think
a bit strategically here. You need to take time and
think about which strategy is most appropriate for
the dynamic resources that populate your App Shell. But you don’t have
to choose just one. sw-toolbox’s routing syntax lets
you apply different strategies to different URL old patterns. But you might be
saying you could implement those caching
strategies yourself. So why use sw-toolbox? sw-toolbox has a couple
of additional options that help solve
problems that arise while fetching your content. These options make Service
Worker caching much more useful in real world scenarios. So first a phenomenon that
I’m sure everyone here has encountered, Lie-Fi. So it’s one of your
device’s network connection, but it’s one that’s
extremely unreliable or slow. While your App Shell
should always be cached, [? as ?] be fetched
cache first, you might request
dynamic content used to populate your
shell using a network for a strategy in some cases. And Lie-Fi can be
deadly in those cases. If there were no network
at all, your requests would fall back to
the cache right away. But when there is a
network request that just drags on and on and on
before eventually failing, you end up wasting precious
seconds just waiting for the inevitable. So using sw-toolbox, you can
set an explicit network timeout. And here we’re setting
it to three seconds when fetching an
image network first. And after those
three seconds have passed, if we don’t have
a response from network, it’ll automatically fallback
to the cache content for us. So here’s another
real world problem you might bump up against. What happens as users go from
page to page on your site. So you probably
caching those page specific contents,
like images associated with each page the user
visits at one time, to make sure that
if they ever return, the full page will
load instantly, not just the App Shell. If you keep adding to your
dynamic caches indefinitely, you’ll end up using an ever
increasing amount of storage. So sw-toolbox will actually
take care of cache expiration for you saving you the trouble
of implementing it yourself. Here we have sw-toolbox to use
a dedicated cache for images with a maximum
cache size of six. So once the cache is
full, as it is now, new images will cause the
least recently used images to be evicted. In addition to the least
recently used expiration option, sw-toolbox also gives
you a time-based expiration option where you can say
automatically expire everything once it reaches a certain age. So flipkart was looking for a
solution to this exact problem and we’re really
happy that they turned to sw-toolbox for that job. All right, so now
let’s take a look at the sw-toolbox configuration
used in our iFixit demo app that we showed earlier. So this file gets included with
our Service Worker at runtime by using the import scripts
option in sw-precache. So first, we define a route
for our iFixit API requests and we’re using the tool box
stuff fastest strategy here, which strikes a nice balance
between automatically returning stale content immediately
and automatically keeping our caches
up-to-date in the background. So next, we explicitly
add an image to our cache that will act as
a placeholder missing image if the request for
a real image fails. That, in turn, uses as part
of our image handler function, which takes a request, attempts
to get a response using a cache first strategy,
and if that fails falls back to that
missing image placeholder. And finally, you can
see how we set up a route that uses
our image handler function to handle
requests for images served from CloudFront.net. We’re also making
use of a maximum size for this dedicated
cache that we’re using for images so after
50 images have loaded in the cache, the least recently
used ones will automatically be expired. All right so I’m sure
that throughout this talk there’s been something
in the back of your mind. This App Shell model
is great, but how does it work in browsers that
don’t support Service Workers? Well, Service Workers
are, in general, progressive enhancement and
that applies when they’re used in the App Shell model as well. So going back to
our analogy, it’s possible to land
planes at an airport without an air
traffic controller, but you’re probably gonna want
to do that very slowly and very deliberately. And I wouldn’t really want to
be a passenger on that plane. So putting our
analogy aside, how do you go about
treating service workers as a progressive enhancement? Well remember that the first
time you visit a web app, you won’t yet be a
Service Worker in place. So you need to make sure that
your web app renders properly without Service Workers anyway
it’d handle that scenario. And browsers that support
Service Workers, next visit to your web can take full
advantage of everything that Service Workers
have to offer. But for browsers that don’t
support Service Workers, the next visit to
your site ends up looking very much
like that first visit. There won’t be a Service
Worker, so you just continue to serve things
via network requests. So your architecture
shouldn’t have to change in order to
accommodate browsers that lack Service Worker support. So whether there’s a Service
Worker in your browser or not, HTTP caching best
practices still apply. So you should continue
to follow patterns like adding in hash fingerprints
into your file names and using far future HTTP
caching expiration headers. This doesn’t change any of that. And sw-precache will happily
work with that set up. But if you are in a browser
that supports Service Workers, you now have a key to unlock
additional performance ones. So you can serve even your
initial HTML landing page directly from the cache when
using a Service Worker, which is difficult to do safely
with just HTTP cache headers. If you remember from
the start of the talk, that means the difference
between 423 milliseconds to retrieve your landing
page and five milliseconds. And you can use that
same cache for a strategy even for pages that
are dynamically rendered on the server
because sw-precache will keep those pages fresh for you when
one of the underlying partials or in-line continent changes. All right so where
does this talk leave you as a developer
who’s interested in using this architecture? Well, we have a number of demos
and jumping off points that you can choose from to get started. So first of all,
you should realize that if you’re building
a modern single page app, you’re probably
already using something similar to an App Shell
already, whether you call it that or not. The details might
vary a bit depending upon which libraries or
frameworks you’re using, but the concept itself is
definitely framework agnostic. So your next step could be
just to add in a Service Worker to your existing web app using
sw-precache and sw-toolbox libraries. But if you’re
starting from scratch and you want some
inspiration or just want to see a finished real
world example here’s the source code
for the iFixit API client I demonstrated earlier. It uses universal JavaScript,
both server and client side rendering, and can be adapted
to go against a different API or source of dynamic
content if you want. Next for those who prefer
a step by step, hands on learning experience,
we put together a code lab that walks
you through adding sw-precache to an existing
web apps build process. Those of you who are
attending in person, you can do it in the code
section that’s outside. I also want to mention
a Polymer based demo that my colleague Rob
Dotson put together, which makes use of an App
Shell, a Service Worker model in a blog web app which
I think is called Zuperkulblog. Rob will be talking more
about that project including the correct pronunciation
a little bit later today in his session. Next most recent release of
the Web Starter Kit project includes sw-precache and
sw-toolbox and projects that use Web Starter
Ki as a starting point will take advantage
of them by default. So this is super
powerful in terms of getting this technology out
there to folks who are getting started on web development. And finally my colleagues
[? Attius ?] [? Mony ?] and [? Matt ?] [? Gant ?] spent
a great deal of time putting together this Vanilla
JavaScript starting point. It gives you an App
Shell plus Service Worker setup with minimal opinions
about frameworks or libraries. I highly recommend that
folks check this project out. And it gives you a great place
to start with all the best practices already baked in. OK so we strongly believe that
the App Shell plus service worker model is the best way to
structure your web apps if you want reliable instant
load times without having to worry about the network
getting in your way. We hope you are inspired
to go out there and adopt this model today. There’s no reason
to wait, and we know your users will appreciate
not having to wait either. Thank you. [APPLAUSE] [MUSIC PLAYING]

Author:

10 thoughts on “Instant Loading with Service Workers (Chrome Dev Summit 2015)”

Leave a Reply

Your email address will not be published. Required fields are marked *