Extreme Web Performance Optimization
Who Are We?
- we build web apps
- we build web developers
- 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>
- html starts parsing
- hits <script>
- html pauses parsing
- download js (if external)
- execute js (blocks)
- html continues parsing
<script defer>
- html starts parsing
- hits <script>
- html continues parsing
- while downloading js (if external)
- html finishes parsing
- deferred js executes in order
<script async>
- html starts parsing
- hits <script>
- html continues parsing
- while downloading js (if external)
- html pauses parsing to execute js
- 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/
- Prepare the low quality images (server-side)
- Load the low quality images (client-side)
- 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