Making GitLab a PWA

Or, how I learned to stop worrying and embrace the boring solution.

Sam Beckham
@samdbeckham

3G

WiFi

1s

3s

3G

WiFi

0.3s

0.3s

Offline

3G

WiFi

0.3s

0.3s

0.3s

It's a
website
that acts like a
native app

Install to
 your device

Load offline

It's a
progressive enhancement

Just a
website

The

boring
solution

Lighthouse

Non-Javascript content

✅ https

✅ https redirect

✅ Fast enough on 3g

✅ <meta viewport> tag

✅ Sized for the viewport

 Custom splash screen

❌ Themed Address Bar

❌ Service Worker

❌ 200 When offline

 Prompted to install the web app

Lighthouse

Custom Splash Screen

Themed Address bar

{
  "name": "GitLab",
  "short_name": "GitLab",
  "display": "standalone",
  "scope": "/",
  "start_url": "/",
  "theme_color": "#474D57",
  "background_color": "#ffffff",
  "icons": [
    {
      "src": "touch-icon-ipad-retina.png",
      "sizes": "152x152",
      "type": "image/png"
    }
  ]
}
./manifest.json
<head>
  
  <!-- other head stuff -->
  
  <link rel="manifest" href="/manifest.json">

</head>
./yourpage.html

Lighthouse

✅ Custom splash screen

✅ Themed Address Bar

❌ Service Worker

❌ 200 When offline

❌ Prompted to install the web app

Service worker

💻

🌐

Browser

Server

request

response

💻

🌐

Browser

Server

🐲

Service

Worker

💻

🌐

Browser

Server

🐲

Service

Worker

cat.png

cat.png

💵

Cache

💻

🌐

Browser

Server

🐲

Service

Worker

cat.png

cat.png

💵

Cache

💤

200 when offline

 

 

The

boring
solution

200 when offline

 

 

The

boring
solution

💻

🌐

Browser

Server

🐲

Service

Worker

offline.html

💵

Cache

💻

🌐

Browser

Server

🐲

Service

Worker

issue

offline

💵

Cache

issue

💧

 

 

// Register a service worker if our
// browser allows it

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register(
    '/service_worker.js',
    { scope: '/'}
  );
}

Register

const CURRENT_CACHE = '...'

self.addEventListener('install', event => {
  event.waitUntil(
    caches
      .open(CURRENT_CACHE)
      .then(cache => 
        cache.add('/offline'))
      )
})

Install

./service_worker.js
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cache =>
          cache !== CURRENT_CACHE
            ? caches.delete(cache)
            : Promise.resolve()
        ),
   ...
})

Activate

./service_worker.js
self.addEventListener('fetch', ({request}) => {
  event.respondWith(
    fetch(request).catch(() => (
      caches.match('/offline')
    ))
  )
})

Fetch

./service_worker.js
const CURRENT_CACHE = '<%= Gitlab.version %>_<%= Gitlab.revision %>';

// eslint-disable-next-line no-restricted-globals
self.addEventListener('install', event => {
  event.waitUntil(caches.open(CURRENT_CACHE).then(cache => cache.add('/offline')));
});

// eslint-disable-next-line no-restricted-globals
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.map(cache =>
          cache !== CURRENT_CACHE ? caches.delete(cache) : Promise.resolve(),
        ),
      );
    }),
  );
});

// eslint-disable-next-line no-restricted-globals
self.addEventListener('fetch', { request } => {
  event.respondWith(
    fetch(request).catch(() => (
      request.mode === 'navigate' ? caches.match('/offline') : null
    )),
  );
});
./service_worker.js

Lighthouse

❌ Prompted to install the web app

✅ Service worker

✅ 200 when offline

✅ https

✅ http => https

✅ Custom Splash Screen

✅ Themed Address bar

✅ non-javascript content

✅ Fast enough on 3g

 <meta viewport> tag

✅ Sized correctly for the viewport

Prompted to install the web app

Do literally nothing else

✅ Service worker

Lighthouse

✅ 200 when offline

✅ https

✅ http => https

✅ Prompted to install the web app

✅ Custom Splash Screen

✅ Themed Address bar

✅ non-javascript content

✅ Fast enough on 3g

 <meta viewport> tag

✅ Sized correctly for the viewport

Gotchas

CI⚡️CD

Naming
clashes

/offline

/-/offline

Request
types

self.addEventListener('fetch', ({request}) => {
  if (
    request.method === 'GET' &&
    request.mode === 'navigate'
  ) {
    event.respondWith(
      fetch(request).catch(() => (
        caches.match('/-/offline')
      ))
    )
  }})

Fetch

./service_worker.js

CI⚡️CD

CI⚡️CD

✅ Basic PWA Support

✅ Vulnerability Caching

June 22nd 2019

v12.0

Thanks!

Sam Beckham
@samdbeckham

Making GitLab a PWA

By Sam Beckham

Making GitLab a PWA

  • 1,234