Extreme Web Performance Optimization

Who Are We?

  1. we build web apps
  2. we build web developers
  3. we go out to work with others
    to get better at 1 and 2

Disclaimer

Young in the ways of WPO

How important is WPO?

Conversions

The reason why we build stuff

store => orders

blog => read articles

app => interact & engage

40%

of users abandon an article if it doesn't load after six seconds

Danny Bernstein, Google

In terms we can understand

load_time > 6000 && 
Math.random() < .4 ?
fail() : profit()

500ms delay

+26% user’s frustration

Source: Radware

In terms we can understand

while( wait_time % 500 )

    (╯°□°)╯︵ ┻━┻ 

500ms delay

-20% Google’s traffic

http://glinden.blogspot.com/2006/11/marissa-mayer-at-web-20.html

+100ms delay

-1% Amazon’s sales

http://glinden.blogspot.com/2006/11/marissa-mayer-at-web-20.html

$1070000000

2015

$1,070,000,000

2015

-2.2s page load

+15% downloads

Source: Firefox

+60% faster

+14% donations

Source: Obama Campaign

- The Google

we’ve decided to take site speed into account in our search rankings

https://webmasters.googleblog.com/2010/04/using-site-speed-in-web-search-ranking.html

My sites are fast enough...

- no one ever

What even is page speed?

Page Load Time

TTFB

TTG

https://blog.cloudflare.com/ttfb-time-to-first-byte-considered-meaningles

how long it takes for a webpage to be loaded and usable by a browser

https://www.littlebizzy.com/blog/ttfb-meaningless

ATF

User satisfaction metrics are based on Perception, ATF

First Tool

www.webpagetest.org

Waterfall charts and HAR

Filmstrip View

Moar Tools

Visual Progress chart

Where to optimize?

Optimizing
on the server

50% optimization

~ 10% of final impact

It's not a simple task

Optimizing
on the client

50% optimization

~ 45% of final impact

is simple

immediate results

Perception

vs.

Performance

Performance

Perception

Performance

Perception

Usability Engineering 101

Delay User reaction
0 - 100 ms Instant
100 - 300 ms Feels sluggish
300 - 1000 ms Acceptable for changing views
> 1 s Mental context switch
> 10 s Frustrated, leave,
may or may not return

https://developers.google.com/web/tools/chrome-devtools/profile/evaluate-performance/rail

RAIL

https://developers.google.com/web/tools/chrome-devtools/profile/evaluate-performance/rail

https://developers.google.com/web/tools/chrome-devtools/profile/evaluate-performance/rail

  • Respond to user input within 100ms
  • Else
    • connection between action and reaction is broken, "laggy"
    • includes click, tap, scroll, swipe
  • Do not block the user, do expensive work in the background (if possible)

https://developers.google.com/web/fundamentals/performance/rendering/

  • Render frames every 16ms
  • Includes scrolling, swiping
  • Every frame must
    JS > Style > Layout > Paint > Composite
  • Each frame has 16.66ms
  • You really have 10ms
  • Within the 100ms time, precalculate if you can

https://developers.google.com/web/tools/chrome-devtools/profile/evaluate-performance/rail

  • Maximize idle time
  • Use idle time to complete deferred work
  • Load BTF content
  • Group deferred work % 50 ms
    • If user interacts, response is > priority
    • Ensure instant response

https://developers.google.com/web/tools/chrome-devtools/profile/evaluate-performance/rail

  • Load ATF in < 1 second
  • Else
    • attention loss
    • broken context
  • Optimize the critical rendering path
    • unblock rendering

#WebPerf

optimize-fu

Rule #1: User First

If it's a "Dirty Hack"

and it makes UX better   

Do it!

Don't make it so pretty that it doesn't work for the end user

Devs must remember...

Do not block the critical rendering path

...what happens in the intermediate steps between receiving the HTML, CSS, and JavaScript bytes and the required processing to turn them into rendered pixels

Allow progressive rendering

load html > load resources > parse > display

https://developers.google.com/web/fundamentals/performance/critical-rendering-path

https://developers.google.com/web/fundamentals/performance/critical-rendering-path/analyzing-crp

https://developers.google.com/web/fundamentals/performance/critical-rendering-path/optimizing-critical-rendering-path

