Pro Yearly is on sale from $80 to $50! »

Watch your back, Browser! You're being observed.

Watch your back, Browser! You're being observed.

22725c2d3eb331146549bf0d5d3c050c?s=128

stefan judis

May 23, 2017
Tweet

Transcript

  1. Watch your back, Browser! You're being observed. @stefanjudis

  2. Stefan Judis Frontend Developer, Occasional Teacher, Meetup Organizer ❤ Open

    Source, Performance and Accessibility ❤ @stefanjudis
  3. None
  4. I'm excited about conferences and meetups

  5. The web platform evolves really fast these days

  6. Staying up to date is part of our job

  7. JavaScript Ecosystem

  8. Browser APIs adapt common use cases

  9. Browser APIs adapt common use cases pull

  10. Browser APIs adapt common use cases pull push

  11. Let's celebrate!

  12. None
  13. None
  14. codepen.io/thebabydino/pen/RRRRZE

  15. codepen.io/thebabydino/pen/RRRRZE

  16. function isElementInViewport (el) { var rect = el.getBoundingClientRect(); return (

    rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); } How to tell if a DOM element is visible in the current viewport? stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport
  17. function isElementInViewport (el) { var rect = el.getBoundingClientRect(); // can

    trigger force layout/reflow return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); } How to tell if a DOM element is visible in the current viewport? stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport
  18. WHAT FORCES LAYOUT / REFLOW gist.github.com/paulirish/5d52fb081b3570c81e3a

  19. Implementation with 'scroll' const speakers = [...document.querySelectorAll('.speaker-details')]; window.addEventListener('scroll', () =>

    { speakers.forEach(elem => { if (isElementInViewport(elem)) { elem.classList.add('party-party'); speakers.splice(speakers.indexOf(elem), 1); } }); });
  20. Implementation with 'scroll' const speakers = [...document.querySelectorAll('.speaker-details')]; window.addEventListener('scroll', () =>

    { // really expensive speakers.forEach(elem => { if (isElementInViewport(elem)) { elem.classList.add('party-party'); speakers.splice(speakers.indexOf(elem), 1); } }); });
  21. Implementation with 'scroll' 60FPS 1s / 60 16ms

  22. Implementation with 'scroll' 60FPS 1s / 60 10ms 16ms

  23. None
  24. None
  25. None
  26. None
  27. Intersection Observer

  28. - Intersection Observer - (function() { const options = {

    threshold: 1.0 }; const intersectionObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add('party-party'); intersectionObserver.unobserve(entry.target); } }); }, options); [...document.querySelectorAll('.speaker-details')] .forEach(elem => intersectionObserver.observe(elem)); })();
  29. - Intersection Observer - (function() { const options = {

    threshold: 1.0 }; const intersectionObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add('party-party'); intersectionObserver.unobserve(entry.target); } }); }, options); [...document.querySelectorAll('.speaker-details')] .forEach(elem => intersectionObserver.observe(elem)); })();
  30. - Intersection Observer - (function() { const options = {

    threshold: 1.0 }; const intersectionObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add('party-party'); intersectionObserver.unobserve(entry.target); } }); }, options); [...document.querySelectorAll('.speaker-details')] .forEach(elem => intersectionObserver.observe(elem)); })();
  31. (function() { const options = { threshold: 1.0 }; const

    intersectionObserver = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { entry.target.classList.add('party-party'); intersectionObserver.unobserve(entry.target); } }); }, options); [...document.querySelectorAll('.speaker-details')] .forEach(elem => intersectionObserver.observe(elem)); })(); - Intersection Observer -
  32. None
  33. None
  34. Measure it!

  35. - Intersection Observer - const options = { root: null,

    // ☝ or document.querySelector('.foo') // default: null rootMargin: '10px', // ☝ or '10% 5%' or '1px 10% 3px 12%' // default: '0px 0px 0px 0px' threshold: 0.5 // ☝ or [0.25, 0.5, 0.75, 1] // default: 0 }; w3c.github.io/IntersectionObserver/#intersection-observer-interface
  36. - Intersection Observer - const options = { root: null,

    // ☝ or document.querySelector('.foo') // default: null rootMargin: '10px', // ☝ or '10% 5%' or '1px 10% 3px 12%' // default: '0px 0px 0px 0px' threshold: 0.5 // ☝ or [0.25, 0.5, 0.75, 1] // default: 0 }; w3c.github.io/IntersectionObserver/#intersection-observer-interface
  37. - Intersection Observer - const options = { root: null,

    // ☝ or document.querySelector('.foo') // default: null rootMargin: '10px', // ☝ or '10% 5%' or '1px 10% 3px 12%' // default: '0px 0px 0px 0px' threshold: 0.5 // ☝ or [0.25, 0.5, 0.75, 1] // default: 0 }; w3c.github.io/IntersectionObserver/#intersection-observer-interface
  38. // this fires when: // 1. The target begins entering

    the viewport (0 < ratio < 1). // 2. The target fully enters the viewport (ratio >= 1). // 3. The target begins leaving the viewport (1 > ratio > 0). // 4. The target fully leaves the viewport (ratio <= 0). let observer = new IntersectionObserver(handler, { threshold: [0, 1] }); - Intersection Observer - hacks.mozilla.org/2017/08/intersection-observer-comes-to-firefox/
  39. - Intersection Observer - threshold = 0 viewport

  40. - Intersection Observer - threshold = 0 entry => {

    console.log(entry.isIntersecting) // true console.log(entry.intersectionRatio) // sth. around 0 } viewport
  41. - Intersection Observer - threshold = 0 entry => {

    console.log(entry.isIntersecting) // false console.log(entry.intersectionRatio) // 0 } viewport
  42. - Intersection Observer - threshold > 0 (e.g. 0.5) viewport

  43. - Intersection Observer - threshold > 0 (e.g. 0.5) entry

    => { console.log(entry.isIntersecting) // true console.log(entry.intersectionRatio) // sth. around 0.5 } viewport
  44. - Intersection Observer - threshold > 0 (e.g. 0.5) entry

    => { console.log(entry.isIntersecting) // true console.log(entry.intersectionRatio) // sth. around 0.5 } viewport
  45. - Intersection Observer - developers.google.com/web/fundamentals/media/mobile-web-video-playback if ('IntersectionObserver' in window) {

    // Show/hide mute button based // on video visibility in the page. function onIntersection(entries) { entries.forEach(function(entry) { muteButton.hidden = video.paused || entry.isIntersecting; }); } var observer = new IntersectionObserver(onIntersection); observer.observe(video); }
  46. - Intersection Observer - developers.google.com/web/fundamentals/media/mobile-web-video-playback if ('IntersectionObserver' in window) {

    // Show/hide mute button based // on video visibility in the page. function onIntersection(entries) { entries.forEach(function(entry) { muteButton.hidden = video.paused || entry.isIntersecting; }); } var observer = new IntersectionObserver(onIntersection); observer.observe(video); }
  47. twitter.com/jaffathecake/status/857537625154097152

  48. twitter.com/jaffathecake/status/857537625154097152

  49. - Intersection Observer - caniuse.com/#feat=intersectionobserver * in development *

  50. - Intersection Observer - * Incompatibilities between spec and implementations

    github.com/WICG/IntersectionObserver/issues/222 github.com/WICG/IntersectionObserver/issues/211
  51. - Intersection Observer - * Incompatibilities between spec and implementations

    github.com/WICG/IntersectionObserver/issues/222 github.com/WICG/IntersectionObserver/issues/211 Resolved since Oct 17, 2017
  52. - Intersection Observer - Polyfillable? github.com/WICG/IntersectionObserver YES!

  53. Let's celebrate the whole schedule!

  54. None
  55. None
  56. - Intersection Observer - (function() { // Intersection Observer code

    // third-party implementation // No interface to hook into // No access to the implementation // (ಥ_ಥ) (ಥ_ಥ) (ಥ_ಥ) })();
  57. Mutation Observer

  58. - Mutation Observer - (function() { const list = document.querySelector('.schedule');

    const nrOfTalks = list.querySelectorAll('.speaker').length; const config = { attributes: true, subtree: true }; let talksSeen = 0; const mutationObserver = new MutationObserver(mutations => { mutations.forEach((mutation) => { if ( mutation.type === 'attributes' && mutation.target.classList.contains('party-party') ) { talksSeen++; } }) if (talksSeen === nrOfTalks) { [...Array(20)].forEach(cornify_add); mutationObserver.disconnect(); } }); mutationObserver.observe(list, config); })();
  59. (function() { const list = document.querySelector('.schedule'); const nrOfTalks = list.querySelectorAll('.speaker').length;

    const config = { attributes: true, subtree: true }; let talksSeen = 0; const mutationObserver = new MutationObserver(mutations => { mutations.forEach((mutation) => { if ( mutation.type === 'attributes' && mutation.target.classList.contains('party-party') ) { talksSeen++; } }) if (talksSeen === nrOfTalks) { [...Array(20)].forEach(cornify_add); mutationObserver.disconnect(); } }); mutationObserver.observe(list, config); })(); - Mutation Observer -
  60. (function() { const list = document.querySelector('.schedule'); const nrOfTalks = list.querySelectorAll('.speaker').length;

    const config = { attributes: true, subtree: true }; let talksSeen = 0; const mutationObserver = new MutationObserver(mutations => { mutations.forEach((mutation) => { if ( mutation.type === 'attributes' && mutation.target.classList.contains('party-party') ) { talksSeen++; } }) if (talksSeen === nrOfTalks) { [...Array(20)].forEach(cornify_add); mutationObserver.disconnect(); } }); mutationObserver.observe(list, config); })(); - Mutation Observer -
  61. (function() { const list = document.querySelector('.schedule'); const nrOfTalks = list.querySelectorAll('.speaker').length;

    const config = { attributes: true, subtree: true }; let talksSeen = 0; const mutationObserver = new MutationObserver(mutations => { mutations.forEach((mutation) => { if ( mutation.type === 'attributes' && mutation.target.classList.contains('party-party') ) { talksSeen++; } }) if (talksSeen === nrOfTalks) { [...Array(20)].forEach(cornify_add); mutationObserver.disconnect(); } }); mutationObserver.observe(list, config); })(); - Mutation Observer -
  62. const options = { childList: true, // ☝ observe target's

    children attributes: true, // ☝ observe target's attributes subtree: true, // ☝ observe target and its descendants ... }; dom.spec.whatwg.org/#interface-mutationobserver - Mutation Observer -
  63. const options = { childList: true, // ☝ observe target's

    children attributes: true, // ☝ observe target's attributes subtree: true, // ☝ observe target and its descendants ... }; dom.spec.whatwg.org/#interface-mutationobserver - Mutation Observer -
  64. const options = { childList: true, // ☝ observe target's

    children attributes: true, // ☝ observe target's attributes subtree: true, // ☝ observe target and its descendants ... }; dom.spec.whatwg.org/#interface-mutationobserver - Mutation Observer -
  65. const options = { childList: true, // ☝ observe target's

    children attributes: true, // ☝ observe target's attributes subtree: true, // ☝ observe target and its descendants ... }; dom.spec.whatwg.org/#interface-mutationobserver - Mutation Observer -
  66. caniuse.com/#feat=mutationobserver - Mutation Observer -

  67. But let's always watch out for performance

  68. WAYS TO MEASURE PERFORMANCE Synthetic Monitoring Real User Monitoring

  69. None
  70. None
  71. None
  72. REAL USER METRICS (RUM) NAVIGATION TIMING API RESOURCE TIMING API

    USER TIMING API
  73. - Navigation Timing API - w3c.github.io/navigation-timing/ window.performance.timing

  74. - Navigation Timing API - { "navigationStart": 1494722965671, "unloadEventStart": 0,

    "unloadEventEnd": 0, "redirectStart": 0, "redirectEnd": 0, "fetchStart": 1494722965838, "domainLookupStart": 1494722965841, "domainLookupEnd": 1494722972627, "connectStart": 1494722972627, "connectEnd": 1494722973191, "secureConnectionStart": 1494722972815, "requestStart": 1494722973191, "responseStart": 1494722973667, "responseEnd": 1494722973681, "domLoading": 1494722973681, "domInteractive": 1494722974288, "domContentLoadedEventStart": 1494722974288, "domContentLoadedEventEnd": 1494722974320, "domComplete": 1494722974571, "loadEventStart": 1494722974571, "loadEventEnd": 1494722974574 } w3c.github.io/navigation-timing/ window.performance.timing
  75. - Navigation Timing API - w3c.github.io/navigation-timing/

  76. - Resource Timing API - window.performance.getEntriesByType('resource')

  77. - Resource Timing API - [ ..., { connectEnd: 117.69500000000001,

    connectStart: 117.69500000000001, decodedBodySize: 20133, domainLookupEnd: 117.69500000000001, domainLookupStart: 117.69500000000001, duration: 846.3100000000001, encodedBodySize: 20133, entryType: 'resource', fetchStart: 117.69500000000001, initiatorType: 'img', name: 'http://127.0.0.1:8080/image.png', redirectEnd: 0, redirectStart: 0, requestStart: 962.6750000000001, responseEnd: 964.0050000000001, responseStart: 963.45, secureConnectionStart: 0, startTime: 117.69500000000001, transferSize: 20391, workerStart: 0 } ] www.w3.org/TR/resource-timing-1/ window.performance.getEntriesByType('resource')
  78. [...Array(20)].forEach(cornify_add); - User Timing API - w3c.github.io/user-timing/ if (talksSeen ===

    nrOfTalks) { }
  79. [...Array(20)].forEach(cornify_add); - User Timing API - w3c.github.io/user-timing/ if (talksSeen ===

    nrOfTalks) { }
  80. - User Timing API - w3c.github.io/user-timing/ if (talksSeen === nrOfTalks)

    { // measure how long this takes performance.mark('cornify_start'); [...Array(20)].forEach(cornify_add); performance.mark('cornify_end'); performance.measure( 'cornify_processing_time', 'cornify_start', 'cornify_end' ); }
  81. - User Timing API - w3c.github.io/user-timing/ window.performance.getEntriesByType('mark') [ { duration:

    0 entryType: 'mark' name: 'cornify_start' startTime: 39613.885 }, ... ]
  82. - User Timing API - w3c.github.io/user-timing/ window.performance.getEntriesByType('measure') [ { duration:

    5.9900000000016 entryType: 'measure' name: 'cornify_processing_time' startTime: 46002.34500000001 }, ... ]
  83. ENTRY TYPE VALUES mark measure navigation resource ACCESSIBLE VIA getEntries

    getEntriesByType getEntriesByName
  84. Performance Observer

  85. - Performance Observer - (function() { const perfObserver = new

    PerformanceObserver(list => { list.getEntries().forEach((entry) => { console.log( `Name: ${ entry.name }, Duration: ${ entry.duration }` ); }); }); perfObserver.observe({entryTypes: ['measure']}); })(); www.w3.org/TR/performance-timeline-2/#dom-performanceobserver
  86. (function() { const perfObserver = new PerformanceObserver(list => { list.getEntries().forEach((entry)

    => { console.log( `Name: ${ entry.name }, Duration: ${ entry.duration }` ); }); }); perfObserver.observe({entryTypes: ['measure']}); })(); www.w3.org/TR/performance-timeline-2/#dom-performanceobserver - Performance Observer -
  87. (function() { const perfObserver = new PerformanceObserver(list => { list.getEntries().forEach((entry)

    => { console.log( `Name: ${ entry.name }, Duration: ${ entry.duration }` ); }); }); perfObserver.observe({entryTypes: ['measure']}); })(); www.w3.org/TR/performance-timeline-2/#dom-performanceobserver - Performance Observer -
  88. - Performance Timeline - The developer is encouraged to use

    PerformanceObserver where possible. Further, new performance APIs and metrics may only be available through the PerformanceObserver interface. www.w3.org/TR/performance-timeline-2/#introduction
  89. - Paint Timing - www.w3.org/TR/paint-timing/ const perfObserver = new PerformanceObserver(list

    => { list.getEntries().forEach((entry) => { // Process entries // report back for analytics and monitoring // entry.name -> 'first-paint' // entry.name -> 'first-contentful-paint' } ); }); perfObserver.observe({entryTypes: ['paint']});
  90. - Paint Timing - www.w3.org/TR/paint-timing/ const perfObserver = new PerformanceObserver(list

    => { list.getEntries().forEach((entry) => { // Process entries // report back for analytics and monitoring // entry.name -> 'first-paint' // entry.name -> 'first-contentful-paint' } ); }); perfObserver.observe({entryTypes: ['paint']});
  91. - Long Task - github.com/w3c/longtasks var perfObserver = new PerformanceObserver(function(list)

    { list.getEntries().forEach((entry) => { // Process entries // report back for analytics and monitoring // ... }); }); perfObserver.observe({entryTypes: ['longtask']});
  92. - Long Task - github.com/w3c/longtasks var perfObserver = new PerformanceObserver(function(list)

    { list.getEntries().forEach((entry) => { // Process entries // report back for analytics and monitoring // ... }); }); perfObserver.observe({entryTypes: ['longtask']});
  93. None
  94. None
  95. None
  96. None
  97. <img src="hero.jpg" onload="performance.clearMarks('img displayed'); performance.mark('img displayed');"> <script> performance.clearMarks('img displayed'); performance.mark('img

    displayed'); </script> - Hero Element Timing - speedcurve.com/blog/user-timing-and-custom-metrics/
  98. - Hero Element Timing - docs.google.com/document/d/1yRYfYR1DnHtgwC4HRR04ipVVhT1h5gkI6yPmKCgJkyQ/edit <div timing=”dom, paint”></div> const

    perfObserver = new PerformanceObserver(list => { list.getEntries().forEach((entry) => { // Process entries // report back for analytics and monitoring // ... }); }); perfObserver.observe({entryTypes: ['element']}); github.com/w3c/charter-webperf/issues/30
  99. - Hero Element Timing - docs.google.com/document/d/1yRYfYR1DnHtgwC4HRR04ipVVhT1h5gkI6yPmKCgJkyQ/edit <div timing=”dom, paint”></div> const

    perfObserver = new PerformanceObserver(list => { list.getEntries().forEach((entry) => { // Process entries // report back for analytics and monitoring // ... }); }); perfObserver.observe({entryTypes: ['element']}); github.com/w3c/charter-webperf/issues/30
  100. - Server Timing - https://developer.akamai.com/blog/2017/06/07/completing-performance-analysis-server-timing/ Server-Timing: acl: 10 Server-Timing: db:

    125 Server-Timing: serverName; edge.machinename.net https://w3c.github.io/server-timing/ const perfObserver = new PerformanceObserver(function(list) { list.getEntries().forEach((entry) => { // Process entries // report back for analytics and monitoring // ... }); }) perfObserver.observe({entryTypes: ['server']})
  101. - Server Timing - https://developer.akamai.com/blog/2017/06/07/completing-performance-analysis-server-timing/ Server-Timing: acl: 10 Server-Timing: db:

    125 Server-Timing: serverName; edge.machinename.net https://w3c.github.io/server-timing/ const perfObserver = new PerformanceObserver(function(list) { list.getEntries().forEach((entry) => { // Process entries // report back for analytics and monitoring // ... }); }) perfObserver.observe({entryTypes: ['server']})
  102. LEVERAGING THE METRICS THAT MOST AFFECT USER EXPERIENCE https://www.youtube.com/watch?v=6Ljq-Jn-EgU

  103. - Performance Observer - developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver * under consideration *

  104. - Performance Observer - we'll see... Polyfillable?

  105. None
  106. I had to cheat a bit...

  107. None
  108. None
  109. Resize Observer

  110. - Resize Observer - (function() { const resizeObserver = new

    ResizeObserver(entries => { entries.forEach((entry) => { drawConfetti(entry); }); }); [...document.querySelectorAll('.hall-schedule__title')] .forEach(desc => resizeObserver.observe(desc)); })();
  111. (function() { const resizeObserver = new ResizeObserver(entries => { entries.forEach((entry)

    => { drawConfetti(entry); }); }); [...document.querySelectorAll('.hall-schedule__title')] .forEach(desc => resizeObserver.observe(desc)); })(); - Resize Observer -
  112. (function() { const resizeObserver = new ResizeObserver(entries => { entries.forEach((entry)

    => { drawConfetti(entry); }); }); [...document.querySelectorAll('.hall-schedule__title')] .forEach(desc => resizeObserver.observe(desc)); })(); - Resize Observer -
  113. - Resize Observer - Observation also fires when watched Element

    is inserted/removed from DOM watched Element display gets set to none Observation does not fire for triggered CSS transforms
 wicg.github.io/ResizeObserver/#intro
  114. None
  115. None
  116. - Resize Observer - developers.google.com/web/updates/2016/10/resizeobserver const ro = new ResizeObserver((entries)

    => { document.scrollingElement.scrollTop = document.scrollingElement.scrollHeight; }); // Observe the scrollingElement // for when the window gets resized ro.observe(document.scrollingElement); // Observe the timeline // to process new messages ro.observe(timeline);
  117. - Resize Observer - developers.google.com/web/updates/2016/10/resizeobserver const ro = new ResizeObserver((entries)

    => { document.scrollingElement.scrollTop = document.scrollingElement.scrollHeight; }); // Observe the scrollingElement // for when the window gets resized ro.observe(document.scrollingElement); // Observe the timeline // to process new messages ro.observe(timeline);
  118. - Resize Observer -

  119. - Resize Observer -

  120. - Resize Observer - developers.google.com/web/updates/2016/10/resizeobserver * under consideration ** behind

    a flag ** * **
  121. - Resize Observer - Polyfillable? github.com/que-etc/resize-observer-polyfill so, so...

  122. But let's face the resize problem Photo by madstreetz

  123. - Event streams - element

  124. - Event streams - element initial call

  125. - Event streams - element initial call

  126. - Event streams - element initial call resize resize resize

    opening transition
  127. element initial call resize resize resize - Event streams -

    opening transition
  128. element initial call resize resize resize resize resize resize -

    Event streams - opening transition closing transition
  129. - Event streams - element initial call resize resize resize

    resize resize resize opening transition closing transition
  130. - Event streams - onResize(entry => { drawConfetti(entry); });

  131. - Event streams - onResize(entry => { drawConfetti(entry); });

  132. - Event streams - }); let isFirst = true; onResize(entry

    => { if (isFirst) { isFirst = false; return; } drawConfetti(entry);
  133. - Event streams - }); let isFirst = true; onResize(entry

    => { if (isFirst) { isFirst = false; return; } drawConfetti(entry);
  134. - Event streams - }); let isFirst = true; let

    last; onResize(entry => { if (isFirst) { isFirst = false; return; } if ( last && last.contentRect.height < entry.contentRect.height ) { drawConfetti(entry); last = entry; }
  135. Observables

  136. - Observables - A collection that arrives over time.

  137. - Observables - function getObservableWithThreeValues () { return new Observable((observer)

    => { observer.next(1); observer.next(2); observer.next(3); observer.complete(); }); } const observer = { next(value) { console.log('next:', value) }, error(err) { console.error(err) }, complete() { console.log('We are done') }, } const subscription = getObservableWithThreeValues().subscribe(observer) // next: 2 // next: 4 // next: 6 // We are done
  138. - Observables - function getObservableWithThreeValues () { return new Observable((observer)

    => { observer.next(1); observer.next(2); observer.next(3); observer.complete(); }); } const observer = { next(value) { console.log('next:', value) }, error(err) { console.error(err) }, complete() { console.log('We are done') }, }; const subscription = getObservableWithThreeValues().subscribe(observer) // next: 2 // next: 4 // next: 6 // We are done
  139. - Observables - function getObservableWithThreeValues () { return new Observable((observer)

    => { observer.next(1); observer.next(2); observer.next(3); observer.complete(); }); } const observer = { next(value) { console.log('next:', value) }, error(err) { console.error(err) }, complete() { console.log('We are done') }, }; const subscription = getObservableWithThreeValues().subscribe(observer); // next: 1 // next: 2 // next: 3 // We are done
  140. - Observables - It's a collection!

  141. - Observables - It's a collection! // like Array.prototype.map observable.map

    // like Array.prototype.filter observable.filter // like Array.prototype.reduce observable.reduce
  142. - Observables - function getResizeStream(elem) { return Rx.Observable.create((observer) => {

    const resizeObserver = new ResizeObserver((entries) => { entries.forEach(entry => { drawConfetti(entry); }); }); resizeObserver.observe(elem); }) }
  143. - Observables - function getResizeStream(elem) { return Rx.Observable.create((observer) => {

    const resizeObserver = new ResizeObserver((entries) => { entries.forEach(entry => { observer.next(entry); }); }); resizeObserver.observe(elem); }); }
  144. - Observables - function getResizeStream(elem) { return Rx.Observable.create((observer) => {

    const resizeObserver = new ResizeObserver(entries => { entries.forEach(entry => { observer.next(entry); }) }); resizeObserver.observe(elem); }); } Collection super powers
  145. - Observables - const subscription = getResizeStream(elem); .skip(1) .pairwise() .filter(([prev,

    current]) => { return prev.contentRect.height < current.contentRect.height; }) .map(([prev, current]) => current) .subscribe({ next: (entry) => drawConfetti(entry), error: console.error, complete: () => { console.log('Complete!') } }); element initial call opening transition closing transition
  146. - Observables - const subscription = getResizeStream(elem) .skip(1); .pairwise() .filter(([prev,

    current]) => { return prev.contentRect.height < current.contentRect.height; }) .map(([prev, current]) => current) .subscribe({ next: (entry) => drawConfetti(entry), error: console.error, complete: () => { console.log('Complete!') } }); element initial call opening transition closing transition
  147. - Observables - const subscription = getResizeStream(elem) .skip(1); .pairwise() .filter(([prev,

    current]) => { return prev.contentRect.height < current.contentRect.height; }) .map(([prev, current]) => current) .subscribe({ next: (entry) => drawConfetti(entry), error: console.error, complete: () => { console.log('Complete!') } }); element initial call opening transition closing transition
  148. - Observables - const subscription = getResizeStream(elem) .skip(1) .pairwise(); .filter(([prev,

    current]) => { return prev.contentRect.height < current.contentRect.height; }) .map(([prev, current]) => current) .subscribe({ next: (entry) => drawConfetti(entry), error: console.error, complete: () => { console.log('Complete!') } }); element initial call opening transition closing transition
  149. - Observables - const subscription = getResizeStream(elem) .skip(1) .pairwise(); .filter(([prev,

    current]) => { return prev.contentRect.height < current.contentRect.height; }) .map(([prev, current]) => current) .subscribe({ next: (entry) => drawConfetti(entry), error: console.error, complete: () => { console.log('Complete!') } }); element initial call opening transition closing transition
  150. - Observables - const subscription = getResizeStream(elem) .skip(1) .pairwise() .filter(([prev,

    current]) => { return prev.contentRect.height < current.contentRect.height; }); .map(([prev, current]) => current) .subscribe({ next: (entry) => drawConfetti(entry), error: console.error, complete: () => { console.log('Complete!') } }); element initial call opening transition closing transition
  151. - Observables - const subscription = getResizeStream(elem) .skip(1) .pairwise() .filter(([prev,

    current]) => { return prev.contentRect.height < current.contentRect.height; }); .map(([prev, current]) => current) .subscribe({ next: (entry) => drawConfetti(entry), error: console.error, complete: () => { console.log('Complete!') } }); element initial call opening transition closing transition
  152. - Observables - const subscription = getResizeStream(elem) .skip(1) .pairwise() .filter(([prev,

    current]) => { return prev.contentRect.height < current.contentRect.height; }) .map(([prev, current]) => current); .subscribe({ next: (entry) => drawConfetti(entry), error: console.error, complete: () => { console.log('Complete!') } }); element initial call opening transition closing transition
  153. - Observables - const subscription = getResizeStream(elem) .skip(1) .pairwise() .filter(([prev,

    current]) => { return prev.contentRect.height < current.contentRect.height; }) .map(([prev, current]) => current); .subscribe({ next: (entry) => drawConfetti(entry), error: console.error, complete: () => { console.log('Complete!') } }); element initial call opening transition closing transition
  154. - Observables - const subscription = getResizeStream(elem) .skip(1) .pairwise() .filter(([prev,

    current]) => { return prev.contentRect.height < current.contentRect.height; }) .map(([prev, current]) => current) .subscribe({ next: (entry) => drawConfetti(entry), error: console.error, complete: () => { console.log('Complete!'); } }); element initial call opening transition closing transition
  155. None
  156. None
  157. Intersection Observer Mutation Observer Performance Observer Resize Observer Observables

  158. So much has changed

  159. So much will change

  160. Let's celebrate!

  161. Let's celebrate!

  162. Let's celebrate! Thanks. @stefanjudis Slides ctfl.io/watch-your-back-browser