We're looking for a person or people to help audit MDN to find places we could speed up. Is this you or someone you know? Check out the RFP: https://mzl.la/2IHcMiE

App Center

Progressive web app structure

Now we know the theory behind PWAs, let's look at the recommended structure of an actual app. We will start with analyzing the js13kPWA application, why it is built that way, and what benefits it brings.

Architecture of an app

There are two main, different approaches to rendering a website — on the server or on the client-side. They both have their advantages and disadvantages, and you can mix the two approaches to some degree.

  • Server-Side Rendering (SSR) means a website is rendered on the server, so it offers quicker first load, but navigating between pages requires downloading everything every single time. It works great across browsers and has lots of tools available to help with the development process, but it suffers in terms of loading speed and therefore general perceived performance if loading every single page requires a new round trip to the server.
  • Client-Side Rendering (CSR) allows the website to be updated in the browser almost instantly when navigating to different pages, but requires more of an initial download hit and extra rendering on the client at the beginning. The website will be slower on an initial visit, but much faster on subsequent visits.

Mixing SSR with CSR can lead to the best results — you can render a website on the server, cache its contents, and then update the rendering on the client-side as and when needed. The first page load is quick because of the SSR, and the navigation between pages is smooth because the client can re-render the page with only the parts that have changed.

PWAs can be built using any approach you like, but some will work better than the others. The most popular approach is the "App Shell" concept, which mixes SSR and CSR in exactly the way described above, and in addition follows the "offline first" methodology which we will explain in detail in upcoming articles and use in our example application. There is also a new approach involving the Streams API, which we'll mention briefly.

App Shell

The App Shell concept is concerned with loading a minimal user interface as soon as possible and then caching it so it is available offline for subsequent visits before then loading all the contents of the app. That way, the next time someone visits an app the UI will load from the cache immediately and any new content will be requested from the server (if it isn’t available in the cache already).

It is fast, and also feels fast as the user sees "something" instantly, instead of a loading spinner or a blank page. It also allows the website to be accessible offline if the network connection is not available.

We can control what is requested from the server and what is retrieved from the cache with a service worker, which will be explained in detail in the next article — for now let's focus on the structure itself.

Why should I use it?

This architecture allows a website to benefit the most from all the PWA features — it caches the Shell and manages the dynamic content in a way that greatly improves the performance. In addition to the basic shell, you can add other features such as add to home screen or push notifications, safe in the knowledge that the app will still work OK if they are not supported — this is the beauty of progressive enhancement.

The website feels like a native app with instant interaction and solid performance while keeping all the benefits of the web.

Being linkable, progressive and responsive by design

It's important to remember the PWA advantages and keep them in mind when designing the application. The App Shell approach allows websites to be:

  • Linkable: Even if it behaves like a native app, it is still a website — you can click on the links within the page and send a URL to someone if you want to share it.
  • Progressive: Start with the "good, old basic website” and progressively add new features while remembering to detect if they are available in the browser and gracefully handle any errors that crop up if support is not available. For example, an offline mode with the help of service workers is just an extra trait that will make your website experience better, but it's still perfectly usable without it.
  • Responsive: Responsive Web Design also applies to Progressive Web Apps, as both are mainly for mobile. There are so many varied devices with browsers — it's important to prepare your website so it works on different screen sizes, viewports or pixel densities, using technologies like viewport meta tag, CSS media queries, Flexbox, and CSS Grid.

Different concept: streams

An entirely different approach to Server or Client-Side Rendering can be achieved with the Streams API. With a little help from Service Workers, streams can greatly improve the way we parse content.

The App Shell model requires all the resources to be available before the website can start rendering. It's different with HTML as the browser is actually streaming the data already and you can see when the elements are loaded and rendered on the website. To have the JavaScript "operational", however, it has to be downloaded in its entirety.

The Streams API allows developers to have direct access to data streaming from the server — if you want to perform an operation on the data (e.g. adding a filter to a video), you no longer need to wait for all of it to be downloaded and converted to a blob (or whatever) — you can start straight away. It provides fine-grained control — the stream can be started, chained with another stream, cancelled, checked for errors, and more.

In theory, streaming is a better model, but it's also more complex, and at the time of writing (March 2018) the Streams API is still work-in-progress and not fully available in any of the major browsers yet. When it is available, it will be the fastest way of serving the content — the benefits are going to be huge in terms of performance.

For working examples and more information, see the Streams API documentation.

Structure of our example application

The js13kPWA website is quite simple: it consists of a single HTML file (index.html) with basic CSS styling (style.css), and a few images and fonts. The folder structure looks like this:


From the HTML point of view, the App Shell is everything outside the content section:

