Optimising Web application frontend Tomáš Kramár, @tkramar
What happens when I type address into browser?
GET /index.html GET /index.html Browser Server
GET /index.html index.html GET /assets/favicon.ico favicon.ico Browser Server
GET /index.html index.html GET /assets/favicon.ico favicon.ico Browser Server GET /assets/application.css application.css
GET /index.html index.html GET /assets/favicon.ico favicon.ico Browser Server GET /assets/application.css application.css GET /assets/bg.png bg.png
GET /index.html index.html Backend GET /assets/favicon.ico favicon.ico Browser Server GET /assets/application.css application.css GET /assets/bg.png bg.png
Page Load Time = Backend Time + Frontend Time
Optimisation rule #1 Optimise only when it makes sense
Optimisation rule #1 Optimise only when it makes sense * http://www.stevesouders.com/blog/2012/02/10/the-performance-golden-rule/
Waterfall / Firebug
Demo
Sprechen Sie Firebug? ● Blocking - request is queued and waiting ● DNS Lookup - time to resolve hostname ● Connection - time to create TCP connection ● Sending - sending request headers ● Waiting - backend is busy now ● Receiving - reading the response ● blue and red vertical lines: DOMContentLoaded and load events ○ http://ie.microsoft. com/testdrive/HTML5/DOMContentLoaded/Default. html
Optimisation rule #2 Download resources in parallel
Resource downloading rules Clients that use persistent connections SHOULD limit the number of simultaneous connections that they maintain to a given server. A single-user client SHOULD NOT maintain more than 2 connections with any server or proxy. -- RFC 2616 (HTTP 1.1)
Demo
Solution: Asset subdomains ● assets01.domain.com ● assets02.domain.com ● ... ● Extra: ○ Cookie-less domain ○ HTTP Keep-Alive ● Pitfalls: ○ DNS lookups
Optimisation rule #3 Fastest request is the one that doesn't happen
Avada Kedavra Request! ● Merge ● Inline ● Sprite ● Cache
Merge ● Merge multiple CSS files into a single file ● Merge multiple JS files into a single file ○ -> Rails/Sprockets/Asset pipeline $ cat app/assets/javascripts/application.js //= require jquery_ujs //= require_tree ./vendor //= require document_viewer/dv //= require_tree .
Inline ● Inline JavaScript ○ Replace <script src=".."></script> with <script>//code</script> ● Inline CSS ○ <style>body { color: red; }</style> ● Usable only for small resources, larger resources benefit more from caching ● Inline images using data URIs ○ <img src='data:image/jpeg;base64, /9j/4AAQSkZJR'/> ○ background-image: url(data:image/jpeg;base64, /9j/4AAQS); ○ Pitfalls: size limit, IE <= 7
Demo
CSS sprites Merge multiple images into one, use background- position to place.
Caching First request: cache miss, hit the server, obtain token Additional requests: use the token from first request to make conditional request Conditional requests ● If-None-Match ● If-Modified-Since
If-None-Match: First request First request: Request headers: GET / Response headers: Status: 200 OK Etag: "be5c5a3edac0592617693fa..."
If-None-Match: Next requests Request headers: GET / If-None-Match: "be5c5a3edac0592617693fa..." Response headers: Status: 304 Not Modified Etag: "be5c5a3edac0592617693fa..."
If-None-Match ● Server needs to calculate ETag (fingerprint) ● How? ○ it depends ○ easiest way: generate output, calculate hash ● If ETag matches, send 304 Not Modified, no response body ● You save on the data transfer ("Receiving" in Firebug)
If-Modified-Since: First request Request headers: GET / Response headers: Status: 200 OK Expires: Thu, 31 Dec 2037 23:55:55 GMT Last-Modified: Mon, 30 Jan 2012 13:36:26 GMT
If-Modified-Since: Local cache Request headers: none Response headers: none Current time is < resource's Expire header
If-Modified-Since: Forced refresh Request headers: GET / If-Modified-Since: Mon, 30 Jan 2012 13:36:26 GMT Response headers: Status: 304 Not Modified Expires: Thu, 31 Dec 2037 23:55:55 GMT Last-Modified: Mon, 30 Jan 2012 13:36:26 GMT
Refresh Regular click on a link: uses local cache and no request is made F5: Skips local cache, sends If-Modified-Since request, potentially 304-ing Ctrl+F5: Sends requests without If-None- Match and If-Modified-Since
Demo
Far future expires strategy Set the Expires header far in the future FAQ Q: But what if I need to change the resource? A: Use a different name application-f7fd224c9bc0fd4c2f7.css fingerprint
Rails gotcha Asset pipeline sets fingerprints, but not Expire headers. You need to DIY. Nginx: location ~* .(js|css|jpg|jpeg|gif|png|swf|ico)$ { expires max; }
Optimisation rule #3 If you need to make the request, make the response (and request) small.
Follow these rules ● Minify and gzip CSS and JavaScript. ● Do not send/set cookies unless necessary (asset subdomains) ● Do not scale images in HTML ○ do not use <img size=""/>, scale image on server ● Optimize images ○ http://www.smushit.com/ysmush.it/
Optimisation rule #4 If possible, defer parsing of JavaScripts
How browsers process JavaScript When browsers encounter <script> tag, the script is downloaded, parsed and executed. Main parsing thread is busy processing the script. Speculative parsing continues downloading and building the DOM in the background, so main thread can pull this fast, but you still pay the parse and execute penalty.
Do you need to execute the script immediately? Probably not. Is your code waiting for document.ready? Mouse action? Then definitely not.
Then defer it <script type="text/javascript"> function downloadJSAtOnload() { var element = document.createElement("script"); element.src = "application.js"; document.body.appendChild(element); } if (window.addEventListener) window.addEventListener("load", downloadJSAtOnload, false); else if (window.attachEvent) window.attachEvent("onload", downloadJSAtOnload); else window.onload = downloadJSAtOnload; </script>
Demo
Optimisation rule #5 As a last resort: simply cheat.
Perceived speed matters Move all CSS to the top so DOM nodes can be rendered progressively If you cannot defer, at least move <script> to the bottom of the page. Post-load parts of page that are slow to render.
Optimisation rule #5 Think globally.
Location matters Downloading image placed on a server in Slovakia is relatively cheap than say downloading the same image from Canada.
Use CDN (content delivery network) Servers spread through the world. Use CDN on common assets (jQuery etc.). Chance is, you will get a cache hit. https://developers.google.com/speed/libraries/
Tools Web Page Test, http://www.webpagetest.org/ Newrelic RUM, http://newrelic.com
Is this all I can do? Definitely not. Google Page Speed, http://developers.google. com/speed/pagespeed/ YSlow, http://yslow.org/
Future SPDY HTTP archives local storage
Summary 1. Optimise only when it makes sense 2. Download resources in parallel 3. Fastest request is the one that doesn't happen 4. If you need to make the request, make the response (and request) small 5. If possible, defer parsing of JavaScripts 6. Think globally

