Last Updated: 2019-04-30
Progressive Web Apps provide an installable, app-like experience on desktop and mobile that are built and delivered directly via the web. They're web apps that are fast and reliable. And most importantly, they're web apps that work in any browser. If you're building a web app today, you're already on the path towards building a Progressive Web App.
Every web experience must be fast, and this is especially true for Progressive Web Apps. Fast refers to the time it takes to get meaningful content on screen, and provide an interactive experience in less than 5 seconds.
And, it must be reliably fast. It's hard to stress enough how much better reliable performance is. Think of it this way: the first load of a native app is frustrating. It's gated by an app store and a huge download, but once you get to a point where the app is installed, that up-front cost is amortized across all app starts, and none of those starts have a variable delay. Each application start is as fast as the last, no variance. A Progressive Web App must deliver this reliable performance that users have come to expect from any installed experience.
Progressive Web Apps can run in a browser tab, but are also installable. Bookmarking a site just adds a shortcut, but an installed Progressive Web App looks and behaves like all of the other installed apps. It launches from the same place as other apps launch. You can control the launch experience, including a customized splash screen, icons and more. It runs as an app, in an app window without an address bar or other browser UI. And like all other installed apps, it's a top level app in the task switcher.
Remember, it's critical that an installable PWA is fast and reliable. Users who install a PWA expect that their apps work, no matter what kind of network connection they're on. It's a baseline expectation that must be met by every installed app.
Using responsive design techniques, Progressive Web Apps work on both mobile and desktop, using a single code base between platforms. If you're considering writing a native app, take a look at the benefits that a PWA offers.
In this codelab, you're going to build a weather web app using Progressive Web App techniques. Your app will:
beforeinstallprompt
event to notify the user it's installable.This codelab is focused on Progressive Web Apps. Non-relevant concepts and code blocks are glossed over and are provided for you to simply copy and paste.
Our weather data comes from the Dark Sky API. In order to use it, you'll need to request an API key. It's easy to use, and free for non-commercial projects.
To test that your API Key is working properly, make an HTTP request to the DarkSky API. Update the URL below to replace DARKSKY_API_KEY
with your API key. If everything works, you should see the latest weather forecast for New York City.
https://api.darksky.net/forecast/DARKSKY_API_KEY/40.7720232,-73.9732319
We've put everything you need for this project into a Git repo. To get started, you'll need to grab the code and open it in your favorite dev environment. For this codelab, we recommend using Glitch.
Using Glitch is the recommended method for working through this codelab.
.env
file, and update it with your DarkSky API key.If you want to download the code and work locally, you'll need to have a recent version of Node, and code editor setup and ready to go.
npm install
to install the dependencies required to run the server.server.js
and set your DarkSky API key.node server.js
to start the server on port 8000.Our starting point is a basic weather app designed for this codelab. The code has been overly simplified to show the concepts in this codelab, and it has little error handling. If you choose to reuse any of this code in a production app, make sure that you handle any errors and fully test all code.
Some things to try...
FORECAST_DELAY
in server.js
Lighthouse is an easy to use tool to help improve the quality of your sites and pages. It has audits for performance, accessibility, progressive web apps, and more. Each audit has a reference doc explaining why the audit is important, as well as how to fix it.
We'll use Lighthouse to audit our Weather app, and verify the changes we've made.
We're going to focus on the results of the Progressive Web App audit.
And there's a lot of red to focus on:
start_url
does not respond with a 200 when offline.start_url.
Let's jump in and start fixing some of these issues!
By the end of this section, our weather app will pass the following audits:
The web app manifest is a simple JSON file that gives you, the developer, the ability to control how your app appears to the user.
Using the web app manifest, your web app can:
display
).start_url
).short_name
, icons
).name
, icons
, colors
).orientation
).Create a file named public/manifest.json
in your project and copy/paste the following contents:
public/manifest.json
{
"name": "Weather",
"short_name": "Weather",
"icons": [{
"src": "/images/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
}, {
"src": "/images/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
}, {
"src": "/images/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
}, {
"src": "/images/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}, {
"src": "/images/icons/icon-256x256.png",
"sizes": "256x256",
"type": "image/png"
}, {
"src": "/images/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}],
"start_url": "/index.html",
"display": "standalone",
"background_color": "#3E4EB8",
"theme_color": "#2F3BA2"
}
The manifest supports an array of icons, intended for different screen sizes. For this code lab, we've included a few others since we needed them for our iOS integration.
Next, we need to tell the browser about our manifest by adding a to each page in our app. Add the following line to the
element in your
index.html
file.
<!-- CODELAB: Add link rel manifest -->
<link rel="manifest" href="/manifest.json">
DevTools provides a quick, easy way to check your manifest.json
file. Open up the Manifest pane on the Application panel. If you've added the manifest information correctly, you'll be able to see it parsed and displayed in a human-friendly format on this pane.
Safari on iOS doesn't support the web app manifest (yet), so you'll need to add the traditional meta
tags to the of your
index.html
file:
<!-- CODELAB: Add iOS meta tags and icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Weather PWA">
<link rel="apple-touch-icon" href="/images/icons/icon-152x152.png">
Our Lighthouse audit called out a few other things that are pretty easy to fix, so let's take care of those while we're here.
Under the SEO audit, Lighthouse noted our "Document does not have a meta description." Descriptions can be displayed in Google's search results. High-quality, unique descriptions can make your results more relevant to search users and can increase your search traffic.
To add a description, add the following meta
tag to the of your document:
<!-- CODELAB: Add description here -->
<meta name="description" content="A sample weather app">
In the PWA audit, Lighthouse noted our app "Does not set an address-bar theme color". Theming the browser's address bar to match your brand's colors provides a more immersive user experience.
To set the theme color on mobile, add the following meta
tag to the of your document:
<!-- CODELAB: Add meta theme-color -->
<meta name="theme-color" content="#2F3BA2" />
Run Lighthouse again (by clicking on the + sign in the upper left corner of the Audits pane) and verify your changes.
SEO Audit
Progressive Web App Audit
start_url
does not respond with a 200 when offline.start_url.
There is an expectation from users that installed apps will always have a baseline experience if they're offline. That's why it's critical for installable web apps to never show Chrome's offline dinosaur. The offline experience can range from a simple, offline page, to a read-only experience with previously cached data, all the way to a fully functional offline experience that automatically syncs when the network connection is restored.
In this section, we're going to add a simple offline page to our weather app. If the user tries to load the app while offline, it'll show our custom page, instead of the typical offline page that the browser shows. By the end of this section, our weather app will pass the following audits:
start_url
does not respond with a 200 when offline.start_url.
In the next section, we'll replace our custom offline page with a full offline experience. This will improve the offline experience, but more importantly, it'll significantly improve our performance, because most of our assets (HTML, CSS and JavaScript) will be stored and served locally, eliminating the network as a potential bottleneck.
If you're unfamiliar with service workers, you can get a basic understanding by reading Introduction To Service Workers about what they can do, how their lifecycle works and more. Once you've completed this code lab, be sure to check out the Debugging Service Workers code lab for a more in-depth look at how to work with service workers.
Features provided via service workers should be considered a progressive enhancement, and added only if supported by the browser. For example, with service workers you can cache the app shell and data for your app, so that it's available even when the network isn't. When service workers aren't supported, the offline code isn't called, and the user gets a basic experience. Using feature detection to provide progressive enhancement has little overhead and it won't break in older browsers that don't support that feature.
The first step is to register the service worker. Add the following code to your index.html
file:
// CODELAB: Register service worker.
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then((reg) => {
console.log('Service worker registered.', reg);
});
});
}
This code checks to see if the service worker API is available, and if it is, the service worker at /service-worker.js
is registered once the page is loaded.
Note, the service worker is served from the root directory, not from a /scripts/
directory. This is the easiest way to set the scope
of your service worker. The scope
of the service worker determines which files the service worker controls, in other words, from which path the service worker will intercept requests. The default scope
is the location of the service worker file, and extends to all directories below. So if service-worker.js
is located in the root directory, the service worker will control requests from all web pages at this domain.
First, we need to tell the service worker what to cache. We've already created a simple offline page (public/offline.html
) that we'll display any time there's no network connection.
In your service-worker.js
, add '/offline.html',
to the FILES_TO_CACHE
array, the final result should look like this:
// CODELAB: Update cache names any time any of the cached files change.
const FILES_TO_CACHE = [
'/offline.html',
];
Next, we need to update the install
event to tell the service worker to pre-cache the offline page:
// CODELAB: Precache static resources here.
evt.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
console.log('[ServiceWorker] Pre-caching offline page');
return cache.addAll(FILES_TO_CACHE);
})
);
Our install
event now opens the cache with caches.open()
and provides a cache name. Providing a cache name allows us to version files, or separate data from the cached resources so that we can easily update one but not affect the other.
Once the cache is open, we can then call cache.addAll()
, which takes a list of URLs, fetches them from the server and adds the response to the cache. Note that cache.addAll()
will reject if any of the individual requests fail. That means you're guaranteed that, if the install step succeeds, you cache will be in a consistent state. But, if it fails for some reason, it will automatically try again the next time the service worker starts up.
Let's take a look at how you can use DevTools to understand and debug service workers. Before reloading your page, open up DevTools, go the Service Workers pane on the Application panel. It should look like this:
When you see a blank page like this, it means that the currently open page does not have any registered service workers.
Now, reload your page. The Service Workers pane should now look like this:
When you see information like this, it means the page has a service worker running.
Next to the Status label, there's a number (34251 in this case), keep an eye on that number as you're working with service workers. It's an easy way to tell if your service worker has been updated.
We'll use the activate
event to clean up any old data in our cache. This code ensures that your service worker updates its cache whenever any of the app shell files change. In order for this to work, you'd need to increment the CACHE_NAME
variable at the top of your service worker file.
Add the following code to your activate
event:
// CODELAB: Remove previous cached data from disk.
evt.waitUntil(
caches.keys().then((keyList) => {
return Promise.all(keyList.map((key) => {
if (key !== CACHE_NAME) {
console.log('[ServiceWorker] Removing old cache', key);
return caches.delete(key);
}
}));
})
);
With the Service Workers pane open, refresh the page, you'll see the new service worker installed, and the status number increment.
The updated service worker takes control immediately because our install
event finishes with self.skipWaiting()
, and the activate
event finishes with self.clients.claim()
. Without those, the old service worker would continue to control the page as long as there is a tab open to the page.
And finally, we need to handle fetch
events. We're going to use a network, falling back to cache strategy. The service worker will first try to fetch the resource from the network, if that fails, it will return the offline page from the cache.
// CODELAB: Add fetch event handler here.
if (evt.request.mode !== 'navigate') {
// Not a page navigation, bail.
return;
}
evt.respondWith(
fetch(evt.request)
.catch(() => {
return caches.open(CACHE_NAME)
.then((cache) => {
return cache.match('offline.html');
});
})
);
The fetch
handler only needs to handle page navigations, so other requests can be dumped out of the handler and will be dealt with normally by the browser. But, if the request .mode
is navigate
, use fetch
to try to get the item from the network. If it fails, the catch
handler opens the cache with caches.open(CACHE_NAME)
and uses cache.match('offline.html')
to get the precached offline page. The result is then passed back to the browser using evt.respondWith()
.
Let's check to make sure everything works as we expect it. With the Service Workers pane open, refresh the page, you'll see the new service worker installed, and the status number increment.
We can also check to see what's been cached. Go to the Cache Storage pane on the Application panel of DevTools. Right click Cache Storage, pick Refresh Caches, expand the section and you should see the name of your static cache listed on the left-hand side. Clicking on the cache name shows all of the files that are cached.
Now, let's test out offline mode. Go back to the Service Workers pane of DevTools and check the Offline checkbox. After checking it, you should see a little yellow warning icon next to the Network panel tab. This indicates that you're offline.
Reload your page and... it works! We get our offline panda, instead of Chrome's offline dino!
Debugging service workers can be a challenge, and when it involves caching, things can become even more of a nightmare if the cache isn't updated when you expect it. Between the typical service worker lifecycle and a bug in your code, you may become quickly frustrated. But don't.
In the Service Workers pane of the Application panel, there are a few checkboxes that will make your life much easier.
In some cases, you may find yourself loading cached data or that things aren't updated as you expect. To clear all saved data (localStorage, indexedDB data, cached files) and remove any service workers, use the Clear storage pane in the Application tab. Alternatively, you can also work in an Incognito window.
Additional tips:
Run Lighthouse again and verify your changes. Don't forget to uncheck the Offline checkbox before you verify your changes!
SEO Audit
Progressive Web App Audit
start_url
responds with a 200 when offline.start_url.
Take a moment and put your phone into airplane mode, and try running some of your favorite apps. In almost all cases, they provide a fairly robust offline experience. Users expect that robust experience from their apps. And the web should be no different. Progressive Web Apps should be designed with offline as a core scenario.
The life cycle of the service worker is the most complicated part. If you don't know what it's trying to do and what the benefits are, it can feel like it's fighting you. But once you know how it works, you can deliver seamless, unobtrusive updates to users, mixing the best of the web and native patterns.
install
eventThe first event a service worker gets is install
. It's triggered as soon as the worker executes, and it's only called once per service worker. If you alter your service worker script the browser considers it a different service worker, and it'll get its own install
event.
Typically the install
event is used to cache everything you need for your app to run.
activate
eventThe service worker will receive an activate
event every time it starts up. The main purpose of the activate
event is to configure the service worker's behavior, clean up any resources left behind from previous runs (e.g. old caches), and get the service worker ready to handle network requests (for example the fetch
event described below).
fetch
eventThe fetch event allows the service worker to intercept any network requests and handle requests. It can go to the network to get the resource, it can pull it from its own cache, generate a custom response or any number of different options. Check out the Offline Cookbook for different strategies that you can use.
The browser checks to see if there is a new version of your service worker on each page load. If it finds a new version, the new version is downloaded and installed in the background, but it is not activated. It's sits in a waiting state, until there are no longer any pages open that use the old service worker. Once all windows using the old service worker are closed, the new service worker is activated and can take control. Refer to the Updating the service worker section of the Service Worker Lifecycle doc for further details.
Choosing the right caching strategy depends on the type of resource you're trying to cache and how you might need it later. For our weather app, we'll split the resources we need to cache into two categories: resources we want to precache and the data that we'll cache at runtime.
Precaching your resources is a similar concept to what happens when a user installs a desktop or mobile app. The key resources needed for the app to run are installed, or cached on the device so that they can be loaded later whether there's a network connection or not.
For our app, we'll precache all of our static resources when our service worker is installed so that everything we need to run our app is stored on the user's device. To ensure our app loads lightning fast, we'll use the cache-first strategy; instead of going to the network to get the resources, they're pulled from the local cache; only if it's not available there will we try to get it from the network.
Pulling from the local cache eliminates any network variability. No matter what kind of network the user is on (WiFi, 5G, 3G, or even 2G), the key resources we need to run are available almost immediately.
The stale-while-revalidate strategy is ideal for certain types of data and works well for our app. It gets data on screen as quickly as possible, then updates that once the network has returned the latest data. Stale-while-revalidate means we need to kick off two asynchronous requests, one to the cache and one to the network.
Under normal circumstances, the cached data will be returned almost immediately providing the app with recent data it can use. Then, when the network request returns, the app will be updated using the latest data from the network.
For our app, this provides a better experience than the network, falling back to cache strategy because the user does not have to wait until the network request times out to see something on screen. They may initially see older data, but once the network request returns, the app will be updated with the latest data.
As mentioned previously, the app needs to kick off two asynchronous requests, one to the cache and one to the network. The app uses the caches
object available in window
to access the cache and retrieve the latest data. This is an excellent example of progressive enhancement as the caches
object may not be available in all browsers, and if it's not the network request should still work.
Update the getForecastFromCache()
function, to check if the caches
object is available in the global window
object, and if it is, request the data from the cache.
// CODELAB: Add code to get weather forecast from the caches object.
if (!('caches' in window)) {
return null;
}
const url = `${window.location.origin}/forecast/${coords}`;
return caches.match(url)
.then((response) => {
if (response) {
return response.json();
}
return null;
})
.catch((err) => {
console.error('Error getting data from cache', err);
return null;
});
Then, we need to modify updateData()
so that it makes two calls, one to getForecastFromNetwork()
to get the forecast from the network, and one to getForecastFromCache()
to get the latest cached forecast:
// CODELAB: Add code to call getForecastFromCache.
getForecastFromCache(location.geo)
.then((forecast) => {
renderForecast(card, forecast);
});
Our weather app now makes two asynchronous requests for data, one from the cache and one via a fetch
. If there's data in the cache, it'll be returned and rendered extremely quickly (tens of milliseconds). Then, when the fetch
responds, the card will be updated with the freshest data direct from the weather API.
Notice how the cache request and the fetch
request both end with a call to update the forecast card. How does the app know whether it's displaying the latest data? This is handled in the following code from renderForecast()
:
// If the data on the element is newer, skip the update.
if (lastUpdated >= data.currently.time) {
return;
}
Every time that a card is updated, the app stores the timestamp of the data on a hidden attribute on the card. The app just bails if the timestamp that already exists on the card is newer than the data that was passed to the function.
In the service worker, let's add a DATA_CACHE_NAME
so that we can separate our applications data from the app shell. When the app shell is updated and older caches are purged, our data will remain untouched, ready for a super fast load. Keep in mind, if your data format changes in the future, you'll need a way to handle that and ensure the app shell and content stay in sync.
// CODELAB: Update cache names any time any of the cached files change.
const CACHE_NAME = 'static-cache-v2';
const DATA_CACHE_NAME = 'data-cache-v1';
Don't forget to also update the CACHE_NAME
; we'll be changing all of our static resources as well.
In order for our app to work offline, we need to precache all of the resources it needs. This will also help our performance. Instead of having to get all of the resources from the network, the app will be able to load all of them from the local cache, eliminating any network instability.
Update the FILES_TO_CACHE
array with the list of files:
// CODELAB: Add list of files to cache here.
const FILES_TO_CACHE = [
'/',
'/index.html',
'/scripts/app.js',
'/scripts/install.js',
'/scripts/luxon-1.11.4.js',
'/styles/inline.css',
'/images/add.svg',
'/images/clear-day.svg',
'/images/clear-night.svg',
'/images/cloudy.svg',
'/images/fog.svg',
'/images/hail.svg',
'/images/install.svg',
'/images/partly-cloudy-day.svg',
'/images/partly-cloudy-night.svg',
'/images/rain.svg',
'/images/refresh.svg',
'/images/sleet.svg',
'/images/snow.svg',
'/images/thunderstorm.svg',
'/images/tornado.svg',
'/images/wind.svg',
];
Since we are manually generating the list of files to cache, every time we update a file we must update the
CACHE_NAME
. We were able to remove offline.html
from our list of cached files because our app now has all the necessary resources it needs to work offline, and won't ever show the offline page again.
To ensure our activate
event doesn't accidentally delete our data, in the activate
event of service-worker.js
, replace if (key !== CACHE_NAME) {
with:
if (key !== CACHE_NAME && key !== DATA_CACHE_NAME) {
We need to modify the service worker to intercept requests to the weather API and store their responses in the cache, so we can easily access them later. In the stale-while-revalidate strategy, we expect the network response to be the ‘source of truth', always providing us with the most recent information. If it can't, it's OK to fail because we've already retrieved the latest cached data in our app.
Update the fetch
event handler to handle requests to the data API separately from other requests.
// CODELAB: Add fetch event handler here.
if (evt.request.url.includes('/forecast/')) {
console.log('[Service Worker] Fetch (data)', evt.request.url);
evt.respondWith(
caches.open(DATA_CACHE_NAME).then((cache) => {
return fetch(evt.request)
.then((response) => {
// If the response was good, clone it and store it in the cache.
if (response.status === 200) {
cache.put(evt.request.url, response.clone());
}
return response;
}).catch((err) => {
// Network request failed, try to get it from the cache.
return cache.match(evt.request);
});
}));
return;
}
evt.respondWith(
caches.open(CACHE_NAME).then((cache) => {
return cache.match(evt.request)
.then((response) => {
return response || fetch(evt.request);
});
})
);
The code intercepts the request and checks if it is for a weather forecast. If it is, use fetch
to make the request. Once the response is returned, open the cache, clone the response, store it in the cache, and return the response to the original requestor.
We need to remove the evt.request.mode !== 'navigate'
check because we want our service worker to handle all requests (including images, scripts, CSS files, etc), not just navigations. If we left that check in, only the HTML would be served from the service worker cache, everything else would be requested from the network.
The app should be completely offline-functional now. Refresh the page to ensure that you've got the latest service worker installed, then save a couple of cities and press the refresh button on the app to get fresh weather data.
Then go to the Cache Storage pane on the Application panel of DevTools. Expand the section and you should see the name of your static cache and data cache listed on the left-hand side. Opening the data cache should show the data stored for each city.
Then, open DevTools and switch to the Service Workers pane, and check the Offline checkbox, then try reloading the page, and then go offline and reload the page.
If you're on a fast network and want to see how weather forecast data is updated on a slow connection, set the FORECAST_DELAY
property in server.js
to 5000
. All requests to the forecast API will be delayed by 5000ms.
It's also a good idea to run Lighthouse again.
SEO Audit
Progressive Web App Audit
start_url
responds with a 200 when offline.start_url.
When a Progressive Web App is installed, it looks and behaves like all of the other installed apps. It launches from the same place as other apps launch. It runs in an app without an address bar or other browser UI. And like all other installed apps, it's a top level app in the task switcher.
In Chrome, a Progressive Web App can either be installed through the three-dot context menu, or you can provide a button or other UI component to the user that will prompt them to install your app.
In order for a user to be able to install your Progressive Web App, it needs to meet certain criteria. The easiest way to check is to use Lighthouse and make sure it meets the installable criteria.
If you're worked through this codelab, your PWA should already meet these criteria.
First, let's add the install.js
to our index.html
file.
<!-- CODELAB: Add the install script here -->
<script src="/scripts/install.js"></script>
beforeinstallprompt
eventIf the add to home screen criteria are met, Chrome will fire a beforeinstallprompt
event, that you can use to indicate your app can be 'installed', and then prompt the user to install it. Add the code below to listen for the beforeinstallprompt
event:
// CODELAB: Add event listener for beforeinstallprompt event
window.addEventListener('beforeinstallprompt', saveBeforeInstallPromptEvent);
In our saveBeforeInstallPromptEvent
function, we'll save a reference to the beforeinstallprompt
event so that we can call prompt()
on it later, and update our UI to show the install button.
// CODELAB: Add code to save event & show the install button.
deferredInstallPrompt = evt;
installButton.removeAttribute('hidden');
When the user clicks the install button, we need to call .prompt()
on the saved beforeinstallprompt
event. We also need to hide the install button, because .prompt()
can only be called once on each saved event.
// CODELAB: Add code show install prompt & hide the install button.
deferredInstallPrompt.prompt();
// Hide the install button, it can't be called twice.
evt.srcElement.setAttribute('hidden', true);
Calling .prompt()
will show a modal dialog to the user, asking them to add your app to their home screen.
You can check to see how the user responded to the install dialog by listening for the promise returned by the userChoice
property of the saved beforeinstallprompt
event. The promise returns an object with an outcome
property after the prompt has shown and the user has responded to it.
// CODELAB: Log user response to prompt.
deferredInstallPrompt.userChoice
.then((choice) => {
if (choice.outcome === 'accepted') {
console.log('User accepted the A2HS prompt', choice);
} else {
console.log('User dismissed the A2HS prompt', choice);
}
deferredInstallPrompt = null;
});
One comment about userChoice
, the spec defines it as a property, not a function as you might expect.
In addition to any UI you add to install your app, users can also install your PWA through other methods, for example Chrome's three-dot menu. To track these events, listen for the appinstalled event.
// CODELAB: Add event listener for appinstalled event
window.addEventListener('appinstalled', logAppInstalled);
Then, we'll need to update the logAppInstalled
function, for this codelab, we'll just use console.log
, but in a production app, you probably want to log this as an event with your analytics software.
// CODELAB: Add code to log the event
console.log('Weather App was installed.', evt);
Don't forget to update the CACHE_NAME
in your service-worker.js
file since you've made changes to files that are already cached. Enabling the Bypass for network checkbox in the Service Workers pane of the Application panel in DevTools will work in development, but won't help in the real world.
Let's see how our install step went. To be safe, use the Clear site data button in the Application panel of DevTools to clear everything away and make sure we're starting fresh. If you previously installed the app, be sure to uninstall it, otherwise the install icon won't show up again.
First, let's verify our install icon shows up properly, be sure to try this on both desktop and mobile.
Next, let's make sure everything installs properly, and our events are properly fired. You can do this either on desktop or mobile. If you want to test this on mobile, be sure you're using remote debugging so you can see what's logged to the console.
Note, if you're running on desktop from localhost, your installed PWA may show an address banner because localhost isn't considered a secure host.
Let's also check the behavior on iOS. If you have an iOS device, you can use that, or if you're on a Mac, try the iOS Simulator available with Xcode.
The display-mode
media query makes it possible to apply styles depending on how the app was launched, or determine how it was launched with JavaScript.
@media all and (display-mode: standalone) {
body {
background-color: yellow;
}
}
You can also check the display-mode
media query in JavaScript to see if you're running in standalone.
Remember, the beforeinstallevent
doesn't fire if the app is already installed, so during development you'll probably want to install and uninstall your app several times to make sure everything is working as expected.
On Android, PWAs are uninstalled in the same way other installed apps are uninstalled.
On ChromeOS, PWAs are easily uninstalled from the launcher search box.
On Mac and Windows, PWAs may be uninstalled through Chrome:
You can also open the installed PWA, click the the dot menu in the upper right corner, and choose "Uninstall Weather PWA..."
Congratulations, you've successfully built your first Progressive Web App!
You added a web app manifest to enable it to be installed, and you added a service worker to ensure that your PWA is always fast, and reliable. You learned how to use DevTools to audit an app and how it can help you improve your user experience.
You now know the key steps required to turn any web app into a Progressive Web App.
Check out some of these codelabs...