https://varvy.com/pagespeed/critical-render-path.html

Web fonts turn

 

Into a

Non-Blocking text

Blocking Resource

https://css-tricks.com/fout-foit-foft

JS blocks parsing HTML

Worse than blocking rendering

</body>

<script>

  1. html starts parsing
  2. hits <script>
  3. html pauses parsing
  4. download js (if external)
  5. execute js (blocks)
  6. html continues parsing

<script defer>

  1. html starts parsing
  2. hits <script>
  3. html continues parsing
  4. while downloading js (if external)
  5. html finishes parsing
  6. deferred js executes in order

<script async>

  1. html starts parsing
  2. hits <script>
  3. html continues parsing
  4. while downloading js (if external)
  5. html pauses parsing to execute js
  6. html completes parsing

Use <script async>

if it doesn't depend on script order

else use <script defer>

http://www.growingwiththeweb.com/2014/02/async-vs-defer-attributes.html

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

Stop using onload

use DOMContentLoaded

https://developer.mozilla.org/en-US/docs/Web/Events/DOMContentLoaded

Inline CSS

Renders faster

<div style="color:salmon">
require('juice')
require('inline-css')

Critical CSS

Extract and inline critical path css

critical.generate({
    base: 'test/',
    src: 'index.html',
    dest: 'styles/styles.min.css',
    minify: true,
    width: 1300,
    height: 900
});

https://github.com/addyosmani/critical

Responsive Images

<picture>

<picture>
  <source media="(min-width: 40em)"
    srcset="logo.large.png 1x, logo.large.2x.png 2x">
  <source 
    srcset="logo.small.png 1x, logo.small.2x.png 2x">
  <img src="logo.png" alt="DevLeague">
</picture>

https://responsiveimages.org

Responsive Images

<img srcset sizes>

<img src="logo.small.png"
     srcset="logo.large.png 1024w, logo.medium.png 640w, logo.small.png 320w"
     sizes="(min-width: 36em) 33.3vw, 100vw"
     alt="DevLeague">

https://responsiveimages.org

Intrinsic Placeholders

<picture> + intrinsic ratios

<picture class="intrinsic intrinsic--square">
  <source media="(min-width: 500px)" srcset="logo.large.png">
  <img class="intrinsic-item" srcset="logo.small.png" alt="">
</picture>

http://daverupert.com/2015/12/intrinsic-placeholders-with-picture

LQIP

low quality image placeholders

http://www.guypo.com/introducing-lqip-low-quality-image-placeholders/

  1. Prepare the low quality images (server-side)
  2. Load the low quality images (client-side)
  3. Load the high quality images (client-side)

Progressive loading

Blur effect

<figure>
  <div>
    <div/> <!-- this div keeps the aspect ratio so the placeholder doesn't collapse -->
    <img/> <!-- this is a tiny image with a resolution of e.g. ~27x17 and low quality -->
    <canvas/> <!-- takes the above image and applies a blur filter -->
    <img/> <!-- the large image to be displayed -->
    <noscript/> <!-- fallback for no JS -->
  </div>
</figure>

https://jmperezperez.com/medium-image-progressive-loading-placeholder/

Facebook's 200 Byte

Preview Photos

https://code.facebook.com/posts/991252547593574/the-technology-behind-preview-photos/

this helped speed up profile and page loads by 30 percent

https://code.facebook.com/posts/991252547593574/the-technology-behind-preview-photos/

Cache Policy should be set by the Content Decider

Not DevOps

proxy_cache_path /var/lib/nginx/cache  levels=1:2 keys_zone=backcache:8m max_size=50m;
proxy_cache_key "$scheme$request_method$host$request_uri$is_args$args";
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;

location / {
    proxy_cache backcache;
    proxy_cache_bypass  $http_cache_control;
    add_header X-Proxy-Cache $upstream_cache_status;
...

- from the Google webmaster guidelines

Make sure that your web server correctly supports the

If-Modified-Since HTTP header.

The If-Modified-Since header is sent to a server as a conditional request.

if ( contentChanged )
  respond.status(200).send(content);
else
  respond.status(304);

https://varvy.com/ifmodified.html

Use Service Workers as a proxy on the client

Create your own Cache Policy

http://www.html5rocks.com/en/tutorials/service-worker/introduction/

Register a serviceWorker

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('/sw.js')
    .then(function(registration) {
      console.log(
        'ServiceWorker registered with scope: ',
        registration.scope
      );
  }).catch(function(err) {
    console.log(
      'ServiceWorker registration failed: ',
      err
    );
  });
}

