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

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

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

stefan judis

May 23, 2017
Tweet

More Decks by stefan judis

Other Decks in Technology

Transcript

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

    Source, Performance and Accessibility ❤ @stefanjudis
  2. 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
  3. 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
  4. 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); } }); });
  5. 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); } }); });
  6. - 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)); })();
  7. - 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)); })();
  8. - 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)); })();
  9. (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 -
  10. - 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
  11. - 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
  12. - 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
  13. // 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/
  14. - Intersection Observer - threshold = 0 entry => {

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

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

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

    => { console.log(entry.isIntersecting) // true console.log(entry.intersectionRatio) // sth. around 0.5 } viewport
  18. - 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); }
  19. - 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); }
  20. - Intersection Observer - * Incompatibilities between spec and implementations

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

    github.com/WICG/IntersectionObserver/issues/222 github.com/WICG/IntersectionObserver/issues/211 Resolved since Oct 17, 2017
  22. - Intersection Observer - (function() { // Intersection Observer code

    // third-party implementation // No interface to hook into // No access to the implementation // (ಥ_ಥ) (ಥ_ಥ) (ಥ_ಥ) })();
  23. - 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); })();
  24. (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 -
  25. (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 -
  26. (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 -
  27. 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 -
  28. 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 -
  29. 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 -
  30. 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 -
  31. - 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
  32. - 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')
  33. - 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' ); }
  34. - User Timing API - w3c.github.io/user-timing/ window.performance.getEntriesByType('mark') [ { duration:

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

    5.9900000000016 entryType: 'measure' name: 'cornify_processing_time' startTime: 46002.34500000001 }, ... ]
  36. - 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
  37. (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 -
  38. (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 -
  39. - 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
  40. - 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']});
  41. - 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']});
  42. - 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']});
  43. - 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']});
  44. - 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
  45. - 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
  46. - 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']})
  47. - 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']})
  48. - Resize Observer - (function() { const resizeObserver = new

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

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

    => { drawConfetti(entry); }); }); [...document.querySelectorAll('.hall-schedule__title')] .forEach(desc => resizeObserver.observe(desc)); })(); - Resize Observer -
  51. - 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
  52. - 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);
  53. - 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);
  54. element initial call resize resize resize resize resize resize -

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

    resize resize resize opening transition closing transition
  56. - Event streams - }); let isFirst = true; onResize(entry

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

    => { if (isFirst) { isFirst = false; return; } drawConfetti(entry);
  58. - 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; }
  59. - 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
  60. - 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
  61. - 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
  62. - Observables - It's a collection! // like Array.prototype.map observable.map

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

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

    const resizeObserver = new ResizeObserver((entries) => { entries.forEach(entry => { observer.next(entry); }); }); resizeObserver.observe(elem); }); }
  65. - 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
  66. - 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
  67. - 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
  68. - 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
  69. - 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
  70. - 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
  71. - 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
  72. - 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
  73. - 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
  74. - 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
  75. - 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