Optimising Web Application Frontend

  • 1.
    Optimising Web application frontend Tomáš Kramár, @tkramar
  • 2.
    What happens whenI type address into browser?
  • 3.
    GET /index.html GET /index.html Browser Server
  • 4.
    GET /index.html index.html GET /assets/favicon.ico favicon.ico Browser Server
  • 5.
    GET /index.html index.html GET /assets/favicon.ico favicon.ico Browser Server GET /assets/application.css application.css
  • 6.
    GET /index.html index.html GET /assets/favicon.ico favicon.ico Browser Server GET /assets/application.css application.css GET /assets/bg.png bg.png
  • 7.
    GET /index.html index.html Backend GET /assets/favicon.ico favicon.ico Browser Server GET /assets/application.css application.css GET /assets/bg.png bg.png
  • 8.
    Page Load Time = Backend Time + Frontend Time
  • 9.
    Optimisation rule #1 Optimiseonly when it makes sense
  • 10.
    Optimisation rule #1 Optimiseonly when it makes sense * http://www.stevesouders.com/blog/2012/02/10/the-performance-golden-rule/
  • 11.
  • 12.
  • 13.
    Sprechen Sie Firebug? ● Blocking - request is queued and waiting ● DNS Lookup - time to resolve hostname ● Connection - time to create TCP connection ● Sending - sending request headers ● Waiting - backend is busy now ● Receiving - reading the response ● blue and red vertical lines: DOMContentLoaded and load events ○ http://ie.microsoft. com/testdrive/HTML5/DOMContentLoaded/Default. html
  • 14.
    Optimisation rule #2 Downloadresources in parallel
  • 15.
    Resource downloading rules Clientsthat use persistent connections SHOULD limit the number of simultaneous connections that they maintain to a given server. A single-user client SHOULD NOT maintain more than 2 connections with any server or proxy. -- RFC 2616 (HTTP 1.1)
  • 16.
  • 17.
    Solution: Asset subdomains ●assets01.domain.com ● assets02.domain.com ● ... ● Extra: ○ Cookie-less domain ○ HTTP Keep-Alive ● Pitfalls: ○ DNS lookups
  • 18.
    Optimisation rule #3 Fastestrequest is the one that doesn't happen
  • 19.
    Avada Kedavra Request! ● Merge ● Inline ● Sprite ● Cache
  • 20.
    Merge ● Merge multipleCSS files into a single file ● Merge multiple JS files into a single file ○ -> Rails/Sprockets/Asset pipeline $ cat app/assets/javascripts/application.js //= require jquery_ujs //= require_tree ./vendor //= require document_viewer/dv //= require_tree .
  • 21.
    Inline ● Inline JavaScript ○ Replace <script src=".."></script> with <script>//code</script> ● Inline CSS ○ <style>body { color: red; }</style> ● Usable only for small resources, larger resources benefit more from caching ● Inline images using data URIs ○ <img src='data:image/jpeg;base64, /9j/4AAQSkZJR'/> ○ background-image: url(data:image/jpeg;base64, /9j/4AAQS); ○ Pitfalls: size limit, IE <= 7
  • 22.
  • 23.
    CSS sprites Merge multipleimages into one, use background- position to place.
  • 24.
    Caching First request: cachemiss, hit the server, obtain token Additional requests: use the token from first request to make conditional request Conditional requests ● If-None-Match ● If-Modified-Since
  • 25.
    If-None-Match: First request Firstrequest: Request headers: GET / Response headers: Status: 200 OK Etag: "be5c5a3edac0592617693fa..."
  • 26.
    If-None-Match: Next requests Requestheaders: GET / If-None-Match: "be5c5a3edac0592617693fa..." Response headers: Status: 304 Not Modified Etag: "be5c5a3edac0592617693fa..."
  • 27.
    If-None-Match ● Server needsto calculate ETag (fingerprint) ● How? ○ it depends ○ easiest way: generate output, calculate hash ● If ETag matches, send 304 Not Modified, no response body ● You save on the data transfer ("Receiving" in Firebug)
  • 28.
    If-Modified-Since: First request Requestheaders: GET / Response headers: Status: 200 OK Expires: Thu, 31 Dec 2037 23:55:55 GMT Last-Modified: Mon, 30 Jan 2012 13:36:26 GMT
  • 29.
    If-Modified-Since: Local cache Requestheaders: none Response headers: none Current time is < resource's Expire header
  • 30.
    If-Modified-Since: Forced refresh Requestheaders: GET / If-Modified-Since: Mon, 30 Jan 2012 13:36:26 GMT Response headers: Status: 304 Not Modified Expires: Thu, 31 Dec 2037 23:55:55 GMT Last-Modified: Mon, 30 Jan 2012 13:36:26 GMT
  • 31.
    Refresh Regular click ona link: uses local cache and no request is made F5: Skips local cache, sends If-Modified-Since request, potentially 304-ing Ctrl+F5: Sends requests without If-None- Match and If-Modified-Since
  • 32.
  • 33.
    Far future expiresstrategy Set the Expires header far in the future FAQ Q: But what if I need to change the resource? A: Use a different name application-f7fd224c9bc0fd4c2f7.css fingerprint
  • 34.
    Rails gotcha Asset pipelinesets fingerprints, but not Expire headers. You need to DIY. Nginx: location ~* .(js|css|jpg|jpeg|gif|png|swf|ico)$ { expires max; }
  • 35.
    Optimisation rule #3 Ifyou need to make the request, make the response (and request) small.
  • 36.
    Follow these rules ●Minify and gzip CSS and JavaScript. ● Do not send/set cookies unless necessary (asset subdomains) ● Do not scale images in HTML ○ do not use <img size=""/>, scale image on server ● Optimize images ○ http://www.smushit.com/ysmush.it/
  • 37.
    Optimisation rule #4 Ifpossible, defer parsing of JavaScripts
  • 38.
    How browsers processJavaScript When browsers encounter <script> tag, the script is downloaded, parsed and executed. Main parsing thread is busy processing the script. Speculative parsing continues downloading and building the DOM in the background, so main thread can pull this fast, but you still pay the parse and execute penalty.
  • 39.
    Do you needto execute the script immediately? Probably not. Is your code waiting for document.ready? Mouse action? Then definitely not.
  • 40.
    Then defer it <scripttype="text/javascript"> function downloadJSAtOnload() { var element = document.createElement("script"); element.src = "application.js"; document.body.appendChild(element); } if (window.addEventListener) window.addEventListener("load", downloadJSAtOnload, false); else if (window.attachEvent) window.attachEvent("onload", downloadJSAtOnload); else window.onload = downloadJSAtOnload; </script>
  • 41.
  • 42.
    Optimisation rule #5 Asa last resort: simply cheat.
  • 43.
    Perceived speed matters Moveall CSS to the top so DOM nodes can be rendered progressively If you cannot defer, at least move <script> to the bottom of the page. Post-load parts of page that are slow to render.
  • 44.
  • 45.
    Location matters Downloading imageplaced on a server in Slovakia is relatively cheap than say downloading the same image from Canada.
  • 46.
    Use CDN (contentdelivery network) Servers spread through the world. Use CDN on common assets (jQuery etc.). Chance is, you will get a cache hit. https://developers.google.com/speed/libraries/
  • 47.
    Tools Web Page Test,http://www.webpagetest.org/ Newrelic RUM, http://newrelic.com
  • 52.
    Is this allI can do? Definitely not. Google Page Speed, http://developers.google. com/speed/pagespeed/ YSlow, http://yslow.org/
  • 54.
  • 55.
    Summary 1. Optimise onlywhen it makes sense 2. Download resources in parallel 3. Fastest request is the one that doesn't happen 4. If you need to make the request, make the response (and request) small 5. If possible, defer parsing of JavaScripts 6. Think globally