+ - 0:00:00
Notes for current slide
Notes for next slide

Our Story

  • Ilios is a 20 year old project which started as an MS Access Database
  • Built by UCSF, but available for free (and supported)
  • Installed at medical, dental, pharmacy, nursing, and even veterinary schools around the world
  • Been through several revisions since then, but in 2014 we decided to rewrite it

Ready Set Go

Loading Single Page Apps Fast

Jon Johnson

jrjohnson
jrjohnson_

Jon's avatar

Slides at: https://jrjohnson.dev/talks/2018-10-ready-set-go-sflive.html

Our Story

  • Ilios is a 20 year old project which started as an MS Access Database
  • Built by UCSF, but available for free (and supported)
  • Installed at medical, dental, pharmacy, nursing, and even veterinary schools around the world
  • Been through several revisions since then, but in 2014 we decided to rewrite it

Our Problem

ilios map

  • Ilios is an on premiss hosted solution
  • Most of our partners are non-technical staff
  • When supported by campus IT organizations updates are infrequent
  • We want to be able to ship UI updates very frequently so we separate the code base
  • Some of this is familiar territory, some of it won't be but I hope you can see where our answers will fit with your questions

Our Solution

symfony logo emberjs logo

  • You're at at Symfony conference so I'm guessing you know why symfony is great (and if you don't you soon will)
  • If you stop me later I'll be happy to sing Embers praises for you as well
  • The real key here for us though is ...

Our Solution

REST API

Single Page Application

  • Ship a PHP application that can be updated infrequently
  • The PHP app is responsible for loading the SPA but it's easy to update
  • Build and test both systems independently

What is a single page app?

A single-page application (SPA) is a web application or web site that fits on a single web page with the goal of providing a more fluid user experience akin to a desktop application. In a SPA, either all necessary code – HTML, JavaScript, and CSS – is retrieved with a single page load,[1] or the appropriate resources are dynamically loaded and added to the page as necessary, usually in response to user actions. The page does not reload at any point in the process, nor does control transfer to another page, although modern web technologies (such as those included in the HTML5 pushState() API) can provide the perception and navigability of separate logical pages in the application. Interaction with the single page application often involves dynamic communication with the web server behind the scenes.

https://en.wikipedia.org/wiki/Single-page_application

No. Seriously.

A single-page application (SPA) is a web application or web site that fits on a single web page with the goal of providing a more fluid user experience akin to a desktop application. In a SPA, either all necessary code – HTML, JavaScript, and CSS – is retrieved with a single page load, or the appropriate resources are dynamically loaded and added to the page as necessary, usually in response to user actions. The page does not reload at any point in the process, nor does control transfer to another page, although modern web technologies (such as those included in the HTML5 pushState() API) can provide the perception and navigability of separate logical pages in the application. Interaction with the single page application often involves dynamic communication with the web server behind the scenes.

No. Seriously.

A single-page application (SPA) is a web application or web site that fits on a single web page with the goal of providing a more fluid user experience akin to a desktop application. In a SPA, either all necessary code – HTML, JavaScript, and CSS – is retrieved with a single page load, or the appropriate resources are dynamically loaded and added to the page as necessary, usually in response to user actions. The page does not reload at any point in the process, nor does control transfer to another page, although modern web technologies (such as those included in the HTML5 pushState() API) can provide the perception and navigability of separate logical pages in the application. Interaction with the single page application often involves dynamic communication with the web server behind the scenes.

does not reload at any point

  • This isn't to say that you have to write your app in javascript
  • We work in a templating language called handlebars, SCSS style sheets, and modern JS interactions

Structure of a Single Page App

Ember.js (using ember-cli)

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>EmberDemo</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="ember-demo/config/environment" content="%7B%22modulePrefix%22%3A%22ember-demo%22%2C%22environment%22%3A%22development%22%2C%22rootURL%22%3A%22/%22%2C%22locationType%22%3A%22auto%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%7D%7D%2C%22APP%22%3A%7B%22name%22%3A%22ember-demo%22%2C%22version%22%3A%220.0.0+8c9fd3a5%22%7D%2C%22exportApplicationGlobal%22%3Atrue%7D" />
<link integrity="" rel="stylesheet" href="/assets/vendor.css">
<link integrity="" rel="stylesheet" href="/assets/ember-demo.css">
</head>
<body>
<script src="/assets/vendor.js"></script>
<script src="/assets/ember-demo.js"></script>
</body>
</html>

React (using create-react-app)

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<link rel="manifest" href="/manifest.json">
<link rel="shortcut icon" href="/favicon.ico">
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<script type="text/javascript" src="/static/js/bundle.js"></script></body>
</html>

Vue.js (with @vue/cli)

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="/favicon.ico">
<title>Vue Demo</title>
<link href="/app.js" rel="preload" as="script"></head>
<body>
<noscript>
<strong>We're sorry but Vue Demo doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script type="text/javascript" src="/app.js"></script></body>
</html>

Angular (with @angular/cli)

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>AngularDemo</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
<script type="text/javascript" src="runtime.js"></script>
<script type="text/javascript" src="polyfills.js"></script>
<script type="text/javascript" src="styles.js"></script>
<script type="text/javascript" src="vendor.js"></script>
<script type="text/javascript" src="main.js"></script></body>
</html>

Webpack Encore

<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/build/app.css">
</head>
<body>
<script src="/build/app.js"></script>
</body>
</html>

Default User Experience

There is a load cost for single page apps and you pay it upfront.

default single page app is a blank screen

Loading time stinks

  • For our faculty who work around the world we have to worry about:
    • airplane wifi latency and the round trip latency from working 20 hops away in China or Africa.
    • Even the speed of light can seem slow when it has to travel most of the way around the globe.

No one should feel like they're wasting their time

students getting their white coats

At the end of the day we have to keep our users in mind

What can we do about it?

First Fix Everything Else!

@todo

Get up to date. Symfony 4 and Flex Are Fast AF

Use DQL and DTOs to fetch data and save memory

@todo

Get up to date. Symfony 4 and Flex Are Fast AF

Use DQL and DTOs to fetch data and save memory

https://blackfire.io logo

@todo

Get up to date. Symfony 4 and Flex Are Fast AF

Use DQL and DTOs to fetch data and save memory

https://blackfire.io logo

When you're got all of that then think about loading

Consider loading in two parts

First Time Visitor

Empty Cache

First Impression

Really Paying Attention

Has No Understanding of Your Value

Consider loading in two parts

First Time Visitor

Empty Cache

First Impression

Really Paying Attention

Has No Understanding of Your Value

The Second Visit (and beyond)

Populated Cache

Your Biggest Fans

Pays the Cost Every Visit

Willing to Wait (but shouldn't have to)

The Second Visit

Entirely from Cache

The second visit is the most fun, we get to play so many fun tricks on the seconds visit. Cached content, service workers, some real magix to speed up the trip, But the first visit is where the Money is. When a dean is evaluating out product they might only spend 15 minutes clicking around. They're not coming back tomorrow. The first visit has to be smooth.

The First Visit

Send Less

Send Fast

Distract Users

Fill That Cache

Send Less

Server Side Rendered

  • Javascript is rendered on the server and only html is sent to the client.
  • Interaction follows a few (or many) seconds behind

Send Less

Server Side Rendered

Count those bytes

  • Javascript is rendered on the server and only html is sent to the client.
  • Interaction follows a few (or many) seconds behind
  • I started building websites in 1995 1hen 10K used to be our limit. For index.html it's worth targeting that again.

Send Less

Server Side Rendered

Count those bytes

What can be loaded later?

  • Javascript is rendered on the server and only html is sent to the client.
  • Interaction follows a few (or many) seconds behind
  • I started building websites in 1995 1hen 10K used to be our limit. For index.html it's worth targeting that again.
  • Many things are not needed on page load. We use an excellent library Papa Parse for processing CSV uploaded, but it's only needed in a few places. Instead of sending it right away we wait until it's needed and load it then.
  • Dropbox password strength zxcvbn 420kb compressed and minified

Send Less

Server Side Rendered

Count those bytes

What can be loaded later?

Take advantage of code splitting

  • Javascript is rendered on the server and only html is sent to the client.
  • Interaction follows a few (or many) seconds behind
  • I started building websites in 1995 1hen 10K used to be our limit. For index.html it's worth targeting that again.
  • Many things are not needed on page load. We use an excellent library Papa Parse for processing CSV uploaded, but it's only needed in a few places. Instead of sending it right away we wait until it's needed and load it then.
  • Dropbox password strength zxcvbn 420kb compressed and minified
  • Code splitting is a technique in many frameworks to statically analyze your code and only send what is needed to render the first page. This can drastically reduce the JS needed on first page load, but it must be tested thoroughly to ensure it doesn't leave users with a broken page when the follow a link deep into your app.

Send Less

Server Side Rendered

Count those bytes

What can be loaded later?

Take advantage of code splitting

Compression matters

  • Javascript is rendered on the server and only html is sent to the client.
  • Interaction follows a few (or many) seconds behind
  • I started building websites in 1995 1hen 10K used to be our limit. For index.html it's worth targeting that again.
  • Many things are not needed on page load. We use an excellent library Papa Parse for processing CSV uploaded, but it's only needed in a few places. Instead of sending it right away we wait until it's needed and load it then.
  • Dropbox password strength zxcvbn 420kb compressed and minified
  • Code splitting is a technique in many frameworks to statically analyze your code and only send what is needed to render the first page. This can drastically reduce the JS needed on first page load, but it must be tested thoroughly to ensure it doesn't leave users with a broken page when the follow a link deep into your app.
  • For sure make sure your assets are being sent gzipped, but if you're only supporting modern browsers consider a switch to brotli for even better compression and performance.

Send Less

Server Side Rendered

Count those bytes

What can be loaded later?

Take advantage of code splitting

Compression matters

In trying to save milliseconds. Kilobytes matter.

  • Javascript is rendered on the server and only html is sent to the client.
  • Interaction follows a few (or many) seconds behind
  • I started building websites in 1995 1hen 10K used to be our limit. For index.html it's worth targeting that again.
  • Many things are not needed on page load. We use an excellent library Papa Parse for processing CSV uploaded, but it's only needed in a few places. Instead of sending it right away we wait until it's needed and load it then.
  • Dropbox password strength zxcvbn 420kb compressed and minified
  • Code splitting is a technique in many frameworks to statically analyze your code and only send what is needed to render the first page. This can drastically reduce the JS needed on first page load, but it must be tested thoroughly to ensure it doesn't leave users with a broken page when the follow a link deep into your app.
  • For sure make sure your assets are being sent gzipped, but if you're only supporting modern browsers consider a switch to brotli for even better compression and performance.

Send Fast

HTTP/2 PUSH

Change

<link href="app.css">

To

<link href="{{ preload(asset('app.css'), { as: 'style' }) }}">

Also worth considering a CDN for some assets if your users are not necessarily located near the server.

Distract Users

  • Loading Animation
  • Beware pithy sayings
  • Remember that lots of stuff will lock up the browser
  • Animations should be simple and look good when frozen

Watch Out for Metaphors

Why is Ilios having a heart attack?

Order Matters

or... #&$%! why do I still see a white screen??

A nice starting place

<html>
<head>
<link rel="stylesheet" href="/assets/app.css">
</head>
<body>
<script src="/assets/framework.js"></script>
<script src="/assets/app.js"></script>
</body>
</html>

Add A Distraction

<html>
<head>
<link rel="stylesheet" href="/assets/app.css">
</head>
<body>
<!-- LOADING INDICATOR-->
<h1>We'll be right with you... Promise!</h1>
<!-- /LOADING INDICATOR-->
<script src="/assets/framework.js"></script>
<script src="/assets/app.js"></script>
</body>
</html>

What this means for us

<html>
<head>
<!-- WAIT --><link rel="stylesheet" href="/assets/app.css">
</head>
<body>
<!-- WAIT --><h1>We'll be right with you... Promise!</h1>
<!-- WAIT --><script src="/assets/framework.js"></script>
<!-- WAIT --><script src="/assets/app.js"></script>
</body>
</html>

Don't wait to style the loader

<html>
<head>
</head>
<body>
<style>
h1 {
color: orange;
font-size: 3rem;
}
</style><!-- WAIT -->
<!-- WAIT --><h1>We'll be right with you... Promise!</h1>
<!-- WAIT --><link rel="stylesheet" href="/assets/app.css">
<!-- WAIT --><script src="/assets/framework.js"></script>
<!-- WAIT --><script src="/assets/app.js"></script>
</body>
</html>

CSS Needs to block the app, but nothing else

Javascript should load, not wait

<body>
<style>
h1 {
color: orange;
font-size: 3rem;
}
</style>
<h1>We'll be right with you... Promise!</h1>
<!-- wait --><link rel="stylesheet" href="/assets/app.css">
<script defer src="/assets/framework.js"></script>
<script defer src="/assets/app.js"></script>
</body>

defer is set to indicate to a browser that the script is meant to be executed after the document has been parsed

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script

Our final structure sends the least possible amount of HTML and doesn't block the browser from rendering the loader at all.

Don't forget the noscript

<link rel="stylesheet" href="/assets/app.css">
<noscript>This Site Requires Javascript!</noscript>
  • Noscript tag can be styled
  • Make sure it sits on top of your loader or it may never be visible

Handle Parse Errors

<h1 id='loading-indicator'>and we're spinning...</h1>
<h1 class='hidden' id='loading-error'>Whoops!</h1>
window.addEventListener('error', function () {
var loadingIndicator = document.getElementById('loading-indicator');
if (loadingIndicator) {
loadingIndicator.parentNode.removeChild(loadingIndicator);
}
var errorContainer = document.getElementById('loading-error');
if (errorContainer) {
errorContainer.classList.remove('hidden');
}
});

Fill That Cache

Forever!

$response = new Response();
$response->setMaxAge(60 * 60 * 24 * 365);
// Cache-Control: max-age=31536000, public

Fill That Cache

Forever!

$response = new Response();
$response->setMaxAge(60 * 60 * 24 * 365);
// Cache-Control: max-age=31536000, public

or require validation every time

$response = new Response();
$response->setEtag(sha1($content));
$response->headers->addCacheControlDirective('no-cache');
$response->isNotModified($request);
// Cache-Control: no-cache, public
// ETag: "2bff8fd31f3a1f11275cd2cff55a0491e2e62da4"
  • With single page apps we get to play in the extremes
  • Assets that are fingerprinted can be cached forever. The fingerprint is the cache buster since it is based on the contents of the file.
  • no-cache doesn't mean don't cache. It means the client has to ask if their cached copy is OK.
  • Symfony can take the request, compare it to the response and send back an empty 304 if no changes have been made
  • I like to use symfony to serve my assets, if I was better at apache configuration maybe I would feel differently, but in an app we distribute and others configure I like building the best optimizations into the code itself.

Fill that cache (index.html edition)

Fill that cache (index.html edition)

Service Workers

  • Using a service worker to cache index.html
  • Show example of a simple worker that caches index.html

Service Workers

The Good Parts

Using a worker to cache index.html

self.addEventListener('fetch', event => {
event.respondWith(caches.match('/index.html').then(response => {
if (response) {
return response;
}
}));
});

Bonus: This works offline!

Service Workers

The Bad Parts

Service Workers

The Bad Parts

Can be accidentally cached for 24hrs

Foot gun protection: spec forces browsers to load it again every day just in case you mess it up

Service Workers

The Bad Parts

Can be accidentally cached for 24hrs

Even after you replace a worker it will still respond to the very next request

Foot gun protection: spec forces browsers to load it again every day just in case you mess it up

Service Workers

The Bad Parts

Can be accidentally cached for 24hrs

Even after you replace a worker it will still respond to the very next request

Syntax is.... um...

Foot gun protection: spec forces browsers to load it again every day just in case you mess it up

The "kill switch" Service Worker

self.addEventListener('install', () => {
// Activate this worker right now, removing other workers
self.skipWaiting();
});

This won't help you if you set bad cache headers when you send your service worker.

In that case users have to wait 24hrs or else shift-refresh

Are Service Workers Worth the Work?

YES!

  • Even on a fast network the initial latency of creating the connection can be noticeable
  • Plus they are the first step into the exciting world of making your app work offline

Bonus content

PWA

Progressive Web Apps are installable and live on the user's home screen, without the need for an app store. They offer an immersive full screen experience with help from a web app manifest file and can even re-engage users with web push notifications.

https://developers.google.com/web/progressive-web-apps/

prompt to install ilios as a pwa

pwa on Android home screen

@todo PWA

Service Worker

HTTPS

Web App Manifest

  • Hopefully you're already on HTTPS, if not check it out. It's SECURE!
  • Deep links are when every site in your app has a unique URL. Any modern framework will take care of this with a router.

The end of Native Apps?

Discussion / Questions?

jrjohnson
jrjohnson_

Jon's avatar

Slides at: https://jrjohnson.dev/talks/2018-10-ready-set-go-sflive.html

Thanks

Our Story

  • Ilios is a 20 year old project which started as an MS Access Database
  • Built by UCSF, but available for free (and supported)
  • Installed at medical, dental, pharmacy, nursing, and even veterinary schools around the world
  • Been through several revisions since then, but in 2014 we decided to rewrite it
Paused

Help

Keyboard shortcuts

, , Pg Up, k Go to previous slide
, , Pg Dn, Space, j Go to next slide
Home Go to first slide
End Go to last slide
Number + Return Go to specific slide
b / m / f Toggle blackout / mirrored / fullscreen mode
c Clone slideshow
p Toggle presenter mode
t Restart the presentation timer
?, h Toggle this help
Esc Back to slideshow