Create a Progressive Web App with React
Web applications have been the bridge between those ugly, desktop-only applications from a few years back and the way-too-cool looking mobile apps we have today. Just look how Google Earth partly became the daily commodity we call Google Maps.
Web applications themselves have evolved: Standards like HTML5, HTTP/2 and CSS3 animations have allowed us to create sleek, fast performing applications, completely out-living the purpose the Web had 10 or 15 years ago. Web Applications are doing more powerful things every day.
But this evolution brings new challenges. Web Applications are not accessed only from desktop browsers anymore. Most of the web traffic comes from mobile devices with limited memory and, most of the time, low speed networks and bad reception. The performance issues we thought we left behind with our shiny, full of RAM computers are back to hunt us again.
Progressive Web Applications (or PWA) give us a way to tackle these challenges: they are accessible, they have good performance, and they follow best practices, which allow the app to be used offline and they work on relatively low-memory devices.
The concept might sound complex, but it is nothing else than to make sure your web application met certain quality guidelines. It’s not a new technology, but the use existing tools to allow your applications to be considered “progressive.”
Serverless
Most of the guides you find on how to build your web app to be considered “progressive” rely heavily in the assumption that you do have a server behind you can thinker with. The use of HTTP/2 features like push or multiplexing requests is constantly suggested. The way we are building Serverless applications don’t allow us to have that fine-grained level of control over our server exchanges.
So, how do take our Single Page Application, which is nothing else than a set of static files, to be a PWA?
Lighthouse
Google is pushing hard to set PWAs as a design pattern for all web apps. They offer a tool called Lighthouse which we can use to measure how ready our web application is to be “progressive”.
We will use this tool to measure a new React web app, and we will use its feedback to upgrade our app to a PWA level.
Install Lighthouse using NPM:
npm install -g lighthouse
A sample app
We will create a very small and simple Web App, also taco related. We will have a single React component, and we’ll use Webpack (with HtmlWebpackPlugin) and Babel to build it. The source code is here.
This is the project structure:
/json
data.json
/src
entry.js
.babelrc
index.html.tmpl
package.json
webpack.config.js
This is our only React
class Main extends React.Component {
constructor(props) {
super(props);
this.state = {
tacos:[]
};
}
componentDidMount(){
axios.get('json/data.json').then(data=>{
this.setState({
tacos: data.data
})
});
}
render() {
return (
<section className="container">
<h1>Today tacos</h1>
<div>
{this.state.tacos.map(t=>(
<div key={t.id}>
<h2>{t.name}</h2>
<span style={{color:'grey'}}>Price: ${t.price}</span>
<p>{t.description}</p>
</div>
))}
</div>
</section>
)
}
}
render((<Main/>), document.getElementById('app'))
Start it using webpack-dev-server, and go to http://localhost:8080/:
Now, let’s see how progressive our app is. Run Lighthouse for our app:
lighthouse http://localhost:8080/ --view
This will generate a report like the following:
You will see that the application by default does well in Accessibility and Best Practices. This is understandable as our application currently does nothing. It, however, does not do well in the PWA and Performance sections. Let’s see what the current application is missing:
Registers a Service Worker
Service Workers (don’t confuse them with Web Workers, like I did) provide a proxy interface for all our HTTP requests, including AJAX requests, scripts, stylesheets and the HTML document itself. This allows SWs to catch the requests and responses before they are submitted to the web app, and do good things with them, the main being caching resources. And the best part is that, since they are JavaScript, we don’t need a backend to control the cache.
So, for this example, let’s add a Web Worker which will cache our resources, and our application it will use this cache to serve them, even if the user has no internet connection.
The web worker will list the resources we want to cache, but since we are using Webpack, those files might have auto-generated hashes in their names. That’s why we need Webpack’s ManifestPlugin. That plugin will generate a file with the list of our resources. We will use this list to tell our Service Worker which resources to cache.
Once we installed the ManifestPlugin, in the src folder, create a file named offline.js:
const CACHE_NAME = 'v1'
self.addEventListener("install", event => {
console.log("Installed");
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache =>
fetch("/dist/pwa-manifest.json")
.then(response => response.json())
.then(assets =>
cache.addAll([
"/",
assets["main.js"],
assets["vendor.js"]
])
)
).then(() => self.skipWaiting())
.catch(err => console.log)
);
});
self.addEventListener('fetch', function(event) {
console.log('Fetch')
event.respondWith(
caches.match(event.request).then(function(response) {
return response || fetch(event.request);
})
.catch(e => {console.error("Error on the cache",e)})
);
});
self.addEventListener("activate", event => {
const cacheWhitelist = [CACHE_NAME];
event.waitUntil(
caches.keys()
.then(keyList =>
Promise.all(keyList.map(key => {
if (!cacheWhitelist.includes(key)) {
return caches.delete(key);
}
}))
)
.then(() => self.clients.claim())
);
});
The service worker adds three listeners:
- Install — This is called only once, when the Service Worker is installed. We open a cache entry called “v1” and using our resources manifest, we list the resources to cache. In this case, vendor.js, main.js, the index.html document itself and data.json.
- Fetch — This gets called once per each HTTP request. Here we check if the requested resource exists in the cache. If it does, we return the the cached version, otherwise, we request for the resource. Here we can add more complex logic to do different things based on what resource we are retrieving, as we don’t want to return stale data to the user.
- Activate — This will get called when the Service Worker is activated. In here, it will clear the cache, to make sure we retrieve newer versions of our files.
Now, we need to register our service worker. We could do it directly in our index.html.tmpl file, but I want to keep all my code managed by Webpack. so we will add the following code to entry.js:
import sw from "!!file?outputPath=../&publicPath=/&name=offline.js!babel!./offline";
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register(sw).catch(err => {
console.error("Could not register service worker",err)
});
} else {
console.error("Service workers are not supported")
}
We use file-loader to make sure the Service Worker code is copied as it is, we check the browser supports Service Workers and we register it.
Run your application, and no errors should show. Let’s see what Lighthouse shows now:
Slightly better, but it still shows red numbers. We can see that “Responds with a 200 when offline” was also fixed, as now the application will return content even if it’s offline.
Redirects HTTP traffic to HTTPS
This will be fixed once we deploy our application, so let’s not worry about it too much as we are testing with a development server.
Has a <meta name="viewport">
tag with width
or initial-scale
We need to add the meta tag for viewport to make sure our app is responsive. Let’s add the following line to index.html.tmpl:
<meta name="viewport" content="width=device-width, initial-scale=1.0">
Metadata provided for Add to Home screen
Another important step for a PWA is to provide a manifest. This is a different manifest as the one being generated by Webpack. We will add a manifest.json file to our src folder:
{
"short_name": "Taco Gallery",
"name": "My Taco Gallery",
"icons": [{
"src": "/dist/icons/favicon-32x32.png",
"type": "image/png",
"sizes": "32x32"
},
{
"src": "/dist/icons/favicon-96x96.png",
"type": "image/png",
"sizes": "96x96"
},
{
"src": "/dist/icons/favicon-230x230.png",
"type": "image/png",
"sizes": "230x230"
}
],
"start_url": "index.html?launcher=true",
"display": "standalone",
"background_color": "white",
"theme_color": "#999999"
}
Add the following line to entry.js to make sure you include the manifest in the bundle:
import manifest from "!!file?publicPath=/&name=manifest.json!./manifest.json";
And add the following line to index.html.tmpl:
<link rel="manifest" href="/manifest.json">
Now, try Lighthouse again:
A lot better now.
Address bar matches brand colors
To provide a more concise style to our application, we need to give a theme color to it.
Add the following to index.html.tmpl:
<meta name="theme-color" content="#999999" />
Now, let’s see what Lighthouse gives us as score:
That’s a great score for the PWA section, considering the score will go up once we deploy or app over HTPPS.
Let’s do a quick experiment. Open your app in Chrome and open your developer tools (F12):
Now, in the Network tab from the DevTools, check the “Offline” box. This will simulate an offline environment. Reload the page and you should see that it still loads:
The difference you see is the Bootstrap stylesheet I’m adding to the app, which is served from a CDN and we didn’t include in the cache.
Lighthouse is an excellent tool to diagnose problems in our web app. Even if you’re not aiming towards a progressive app, it can be helpful to diagnose performance and accessibility issues.
This covers the PWA part of our app. Technically we followed the main steps for our app to become a PWA, but Lighthouse keeps complaining about performance; so in the next post we will go over some improvements that will help us to increase our score and will let us see the complete picture of a Progressive Web Application.