chrome://inspect/#service-workers

Open Cache, Cache urls

var CACHE_NAME = 'my-app-cache-v1';
var urlsToCache = [
  '/',
  '/css/bundle.css',
  '/js/bundle.js'
];

self.addEventListener('install', function(event) {
  // Perform install steps
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        console.log('Opened cache');
        return cache.addAll(urlsToCache);
      })
  );
});

Cache Hit, or return Request

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request)
      .then(function(response) {
        // Cache hit - return response
        if (response) {
          return response;
        }

        return fetch(event.request);
      }
    )
  );
});

There's a proxy in your browser

https://docs.google.com/presentation/d/1GNLc4oRZzazq4Th8vsH3v5GekAbKWsxIXHbNtQFFG-c/present

Set static content to expire late in the future

<img src="logo.png" />
<img src="logo.242.png" />
<img src="logo.png#20160831" />

gzip all the plaintext

gzip            on;
gzip_min_length 1000;
gzip_proxied    expired 
                no-cache 
                no-store 
                private 
                auth;
gzip_types      text/plain 
                application/xml
                application/json;
Accept-Encoding: gzip, deflate
Content-Encoding: gzip

zopfli all the plaintext

# Enable Nginx HttpGzipStaticModule
server {
  ...
  gzip_static on;
}
Accept-Encoding: gzip, deflate
Content-Encoding: gzip
  • compression only
  • use existing methods to decompress
  • good for (pre) compressing once
  • very slow, not good for on the fly compression
  • uncompressed: 106,272 bytes

  • mod_deflate: 28,732 bytes (27%)

  • gzip -9: 28,062 bytes (26%)

  • Zopfli: 27,033 bytes (25%)

https://github.com/google/zopfli
http://www.cambus.net/serving-precompressed-content-with-nginx-and-zopfli/
https://www.stevesouders.com/blog/2013/03/08/zopflinator/

brotli all the plaintext

server {
  ...
  brotli on;
  brotli_static on;
  brotli_types  text/plain application/xml application/json;
}
Accept-Encoding: brotli

https://github.com/google/ngx_brotli
https://quixdb.github.io/squash-benchmark/

ngx_brotli filter module - on the fly

ngx_brotli static module - precompressed

comp level comp speed decomp speed
zlib deflate 4 21.45 MiB/s 96.16 MiB/s
zlib gzip 4 20.71 MiB/s 89.21 MiB/s
brotli 1 23.94 MiB/s 115.02 MiB/s

What about images?

png, lossless, everyone uses it

FLIF

http://flif.info

  • 14% smaller than lossless WebP,
  • 22% smaller than lossless BPG,
  • 33% smaller than brute-force crushed PNG files (using ZopfliPNG),
  • 43% smaller than typical PNG files,
  • 46% smaller than optimized Adam7-interlaced PNG files,
  • 53% smaller than lossless JPEG 2000 compression,
  • 74% smaller than lossless JPEG XR compression.

Browser opens

6 lanes per origin

exploit that

https://www.devleague.com/css/bundle.css

origin

:443

https://www.devleague.com/css/bundle.css

https://www.devleague.com/js/bundle.js

https://www.devleague.com/images/asset1.png

https://www.devleague.com/images/asset2.png

https://www.devleague.com/images/asset3.png

https://www.devleague.com/images/asset4.png

https://www.devleague.com/images/asset5.png

https://www.devleague.com/images/asset6.png

https://www.devleague.com/images/asset7.png

https://www.devleague.com/images/asset8.png

https://www.devleague.com/images/asset9.png

https://www.devleague.com/images/asset10.png

https://www.devleague.com/images/asset11.png

https://www.devleague.com/images/asset12.png

https://www.devleague.com/images/asset13.png

https://www.devleague.com/images/asset14.png

https://www.devleague.com/images/asset15.png

https://www.devleague.com/images/asset16.png

Waiting...

Loading...

https://cdn1.devleague.com/css/bundle.css

