Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Performance on RAILs

E890a763587331f62cf2c39d38137b30?s=47 Paul Lewis
November 23, 2015

Performance on RAILs

This is my talk from Nordic.js and BeyondConf.

Nordic.js Video: https://www.youtube.com/watch?v=uJMA2n4RL6s

Looking at web performance advice can be overwhelming: everything comes with caveats, disclaimers, and sometimes one piece of advice can seem to actively contradict another.

Phrases like “the DOM is slow” or “always use CSS animations!” make for great headlines, but the truth is often far more nuanced.

In this session we'll look at how to think holistically about performance, and how to prioritise optimisation work that your users will notice and appreciate.

E890a763587331f62cf2c39d38137b30?s=128

Paul Lewis

November 23, 2015
Tweet

Transcript

  1. Performance on RAILs

  2. SUCCESS PERFORMANCE “Fast is good, faster is better” - every

    bit of perf research Painfully slow user quits Bearable user persists Smooth user delight @aerotwist
  3. SUCCESS PERFORMANCE Where is the happy middle? Smooth user delight

    Too slow Small ROI @aerotwist
  4. too slow. @aerotwist

  5. is 50ms too slow? @aerotwist

  6. Page Load

  7. Scrolling

  8. WebGL Animation Cruciform by Jaume Sanchez Elias

  9. THE USER CONTEXT matters. @aerotwist

  10. @aerotwist

  11. None
  12. None
  13. How do we make performance user-focused? @aerotwist

  14. LET’S START WITH perception. @aerotwist

  15. http://www.nngroup.com/articles/response-times-3-important-limits/ @aerotwist 0.1 SECONDS 1 SECOND 10 SECONDS

  16. The user feels an instantaneous response. Any longer and the

    connection between action and reaction is broken. 0.1 SECONDS http://www.nngroup.com/articles/response-times-3-important-limits/ @aerotwist
  17. The user feels an instantaneous response. Any longer and the

    connection between action and reaction is broken. 0.1 SECONDS The user’s flow of thought is seamless. Beyond it, the user loses focus and attention. 1 SECOND http://www.nngroup.com/articles/response-times-3-important-limits/ @aerotwist
  18. The user feels an instantaneous response. Any longer and the

    connection between action and reaction is broken. 0.1 SECONDS The user’s flow of thought is seamless. Beyond it, the user loses focus and attention. 1 SECOND You’ve lost the user’s attention. 10 SECONDS http://www.nngroup.com/articles/response-times-3-important-limits/ @aerotwist
  19. 60fps @aerotwist

  20. http://www.nngroup.com/articles/response-times-3-important-limits/ @aerotwist 0.1 SECONDS 1 SECOND 10 SECONDS 16 MILLISECONDS

  21. NOW LET’S LOOK AT A real website. @aerotwist

  22. None
  23. LET’S TIE THE TWO together. (perception, meet reality) @aerotwist

  24. R A I L ESPONSE NIMATION DLE OAD @aerotwist

  25. RAIL ESPONSE NIMATION DLE OAD @aerotwist

  26. Response. @aerotwist

  27. The user feels an instantaneous response. Any longer and the

    connection between action and reaction is broken. 0.1 SECONDS @aerotwist
  28. Animation. @aerotwist

  29. Visual changes feel smooth and consistent. 16 MILLISECONDS Any variation

    in frame rate will be disconcerting to the user. @aerotwist
  30. Idle. @aerotwist

  31. Allows for responding to user interaction. Any longer and we

    may not be responsive. 50 MILLISECONDS @aerotwist
  32. Load. @aerotwist

  33. The user’s flow of thought is seamless. Beyond it, the

    user loses focus and attention. 1 SECOND @aerotwist
  34. http://www.nngroup.com/articles/response-times-3-important-limits/ 0.1 SECONDS 1 SECOND 16 MILLISECONDS 50 MILLISECONDS RESPONSE

    ANIMATION IDLE LOAD @aerotwist
  35. TIME FOR A WORKED Example! @aerotwist

  36. voice-memos.appspot.com @aerotwist

  37. @aerotwist MEASURE & OPTIMIZE Response. (our target: 100ms)

  38. // Response: 100ms. this.sideNavToggleButton.addEventListener('click', () => { console.time('side-nav-tap'); this.toggleSideNav(); //

    Wait until the frame ships. requestAnimationFrame( _ => { console.timeEnd('side-nav-tap'); }); } );
  39. // Response: 100ms. this.sideNavToggleButton.addEventListener('click', () => { console.time('side-nav-tap'); this.toggleSideNav(); //

    Wait until the frame ships. requestAnimationFrame( _ => { console.timeEnd('side-nav-tap'); }); } );
  40. // Response: 100ms. this.sideNavToggleButton.addEventListener('click', () => { console.time('side-nav-tap'); this.toggleSideNav(); //

    Wait until the frame ships. requestAnimationFrame( _ => { console.timeEnd('side-nav-tap'); }); } );
  41. None
  42. Simple responses are usually well under 100ms. Use console.time /

    timeEnd to track response times. RESPONSES @aerotwist
  43. MEASURE & OPTIMIZE Animation. (our target: 8ms) @aerotwist

  44. // Animation: 60fps / 16ms per frame. showDetailsView () {

    console.time('heroTransitionShow'); this.heroImage.classList.add('hero-image--visible'); let onHeroAnimationFinished = () => { console.timeEnd('heroTransitionShow'); this.heroImage.removeEventListener('transitionend', onSideNavOpen); } // Listen for the end of the animation. this.heroImage.addEventListener(‘transitionend', onSideNavOpen); }
  45. // Animation: 60fps / 16ms per frame. showDetailsView () {

    console.time('heroTransitionShow'); this.heroImage.classList.add('hero-image--visible'); let onHeroAnimationFinished = () => { console.timeEnd('heroTransitionShow'); this.heroImage.removeEventListener('transitionend', onSideNavOpen); } // Listen for the end of the animation. this.heroImage.addEventListener(‘transitionend', onSideNavOpen); }
  46. // Animation: 60fps / 16ms per frame. showDetailsView () {

    console.time('heroTransitionShow'); this.heroImage.classList.add('hero-image--visible'); let onHeroAnimationFinished = () => { console.timeEnd('heroTransitionShow'); this.heroImage.removeEventListener('transitionend', onSideNavOpen); } // Listen for the end of the animation. this.heroImage.addEventListener(‘transitionend', onSideNavOpen); }
  47. None
  48. @aerotwist AND NOW, IT’S Combo time!

  49. None
  50. None
  51. OPTION 1: NATURAL transition: width, height, left, top;

  52. csstriggers.com

  53. csstriggers.com

  54. Response Animation 0.1s Response work Animation setup

  55. @aerotwist FLIP FIRST, LAST, INVERT, PLAY

  56. csstriggers.com

  57. csstriggers.com

  58. OPTION 2: FLIP getBoundingClientRect();

  59. OPTION 2: FLIP getBoundingClientRect(); el.classList.add(‘expanded’);

  60. OPTION 2: FLIP getBoundingClientRect(); el.classList.add(‘expanded’);

  61. OPTION 2: FLIP getBoundingClientRect(); el.classList.add(‘expanded’); transform: translate(-321px, -89px) scale(0.32);

  62. OPTION 2: FLIP getBoundingClientRect(); el.classList.add(‘expanded’); transform: translate(-321px, -89px) scale(0.32); transition:

    transform;
  63. OPTION 2: FLIP getBoundingClientRect(); el.classList.add(‘expanded’); transform: translate(-321px, -89px) scale(0.32); transform:

    none; transition: transform;
  64. Response Animation 0.1s Response work FLI P

  65. None
  66. bit.ly/flip-anims

  67. Animate with transforms and opacity where you can. FLIP expensive

    properties when coupled with Responses. ANIMATIONS Be super cautious; avoid work during scrolls. @aerotwist
  68. @aerotwist MEASURE & OPTIMIZE Load. (our target: 1,000ms)

  69. voicememos-core.css INLINED record-something.svg 1 2 voicememos-record.js voicememos-record.css voicememos-core.js 3 voicememos-edit.js

    voicememos-edit.css 4 voicememos-list.js voicememos-list.css 5 voicememos-details.js voicememos-details.css
  70. None
  71. None
  72. None
  73. None
  74. None
  75. None
  76. 99% CONNECTION

  77. @aerotwist

  78. @aerotwist AND NOW REPEAT Loads…

  79. None
  80. None
  81. None
  82. @aerotwist SERVICE WORKERS ARE awesome.

  83. @aerotwist

  84. @aerotwist

  85. @aerotwist

  86. @aerotwist

  87. @aerotwist

  88. @aerotwist

  89. @aerotwist

  90. @aerotwist

  91. @aerotwist

  92. @aerotwist

  93. Cache @aerotwist

  94. Cache @aerotwist

  95. @aerotwist

  96. Cache @aerotwist

  97. Cache @aerotwist

  98. self.onfetch = function(event) { event.respondWith( // Check the cache... caches.match(request).then(

    function(response) { // Return it if you got it. if (response) return response; // Otherwise... }); ); };
  99. self.onfetch = function(event) { event.respondWith( // Check the cache... caches.match(request).then(

    function(response) { // Return it if you got it. if (response) return response; // Otherwise... }); ); };
  100. self.onfetch = function(event) { event.respondWith( // Check the cache... caches.match(request).then(

    function(response) { // Return it if you got it. if (response) return response; // Otherwise... }); ); };
  101. self.onfetch = function(event) { event.respondWith( // Check the cache... caches.match(request).then(

    function(response) { // Return it if you got it. if (response) return response; // Otherwise... }); ); };
  102. self.onfetch = function(event) { event.respondWith( // Check the cache... caches.match(request).then(

    function(response) { // Return it if you got it. if (response) return response; // Otherwise... }); ); };
  103. return fetch(request).then(function(response) { // Anything not A-OK or opaque (CORSy

    stuff) we return. if (response.status !== 200 && response.status !== 0) return response; // Clone and cache the request and response. var responseToCache = response.clone(); var requestToCache = request.clone(); caches.open('le-cache').then(function(cache) { cache.put(requestToCache, responseToCache).catch(function(err) { console.warn(err.message); }); }); return response; });
  104. return fetch(request).then(function(response) { // Anything not A-OK or opaque (CORSy

    stuff) we return. if (response.status !== 200 && response.status !== 0) return response; // Clone and cache the request and response. var responseToCache = response.clone(); var requestToCache = request.clone(); caches.open('le-cache').then(function(cache) { cache.put(requestToCache, responseToCache).catch(function(err) { console.warn(err.message); }); }); return response; });
  105. return fetch(request).then(function(response) { // Anything not A-OK or opaque (CORSy

    stuff) we return. if (response.status !== 200 && response.status !== 0) return response; // Clone and cache the request and response. var responseToCache = response.clone(); var requestToCache = request.clone(); caches.open('le-cache').then(function(cache) { cache.put(requestToCache, responseToCache).catch(function(err) { console.warn(err.message); }); }); return response; });
  106. return fetch(request).then(function(response) { // Anything not A-OK or opaque (CORSy

    stuff) we return. if (response.status !== 200 && response.status !== 0) return response; // Clone and cache the request and response. var responseToCache = response.clone(); var requestToCache = request.clone(); caches.open('le-cache').then(function(cache) { cache.put(requestToCache, responseToCache).catch(function(err) { console.warn(err.message); }); }); return response; });
  107. return fetch(request).then(function(response) { // Anything not A-OK or opaque (CORSy

    stuff) we return. if (response.status !== 200 && response.status !== 0) return response; // Clone and cache the request and response. var responseToCache = response.clone(); var requestToCache = request.clone(); caches.open('le-cache').then(function(cache) { cache.put(requestToCache, responseToCache).catch(function(err) { console.warn(err.message); }); }); return response; });
  108. 75TH PERCENTILE LOAD TIME < 1 SECOND @aerotwist

  109. Inline app shell CSS. Lazy-load (async, defer) everything else. Far-future

    cache JS, CSS, images etc. LOAD Use a Service Worker. @aerotwist
  110. @aerotwist WHATEVER HAPPENED TO Idle?

  111. requestIdleCallback @aerotwist

  112. var tasks = []; var MIN_TASK_TIME = 1; function handleTasks(deadline)

    { while (deadline.timeRemaining() > MIN_TASK_TIME && tasks.length) processTask(tasks.pop()); if (tasks.length) requestIdleCallback(handleTasks); } requestIdleCallback(handleTasks);
  113. var tasks = []; var MIN_TASK_TIME = 1; function handleTasks(deadline)

    { while (deadline.timeRemaining() > MIN_TASK_TIME && tasks.length) processTask(tasks.pop()); if (tasks.length) requestIdleCallback(handleTasks); } requestIdleCallback(handleTasks);
  114. var tasks = []; var MIN_TASK_TIME = 1; function handleTasks(deadline)

    { while (deadline.timeRemaining() > MIN_TASK_TIME && tasks.length) processTask(tasks.pop()); if (tasks.length) requestIdleCallback(handleTasks); } requestIdleCallback(handleTasks);
  115. var tasks = []; var MIN_TASK_TIME = 1; function handleTasks(deadline)

    { while (deadline.timeRemaining() > MIN_TASK_TIME && tasks.length) processTask(tasks.pop()); if (tasks.length) requestIdleCallback(handleTasks); } requestIdleCallback(handleTasks);
  116. var tasks = []; var MIN_TASK_TIME = 1; function handleTasks(deadline)

    { while (deadline.timeRemaining() > MIN_TASK_TIME && tasks.length) processTask(tasks.pop()); if (tasks.length) requestIdleCallback(handleTasks); } requestIdleCallback(handleTasks);
  117. var tasks = []; var MIN_TASK_TIME = 1; function handleTasks(deadline)

    { while (deadline.timeRemaining() > MIN_TASK_TIME && tasks.length) processTask(tasks.pop()); if (tasks.length) requestIdleCallback(handleTasks); } requestIdleCallback(handleTasks);
  118. bit.ly/requestidlecallback

  119. IDLE TIME IS different. @aerotwist

  120. Use it for any non-essential work e.g. analytics. Think in

    small, deterministic operations. IDLE Think defensively. @aerotwist
  121. SOME HANDY Links. @aerotwist

  122. Udacity: Browser Rendering Optimization bit.ly/rail-udacity Web Fundamentals: Performance bit.ly/fundamentals-perf Voice

    Memos voice-memos.appspot.com aerotwist.com/blog/voice-memos/ Udacity: Critical Rendering Path bit.ly/crp-udacity @aerotwist
  123. feedback PLEASE SEND YOUR

  124. Thanks.