<!DOCTYPE html>
    <meta charset="utf-8">
    <title>js13kGames A-Frame entries</title>
    <meta name="description" content="A list of A-Frame entries submitted to the js13kGames 2017 competition, used as an example for the MDN articles about Progressive Web Apps." />
    <meta property="og:image" content="img/logo.png" />
    <link rel="shortcut icon" href="favicon.png">
    <link rel="stylesheet" href="style.css">
    <link rel="manifest" href="js13kpwa.webmanifest">
    <script src="data/games.js"></script>
        <a href="http://js13kgames.com">js13kGames</a>
            <li><a href="http://js13kgames.com/aframe">js13kgames.com/aframe</a></li>

    <h1>js13kGames A-Frame entries</h1>
    <p class="description">List of games submitted to the A-Frame category in the js13kGames 2017 competition.</p>

    <section id="content">

      // Content inserted in here


    <a class="fork page" href="https://github.com/mdn/pwa-examples/blob/master/js13kpwa" title="Fork me on GitHub"></a>

        &copy; js13kGames 2012-2018
        <p>Created and maintained by <a href="http://end3r.com">Andrzej Mazur</a> from <a href="http://enclavegames.com">Enclave Games</a>.</p>

<script src="app.js"></script>

The <head> section contains some basic info like title, description and links to CSS, web manifest and games content JS files. The <body> is split into the <header> (containing two links), <main> page (with title, description, place for a content and the GitHub "fork me" banner), and <footer> (copy and links). At the bottom of the page the app.js file is loaded — that's where our JavaScript application is initialized.

The only job the app has is to list all the A-Frame entries from the js13kGames 2017 competition. As you can see it is a very ordinary, one page website — the point is to have something simple so we can focus on the implementation of the actual PWA features.


The CSS is also as plain as possible: we use @font-face to load and use a custom font, and some simple styling of the HTML elements. The overall approach is to have the design look good on both the mobile and desktop devices.

The main app JavaScript

The app.js file does a few things we will look into closely in the next articles. First of all it generates the content based on the template:

var template = "<article>\n\
    <img src='data/img/SLUG.jpg'>\n\
    <h3>#POS. NAME</h3>\n\
    <li><span>Author:</span> <strong>AUTHOR</strong></li>\n\
    <li><span>Twitter:</span> <a href='https://twitter.com/TWITTER'>@TWITTER</a></li>\n\
    <li><span>Website:</span> <a href='http://WEBSITE/'>WEBSITE</a></li>\n\
    <li><span>GitHub:</span> <a href='https://GITHUB'>GITHUB</a></li>\n\
    <li><span>More:</span> <a href='http://js13kgames.com/entries/SLUG'>js13kgames.com/entries/SLUG</a></li>\n\
var content = '';
for(var i=0; i<games.length; i++) {
    var entry = template.replace(/POS/g,(i+1))
    content += entry;
document.getElementById('content').innerHTML = content;

Next, it registers a Service Worker:

if('serviceWorker' in navigator) {

The next code block requests permission for notifications:

Notification.requestPermission().then(function(result) {
    if(result === 'granted') {

The last block creates random notifications:

function randomNotification() {
    var randomItem = Math.floor(Math.random()*games.length);
    var notifTitle = games[randomItem].name;
    var notifBody = 'Created by '+games[randomItem].author+'.';
    var notifImg = 'data/img/'+games[randomItem].slug+'.jpg';
    var options = {
        body: notifBody,
        icon: notifImg
    var notif = new Notification(notifTitle, options);
    setTimeout(randomNotification, 30000);

The service worker

The last file we will quickly look at is the Service Worker: sw.js — it first imports data from the games.js file:


Next, it creates a list of all the files to be cached, both from the App Shell and the content:

var cacheName = 'js13kPWA-v1';
var appShellFiles = [
var gamesImages = [];
for(var i=0; i<games.length; i++) {
var contentToCache = appShellFiles.concat(gamesImages);

The next block installs the service worker, which then actually caches all the files contained in the above list:

self.addEventListener('install', function(e) {
  console.log('[Service Worker] Install');
    caches.open(cacheName).then(function(cache) {
      console.log('[Service Worker] Caching all: app shell and content');
      return cache.addAll(contentToCache);

Last of all, the service worker fetches content from the cache if it is available there, providing offline functionality:

self.addEventListener('fetch', function(e) {
    caches.match(e.request).then(function(r) {
      console.log('[Service Worker] Fetching resource: '+e.request.url);
      return r || fetch(e.request).then(function(response) {
        return caches.open(cacheName).then(function(cache) {
          console.log('[Service Worker] Caching new resource: '+e.request.url);
          cache.put(e.request, response.clone());
          return response;

The JavaScript data

The games data is present in the data folder in a form of a JavaScript object (games.js):

var games = [
        slug: 'lost-in-cyberspace',
        name: 'Lost in Cyberspace',
        author: 'Zosia and Bartek',
        twitter: 'bartaz',
        website: '',
        github: 'github.com/bartaz/lost-in-cyberspace'
        slug: 'vernissage',
        name: 'Vernissage',
        author: 'Platane',
        twitter: 'platane_',
        website: 'github.com/Platane',
        github: 'github.com/Platane/js13k-2017'
// ...
        slug: 'emma-3d',
        name: 'Emma-3D',
        author: 'Prateek Roushan',
        twitter: '',
        website: '',
        github: 'github.com/coderprateek/Emma-3D'

Every entry has its own image in the data/img folder. This is our content, loaded into the content section with JavaScript.

Next up

In the next article we will look in more detail at how the App Shell and the content are cached for offline use with the help from the Service Worker.


Document Tags and Contributors

 Contributors to this page: chrisdavidmills
 Last updated by: chrisdavidmills,