class: center, middle, title-slide # Ready Set Go ## Loading Single Page Apps Fast ### Jon Johnson .boxes[ .icons[
jrjohnson
jrjohnson_
] .avatar[  ] ] Slides at: https://jrjohnson.dev/talks/2018-10-ready-set-go-sflive.html --- class: center,middle,loading
Ilios Logo
Ilios
??? #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 --- class: center,middle #Our Problem  ??? - 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 --- class: center,logos #Our Solution   ??? - 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 ... --- class: center,impact-boxes #Our Solution .left-column[ ##
## REST API ] .right-column[ ##
## 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. .left[https://en.wikipedia.org/wiki/Single-page_application] --- class: middle,center
--- class: high-contrast # 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**. --- .grey[ # 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**. ] .center[ ## 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 --- class: center, middle # Structure of a Single Page App --- # Ember.js (using ember-cli) ```html
EmberDemo
``` --- # React (using create-react-app) ```html
React App
You need to enable JavaScript to run this app.
``` --- # Vue.js (with @vue/cli) ```html
Vue Demo
We're sorry but Vue Demo doesn't work properly without JavaScript enabled. Please enable it to continue.
``` --- # Angular (with @angular/cli) ```html
AngularDemo
``` --- # Webpack Encore ```html
``` --- class: center,middle # Default User Experience ### There is a load cost for single page apps and you pay it upfront. --- class: center, middle  --- class: center, middle # 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. --- class: center,middle # No one should feel like they're wasting their time --- class: center,middle,photo-slide  ??? # At the end of the day we have to keep our users in mind --- class: center, middle # What can we do about it? --- class: center, middle # First Fix Everything Else! ##
--- class: blackfire-logo-list # @todo ##
Get up to date. Symfony 4 and Flex Are Fast AF ##
Use DQL and DTOs to fetch data and save memory -- .whitespace[]  .whitespace[] .whitespace[] .whitespace[] -- .center[ ### When you're got all of that then think about loading ] --- .center[ # Consider loading in two parts ] .left-column[ ## First Time Visitor ### Empty Cache ### First Impression ### Really Paying Attention ### Has No Understanding of Your Value
] -- .right-column[ ## The Second Visit (and beyond) ### Populated Cache ### Your Biggest Fans ### Pays the Cost Every Visit ### Willing to Wait (but shouldn't have to)
] --- class: center, middle # 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. --- class: center # The First Visit .boxes[ ##
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 -- ## Count those bytes ??? - I started building websites in 1995 1hen 10K used to be our limit. For index.html it's worth targeting that again. -- ## What can be loaded later? ??? - 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 -- ## Take advantage of code splitting ??? - 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. -- ## Compression matters ??? - 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. -- .whitespace[] .whitespace[] .whitespace[] .center[ ###
In trying to save milliseconds. Kilobytes matter. ] --- class: center,middle #
Send Fast ## HTTP/2 PUSH #### Change ```
``` #### To ```
``` ### Thanks WebLink! ??? Also worth considering a CDN for some assets if your users are not necessarily located near the server. --- class: center,middle #
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 --- class: center,middle,pulse-loader ## Watch Out for Metaphors
--- class: center,middle,heart-attack
##
Why is Ilios having a heart attack?
--- class: center,middle # Order Matters ## or... #&$%! why do I still see a white screen?? ###
--- # A nice starting place ```html
``` --- # Add A Distraction ```html
We'll be right with you... Promise!
``` --- class: center,middle # Browser Rendering Flow  .footnote[ https://www.html5rocks.com/en/tutorials/internals/howbrowserswork/#The_main_flow ] --- # What this means for us ```html
We'll be right with you... Promise!
``` --- # Don't wait to style the loader ```html
We'll be right with you... Promise!
``` ### CSS Needs to block the app, but nothing else ??? - Link with `rel="stylesheet"` can be in the body https://html.spec.whatwg.org/multipage/links.html#body-ok --- # Javascript should load, not wait ```html
We'll be right with you... Promise!
``` ###
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 ```html
This Site Requires Javascript!
``` ??? - Noscript tag can be styled - Make sure it sits on top of your loader or it may never be visible --- # Handle Parse Errors ```html
and we're spinning...
Whoops!
``` ```javascript 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! ```php $response = new Response(); $response->setMaxAge(60 * 60 * 24 * 365); // Cache-Control: max-age=31536000, public ``` -- ## or require validation every time ```php $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. --- class: center,middle #
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 ```javascript 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 -- ### 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 -- ### Even after you replace a worker it will still respond to the very next request -- ### Syntax is.... um...
--- # The "kill switch" Service Worker ```javascript 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 --- class: center,middle # 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 --- class: center,middle # 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.
.footnote[https://developers.google.com/web/progressive-web-apps/] --- class: center,middle,photo-slide  --- class: center,middle,photo-slide  --- # @todo PWA ##
Service Worker ##
HTTPS ##
Web App Manifest ##
Deep Links ??? - 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. --- class: center,middle # The end of Native Apps? --- class: center, middle, title-slide ## Discussion / Questions? .boxes[ .icons[
jrjohnson
jrjohnson_
] .avatar[  ] ] Slides at: https://jrjohnson.dev/talks/2018-10-ready-set-go-sflive.html ??? # Thanks - Symfony for loving PHP and our sponsors! - UCSF Library - Ilios Team # Ask me three questions and then we can go to lunch! (from https://twitter.com/MrsSasser/status/1034118861354950656?s=19)