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. 2.

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

    Source, Performance and Accessibility ❤ @stefanjudis
  2. 3.
  3. 12.
  4. 13.
  5. 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
  6. 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
  7. 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); } }); });
  8. 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); } }); });
  9. 23.
  10. 24.
  11. 25.
  12. 26.
  13. 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)); })();
  14. 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)); })();
  15. 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)); })();
  16. 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 -
  17. 32.
  18. 33.
  19. 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
  20. 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
  21. 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
  22. 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/
  23. 40.

    - Intersection Observer - threshold = 0 entry => {

    console.log(entry.isIntersecting) // true console.log(entry.intersectionRatio) // sth. around 0 } viewport
  24. 41.

    - Intersection Observer - threshold = 0 entry => {

    console.log(entry.isIntersecting) // false console.log(entry.intersectionRatio) // 0 } viewport
  25. 43.

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

    => { console.log(entry.isIntersecting) // true console.log(entry.intersectionRatio) // sth. around 0.5 } viewport
  26. 44.

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

    => { console.log(entry.isIntersecting) // true console.log(entry.intersectionRatio) // sth. around 0.5 } viewport
  27. 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); }
  28. 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); }
  29. 50.

    - Intersection Observer - * Incompatibilities between spec and implementations

    github.com/WICG/IntersectionObserver/issues/222 github.com/WICG/IntersectionObserver/issues/211
  30. 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
  31. 54.
  32. 55.
  33. 56.

    - Intersection Observer - (function() { // Intersection Observer code

    // third-party implementation // No interface to hook into // No access to the implementation // (ಥ_ಥ) (ಥ_ಥ) (ಥ_ಥ) })();
  34. 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); })();
  35. 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 -
  36. 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 -
  37. 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 -
  38. 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 -
  39. 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 -
  40. 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 -
  41. 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 -
  42. 69.
  43. 70.
  44. 71.
  45. 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
  46. 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')
  47. 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' ); }
  48. 81.

    - User Timing API - w3c.github.io/user-timing/ window.performance.getEntriesByType('mark') [ { duration:

    0 entryType: 'mark' name: 'cornify_start' startTime: 39613.885 }, ... ]
  49. 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 }, ... ]
  50. 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
  51. 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 -
  52. 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 -
  53. 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
  54. 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']});
  55. 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']});
  56. 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']});
  57. 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']});
  58. 93.
  59. 94.
  60. 95.
  61. 96.
  62. 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
  63. 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
  64. 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']})
  65. 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']})
  66. 105.
  67. 107.
  68. 108.
  69. 110.

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

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

    (function() { const resizeObserver = new ResizeObserver(entries => { entries.forEach((entry)

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

    (function() { const resizeObserver = new ResizeObserver(entries => { entries.forEach((entry)

    => { drawConfetti(entry); }); }); [...document.querySelectorAll('.hall-schedule__title')] .forEach(desc => resizeObserver.observe(desc)); })(); - Resize Observer -
  72. 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
  73. 114.
  74. 115.
  75. 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);
  76. 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);
  77. 128.

    element initial call resize resize resize resize resize resize -

    Event streams - opening transition closing transition
  78. 129.

    - Event streams - element initial call resize resize resize

    resize resize resize opening transition closing transition
  79. 132.

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

    => { if (isFirst) { isFirst = false; return; } drawConfetti(entry);
  80. 133.

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

    => { if (isFirst) { isFirst = false; return; } drawConfetti(entry);
  81. 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; }
  82. 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
  83. 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
  84. 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
  85. 141.

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

    // like Array.prototype.filter observable.filter // like Array.prototype.reduce observable.reduce
  86. 142.

    - Observables - function getResizeStream(elem) { return Rx.Observable.create((observer) => {

    const resizeObserver = new ResizeObserver((entries) => { entries.forEach(entry => { drawConfetti(entry); }); }); resizeObserver.observe(elem); }) }
  87. 143.

    - Observables - function getResizeStream(elem) { return Rx.Observable.create((observer) => {

    const resizeObserver = new ResizeObserver((entries) => { entries.forEach(entry => { observer.next(entry); }); }); resizeObserver.observe(elem); }); }
  88. 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
  89. 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
  90. 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
  91. 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
  92. 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
  93. 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
  94. 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
  95. 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
  96. 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
  97. 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
  98. 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
  99. 155.
  100. 156.