origin

:443

https://cdn1.devleague.com/css/bundle.css

https://cdn1.devleague.com/js/bundle.js

https://cdn1.devleague.com/images/asset1.png

https://cdn1.devleague.com/images/asset2.png

https://cdn1.devleague.com/images/asset3.png

https://cdn1.devleague.com/images/asset4.png

https://cdn2.devleague.com/images/asset5.png

https://cdn2.devleague.com/images/asset6.png

https://cdn2.devleague.com/images/asset7.png

https://cdn2.devleague.com/images/asset8.png

https://cdn2.devleague.com/images/asset9.png

https://cdn2.devleague.com/images/asset10.png

https://cdn3.devleague.com/images/asset11.png

https://cdn3.devleague.com/images/asset12.png

https://cdn3.devleague.com/images/asset13.png

https://cdn3.devleague.com/images/asset14.png

https://cdn3.devleague.com/images/asset15.png

https://cdn3.devleague.com/images/asset16.png

Loading...

Loading...

Loading...

Domain Sharding

Use server side rendering for first view

then js to load the rest

avoid client side rendering

page load performance decrease 5x

https://facebook.github.io/react/docs/environments.html

const React = require('react');
const ReactDOMServer = require('react-dom/server');

...

app.get('/', (req, res) => {
  const element = React.createElement('div', null, 'Hello World!');
  res.send(ReactDOMServer.renderToString(element));
});
function ngApp(req, res) {
  let baseUrl = '/';
  let url = req.originalUrl || '/';

  let config: ExpressEngineConfig = {
    directives: [ App ],
    platformProviders: [
      {provide: ORIGIN_URL, useValue: 'http://localhost:3000'},
      {provide: BASE_URL, useValue: baseUrl},
    ],
    providers: [
      {provide: REQUEST_URL, useValue: url},
      provideRouter(routes),
      NODE_LOCATION_PROVIDERS,
      NODE_HTTP_PROVIDERS,
    ],
    async: true,
    preboot: false
  };

  res.render('index', config);
}
app.get('/', ngApp);
app.get('/home', ngApp);
app.get('/about', ngApp);

https://universal.angular.io

Better Perceived Performance

https://universal.angular.io

Optimized for Search Engines

Site Preview

facebook, twitter, etc.

CPU Intensive JS

setTimeout( doSomeHeavyLifting, 0);

releases control from the main thread

pushes it to the end

Use a Worker

var worker = new Worker(‘heavy.js’);
worker.addEventListener(‘message’, workDone);

https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers

Responsive Web

is not the goal

It's just a tool

Responsive Web Design

for designers and developers

Responsive Web
does not have performance as a goal

People who say they want a Responsive Site

Actually want

mobile-friendly

Don't use
progress indicators

User perception: "This is slow"

Prebrowsing

get resources before they are needed

<link rel="prefetch" href="hero.2x.jpg">
<link rel="prerender" href="http://www.devleague.com">
<link rel="dns-prefetch" href="//devleague.com">

https://css-tricks.com/prefetching-preloading-prebrowsing/

https://www.stevesouders.com/blog/2013/11/07/prebrowsing/

AMP Project

<!doctype html>
<html ⚡>
  <head>
    <meta charset="utf-8">
    <link rel="canonical" href="hello-world.html" >
    <meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
    <style amp-boilerplate>
      body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;...
    </style>
    <noscript><style amp-boilerplate>
      body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}
    </style></noscript>
    <script async src="https://cdn.ampproject.org/v0.js"></script>
  </head>
  <body>Hello World!</body>
</html>

https://www.ampproject.org

HTTP/2

HTTP/2

  • Header Compression
  • TCP connection reuse
  • Push to Cache
  • Early tests show 15-60% load time improvement
Link: </css/style.css>; rel=preload;

https://www.cloudflare.com/http2/server-push

Web Performance Goals

all content 14k compressed

~70k budget, ATF

ATF rendered < 1s

Speed Index 1000-2000

https://sites.google.com/a/webpagetest.org/docs/using-webpagetest/metrics/speed-index

RAIL

... just the beginning

http://bit.do/devleague-wpo

Extreme Web Performance Optimization

By Jon Borgonia

Extreme Web Performance Optimization

  • 2,170