[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"article-intersection-observer-visibility":32,"code:js:true:fj4m81":237,"code:js:true:djuwb5":238,"code:js:true:1s9szvu":239,"code:js:true:1y9zion":240,"code:js:true:1eankri":241,"code:js:true:1f1yxyb":242,"code:js:true:1s6ju5b":243},[4,20],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Build. Learn. Ship.","Practical courses, engineering-grade articles and open-source tools for people who ship.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Structured paths, listening drills and spaced repetition that actually sticks.","amber-400","⌘",[13,15,29],"es",{"courses":17,"paths":17,"articles":18,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":17},2,{"id":33,"slug":34,"title":35,"subtitle":36,"excerpt":37,"coverUrl":38,"locale":13,"readingMinutes":39,"publishedAt":40,"viewCount":41,"likeCount":19,"commentCount":19,"author":42,"vertical":47,"topic":48,"tags":51,"_count":63,"playground":65,"body":67,"bodyMd":219,"seo":220,"translationGroupId":224,"thread":225,"assessments":227,"translations":233,"quiz":235},"019fe660-503a-7133-b7b0-7d5ce9587539","intersection-observer-visibility","Your scroll listener fires on every pixel. `IntersectionObserver` fires when visibility actually changes.",null,"Detecting whether an element is in the viewport by listening to scroll events runs an expensive layout calculation on every frame. `IntersectionObserver` is the browser-native alternative — it fires only when visibility changes, off the main thread, with no scroll handler involved.","\u002Fmedia\u002Fcovers\u002Fintersection-observer-visibility.png",4,"2026-07-19T08:25:59.411Z",35,{"id":43,"name":44,"username":45,"avatarUrl":36,"headline":46},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":49,"name":50},"javascript","JavaScript",[52,54,57,60],{"slug":49,"name":53,"color":36},"Javascript",{"slug":55,"name":56,"color":36},"webdev","Webdev",{"slug":58,"name":59,"color":36},"frontend","Frontend",{"slug":61,"name":62,"color":36},"performance","Performance",{"assessments":64},1,{"slug":34,"title":66},"IntersectionObserver — interactive playground",{"blocks":68,"version":64},[69,73,79,82,85,89,92,96,99,102,105,108,111,114,117,120,124,127,131,134,137,141,144,147,151,154,157,160,164,167,170,173,177,180,183,186,189,192,195,198,201,204,207,210],{"id":70,"html":71,"type":72},"b1","\u003Cp>Here is a pattern I still see in real codebases:\u003C\u002Fp>","paragraph",{"id":74,"code":75,"type":76,"language":77,"highlight":78},"b2","window.addEventListener('scroll', () => {\n  const rect = element.getBoundingClientRect();\n  if (rect.top \u003C window.innerHeight && rect.bottom >= 0) {\n    loadImage(element);\n  }\n});","code","js",[],{"id":80,"html":81,"type":72},"b3","\u003Cp>It works. It&#39;s also firing on every pixel of scroll movement — potentially 60 callbacks per second on a smooth display — and each one calls \u003Ccode>getBoundingClientRect()\u003C\u002Fcode>, which forces a synchronous layout recalculation. You probably added throttling or \u003Ccode>requestAnimationFrame\u003C\u002Fcode> wrapping to soften the blow. You&#39;re still doing layout work on the main thread to answer a question the browser already knows the answer to.\u003C\u002Fp>",{"id":83,"html":84,"type":72},"b4","\u003Cp>\u003Ccode>IntersectionObserver\u003C\u002Fcode> has been baseline since 2019. It&#39;s still underused.\u003C\u002Fp>",{"id":86,"html":87,"text":87,"type":88,"level":31},"b5","What it does","heading",{"id":90,"html":91,"type":72},"b6","\u003Cp>\u003Ccode>IntersectionObserver\u003C\u002Fcode> watches one or more elements and fires a callback \u003Cem>only when their visibility status changes\u003C\u002Fem> — entering the viewport, leaving it, or crossing a fraction threshold you specify. The geometry calculations happen off the main thread; you get called when something meaningful happens.\u003C\u002Fp>",{"id":93,"code":94,"type":76,"language":77,"highlight":95},"b7","const observer = new IntersectionObserver((entries) => {\n  entries.forEach(entry => {\n    if (entry.isIntersecting) {\n      console.log(entry.target, 'entered the viewport');\n    }\n  });\n});\n\nobserver.observe(document.querySelector('.card'));",[],{"id":97,"html":98,"type":72},"b8","\u003Cp>\u003Ccode>entries\u003C\u002Fcode> is an array of \u003Ccode>IntersectionObserverEntry\u003C\u002Fcode> objects — one per observed element that changed state in this tick. \u003Ccode>entry.isIntersecting\u003C\u002Fcode> is \u003Ccode>true\u003C\u002Fcode> when the element entered, \u003Ccode>false\u003C\u002Fcode> when it left.\u003C\u002Fp>",{"id":100,"html":101,"type":72},"b9","\u003C!-- playground:start -->",{"id":103,"html":104,"text":104,"type":88,"level":31},"b10","🎮 Try it yourself",{"id":106,"html":107,"type":72},"b11","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fdaily-post-dev.netlify.app\u002Fposts\u002F2026-07-19-intersection-observer-visibility\u002Fplayground\u002F\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":109,"html":110,"type":72},"b12","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":112,"html":113,"type":72},"b13","\u003C!-- playground:end -->",{"id":115,"html":116,"text":116,"type":88,"level":31},"b14","Lazy loading an image",{"id":118,"html":119,"type":72},"b15","\u003Cp>The classic use case. No scroll event, no position math:\u003C\u002Fp>",{"id":121,"code":122,"type":76,"language":77,"highlight":123},"b16","const lazyImages = document.querySelectorAll('img[data-src]');\n\nconst imageObserver = new IntersectionObserver((entries) => {\n  entries.forEach(entry => {\n    if (!entry.isIntersecting) return;\n\n    const img = entry.target;\n    img.src = img.dataset.src;\n    imageObserver.unobserve(img); \u002F\u002F done watching this one\n  });\n});\n\nlazyImages.forEach(img => imageObserver.observe(img));",[],{"id":125,"html":126,"type":72},"b17","\u003Cp>The observer fires once when each image enters the viewport. After setting \u003Ccode>src\u003C\u002Fcode>, \u003Ccode>unobserve\u003C\u002Fcode> drops the element from the watch list. No cleanup loop, no residual listener, no memory leak.\u003C\u002Fp>",{"id":128,"html":129,"text":130,"type":88,"level":31},"b18","\u003Ccode>threshold\u003C\u002Fcode> and \u003Ccode>rootMargin\u003C\u002Fcode>","threshold and rootMargin",{"id":132,"html":133,"type":72},"b19","\u003Cp>Two options control exactly when the callback fires.\u003C\u002Fp>",{"id":135,"html":136,"type":72},"b20","\u003Cp>\u003Cstrong>\u003Ccode>threshold\u003C\u002Fcode>\u003C\u002Fstrong> is a number between 0 and 1 — the fraction of the element that must be visible. The default is \u003Ccode>0\u003C\u002Fcode>, which fires the moment a single pixel enters the viewport. \u003Ccode>threshold: 1\u003C\u002Fcode> waits until the element is fully visible. Pass an array to fire at multiple milestones:\u003C\u002Fp>",{"id":138,"code":139,"type":76,"language":77,"highlight":140},"b21","const observer = new IntersectionObserver(callback, {\n  threshold: [0, 0.25, 0.5, 0.75, 1],\n});",[],{"id":142,"html":143,"type":72},"b22","\u003Cp>\u003Ccode>entry.intersectionRatio\u003C\u002Fcode> tells you the actual fraction at the moment of the callback — useful for video autoplay, progressive reveal animations, or impression-depth analytics.\u003C\u002Fp>",{"id":145,"html":146,"type":72},"b23","\u003Cp>\u003Cstrong>\u003Ccode>rootMargin\u003C\u002Fcode>\u003C\u002Fstrong> works like a CSS margin around the viewport boundary, shifting where &quot;visible&quot; begins:\u003C\u002Fp>",{"id":148,"code":149,"type":76,"language":77,"highlight":150},"b24","const observer = new IntersectionObserver(callback, {\n  rootMargin: '0px 0px 300px 0px', \u002F\u002F fire 300px before entering from the bottom\n});",[],{"id":152,"html":153,"type":72},"b25","\u003Cp>This is the correct tool for preloading. Trigger the network request 300px before the element reaches the viewport, so it&#39;s ready when the user gets there — without any manual distance math.\u003C\u002Fp>",{"id":155,"html":156,"text":156,"type":88,"level":31},"b26","Analytics impression tracking",{"id":158,"html":159,"type":72},"b27","\u003Cp>Knowing whether a user \u003Cem>actually saw\u003C\u002Fem> a piece of content, versus just scrolling past it, is a common product requirement. With a scroll listener you&#39;re sampling at intervals and accepting inaccuracy. With \u003Ccode>IntersectionObserver\u003C\u002Fcode>:\u003C\u002Fp>",{"id":161,"code":162,"type":76,"language":77,"highlight":163},"b28","const impressionObserver = new IntersectionObserver((entries) => {\n  entries.forEach(entry => {\n    if (!entry.isIntersecting) return;\n    analytics.track('impression', {\n      id: entry.target.dataset.id,\n      ratio: entry.intersectionRatio,\n    });\n    impressionObserver.unobserve(entry.target);\n  });\n}, { threshold: 0.5 }); \u002F\u002F at least half the element must be visible\n\ndocument.querySelectorAll('[data-track]').forEach(el => impressionObserver.observe(el));",[],{"id":165,"html":166,"type":72},"b29","\u003Cp>The \u003Ccode>threshold: 0.5\u003C\u002Fcode> option means a fast scroll past an element doesn&#39;t count — the user had to slow down enough for half of it to enter view. One observer, zero scroll event listeners, accurate data.\u003C\u002Fp>",{"id":168,"html":169,"text":169,"type":88,"level":31},"b30","Cleanup in components",{"id":171,"html":172,"type":72},"b31","\u003Cp>In React, Vue, or any component that mounts and unmounts, disconnect the observer on teardown:\u003C\u002Fp>",{"id":174,"code":175,"type":76,"language":77,"highlight":176},"b32","useEffect(() => {\n  const observer = new IntersectionObserver(callback);\n  observer.observe(ref.current);\n\n  return () => observer.disconnect(); \u002F\u002F stops watching all elements\n}, []);",[],{"id":178,"html":179,"type":72},"b33","\u003Cp>\u003Ccode>disconnect()\u003C\u002Fcode> removes all observed elements at once. If you have a single long-lived observer watching multiple elements and only want to stop tracking one, \u003Ccode>unobserve(element)\u003C\u002Fcode> does that without affecting the rest.\u003C\u002Fp>",{"id":181,"html":182,"type":72},"b34","\u003C!-- quiz:start -->",{"id":184,"html":185,"text":185,"type":88,"level":31},"b35","🧠 Test yourself",{"id":187,"html":188,"type":72},"b36","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fdaily-post-dev.netlify.app\u002Fquiz\u002Ftake.html?post=2026-07-19-intersection-observer-visibility\">Take the 6-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":190,"html":191,"type":72},"b37","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":193,"html":194,"type":72},"b38","\u003C!-- quiz:end -->",{"id":196,"html":197,"text":197,"type":88,"level":31},"b39","The takeaway",{"id":199,"html":200,"type":72},"b40","\u003Cp>\u003Ccode>IntersectionObserver\u003C\u002Fcode> answers the question &quot;is this element visible?&quot; without running on every scroll tick. The main thread is not involved — the browser handles the geometry internally and calls your callback only when the answer changes.\u003C\u002Fp>",{"id":202,"html":203,"type":72},"b41","\u003Cp>Search your codebase for scroll event listeners that contain \u003Ccode>getBoundingClientRect\u003C\u002Fcode>. Every one of them is a candidate for replacement. The handler runs less often, the layout thrash disappears, and the intent is clearer in the code: you&#39;re watching for visibility, and the observer is the right name for that.\u003C\u002Fp>",{"id":205,"type":206},"b42","divider",{"id":208,"html":209,"type":72},"b43","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":211,"type":212,"items":213,"ordered":18},"b44","list",[214,215,216,217,218],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","💼 \u003Cstrong>LinkedIn\u003C\u002Fstrong> — \u003Ca href=\"https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F\">linkedin.com\u002Fin\u002Fparsa-jiravand\u003C\u002Fa>","✉️ \u003Cstrong>Email\u003C\u002Fstrong> (work &amp; contract inquiries): \u003Ca href=\"mailto:bestpractice2026@gmail.com\">bestpractice2026@gmail.com\u003C\u002Fa>","Here is a pattern I still see in real codebases:\n\n```js\nwindow.addEventListener('scroll', () => {\n  const rect = element.getBoundingClientRect();\n  if (rect.top \u003C window.innerHeight && rect.bottom >= 0) {\n    loadImage(element);\n  }\n});\n```\n\nIt works. It's also firing on every pixel of scroll movement — potentially 60 callbacks per second on a smooth display — and each one calls `getBoundingClientRect()`, which forces a synchronous layout recalculation. You probably added throttling or `requestAnimationFrame` wrapping to soften the blow. You're still doing layout work on the main thread to answer a question the browser already knows the answer to.\n\n`IntersectionObserver` has been baseline since 2019. It's still underused.\n\n## What it does\n\n`IntersectionObserver` watches one or more elements and fires a callback *only when their visibility status changes* — entering the viewport, leaving it, or crossing a fraction threshold you specify. The geometry calculations happen off the main thread; you get called when something meaningful happens.\n\n```js\nconst observer = new IntersectionObserver((entries) => {\n  entries.forEach(entry => {\n    if (entry.isIntersecting) {\n      console.log(entry.target, 'entered the viewport');\n    }\n  });\n});\n\nobserver.observe(document.querySelector('.card'));\n```\n\n`entries` is an array of `IntersectionObserverEntry` objects — one per observed element that changed state in this tick. `entry.isIntersecting` is `true` when the element entered, `false` when it left.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fdaily-post-dev.netlify.app\u002Fposts\u002F2026-07-19-intersection-observer-visibility\u002Fplayground\u002F)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## Lazy loading an image\n\nThe classic use case. No scroll event, no position math:\n\n```js\nconst lazyImages = document.querySelectorAll('img[data-src]');\n\nconst imageObserver = new IntersectionObserver((entries) => {\n  entries.forEach(entry => {\n    if (!entry.isIntersecting) return;\n\n    const img = entry.target;\n    img.src = img.dataset.src;\n    imageObserver.unobserve(img); \u002F\u002F done watching this one\n  });\n});\n\nlazyImages.forEach(img => imageObserver.observe(img));\n```\n\nThe observer fires once when each image enters the viewport. After setting `src`, `unobserve` drops the element from the watch list. No cleanup loop, no residual listener, no memory leak.\n\n## `threshold` and `rootMargin`\n\nTwo options control exactly when the callback fires.\n\n**`threshold`** is a number between 0 and 1 — the fraction of the element that must be visible. The default is `0`, which fires the moment a single pixel enters the viewport. `threshold: 1` waits until the element is fully visible. Pass an array to fire at multiple milestones:\n\n```js\nconst observer = new IntersectionObserver(callback, {\n  threshold: [0, 0.25, 0.5, 0.75, 1],\n});\n```\n\n`entry.intersectionRatio` tells you the actual fraction at the moment of the callback — useful for video autoplay, progressive reveal animations, or impression-depth analytics.\n\n**`rootMargin`** works like a CSS margin around the viewport boundary, shifting where \"visible\" begins:\n\n```js\nconst observer = new IntersectionObserver(callback, {\n  rootMargin: '0px 0px 300px 0px', \u002F\u002F fire 300px before entering from the bottom\n});\n```\n\nThis is the correct tool for preloading. Trigger the network request 300px before the element reaches the viewport, so it's ready when the user gets there — without any manual distance math.\n\n## Analytics impression tracking\n\nKnowing whether a user *actually saw* a piece of content, versus just scrolling past it, is a common product requirement. With a scroll listener you're sampling at intervals and accepting inaccuracy. With `IntersectionObserver`:\n\n```js\nconst impressionObserver = new IntersectionObserver((entries) => {\n  entries.forEach(entry => {\n    if (!entry.isIntersecting) return;\n    analytics.track('impression', {\n      id: entry.target.dataset.id,\n      ratio: entry.intersectionRatio,\n    });\n    impressionObserver.unobserve(entry.target);\n  });\n}, { threshold: 0.5 }); \u002F\u002F at least half the element must be visible\n\ndocument.querySelectorAll('[data-track]').forEach(el => impressionObserver.observe(el));\n```\n\nThe `threshold: 0.5` option means a fast scroll past an element doesn't count — the user had to slow down enough for half of it to enter view. One observer, zero scroll event listeners, accurate data.\n\n## Cleanup in components\n\nIn React, Vue, or any component that mounts and unmounts, disconnect the observer on teardown:\n\n```js\nuseEffect(() => {\n  const observer = new IntersectionObserver(callback);\n  observer.observe(ref.current);\n\n  return () => observer.disconnect(); \u002F\u002F stops watching all elements\n}, []);\n```\n\n`disconnect()` removes all observed elements at once. If you have a single long-lived observer watching multiple elements and only want to stop tracking one, `unobserve(element)` does that without affecting the rest.\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 6-question quiz →](https:\u002F\u002Fdaily-post-dev.netlify.app\u002Fquiz\u002Ftake.html?post=2026-07-19-intersection-observer-visibility)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n## The takeaway\n\n`IntersectionObserver` answers the question \"is this element visible?\" without running on every scroll tick. The main thread is not involved — the browser handles the geometry internally and calls your callback only when the answer changes.\n\nSearch your codebase for scroll event listeners that contain `getBoundingClientRect`. Every one of them is a candidate for replacement. The handler runs less often, the layout thrash disappears, and the intent is clearer in the code: you're watching for visibility, and the observer is the right name for that.\n\n---\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)\n- 💼 **LinkedIn** — [linkedin.com\u002Fin\u002Fparsa-jiravand](https:\u002F\u002Fwww.linkedin.com\u002Fin\u002Fparsa-jiravand\u002F)\n- ✉️ **Email** (work & contract inquiries): [bestpractice2026@gmail.com](mailto:bestpractice2026@gmail.com)",{"title":221,"canonical":222,"description":223},"Your scroll listener fires on every pixel. `IntersectionObserver` fire","https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fintersection-observer-visibility","Detecting whether an element is in the viewport by listening to scroll events runs an expensive layout calculation on every frame. `IntersectionObserver` is the browser-native alte","019fe660-503a-7133-b7b0-80bdca1ae636",{"id":226,"locked":18},"019fe660-5759-7723-8d7f-95aa26fa1247",[228],{"id":229,"slug":34,"title":230,"_count":231},"019fe776-5f89-739f-b169-bbce60a3949d","IntersectionObserver: visibility without a scroll listener",{"questions":232},6,[234],{"locale":13,"slug":34},{"id":229,"slug":34,"title":230,"_count":236,"questionCount":232},{"questions":232},"\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">window\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">addEventListener\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'scroll'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> rect \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> element\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">getBoundingClientRect\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>rect\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>top \u003Cspan class=\"shj-oper\">&lt;\u003C\u002Fspan> window\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>innerHeight \u003Cspan class=\"shj-oper\">&#38;&#38;\u003C\u002Fspan> rect\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>bottom \u003Cspan class=\"shj-oper\">&gt;=\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-func\">loadImage\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>element\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> observer \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">IntersectionObserver\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>entries\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  entries\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">forEach\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>entry \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>isIntersecting\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n      console\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">log\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>target\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'entered the viewport'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n    \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\nobserver\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">observe\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>document\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">querySelector\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'.card'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003Cdiv>13\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> lazyImages \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> document\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">querySelectorAll\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'img[data-src]'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> imageObserver \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">IntersectionObserver\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>entries\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  entries\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">forEach\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>entry \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">!\u003C\u002Fspan>entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>isIntersecting\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan>;\n\n    \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> img \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>target;\n    img\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>src \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> img\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>dataset\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>src;\n    imageObserver\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">unobserve\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>img\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F done watching this one\n\u003C\u002Fspan>  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\nlazyImages\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">forEach\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>img \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> imageObserver\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">observe\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>img\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> observer \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">IntersectionObserver\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>callback\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  threshold\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-num\">0\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0.25\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0.5\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0.75\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-num\">1\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> observer \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">IntersectionObserver\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>callback\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  rootMargin\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-str\">'0px 0px 300px 0px'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-cmnt\">\u002F\u002F fire 300px before entering from the bottom\n\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003Cdiv>7\u003C\u002Fdiv>\u003Cdiv>8\u003C\u002Fdiv>\u003Cdiv>9\u003C\u002Fdiv>\u003Cdiv>10\u003C\u002Fdiv>\u003Cdiv>11\u003C\u002Fdiv>\u003Cdiv>12\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> impressionObserver \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">IntersectionObserver\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>entries\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  entries\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">forEach\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>entry \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n    \u003Cspan class=\"shj-kwd\">if\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">!\u003C\u002Fspan>entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>isIntersecting\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan>;\n    analytics\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">track\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'impression'\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n      id\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>target\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>dataset\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>id\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n      ratio\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>intersectionRatio\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan>\n    \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n    impressionObserver\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">unobserve\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>entry\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>target\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan> threshold\u003Cspan class=\"shj-oper\">:\u003C\u002Fspan> \u003Cspan class=\"shj-num\">0.5\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F at least half the element must be visible\n\u003C\u002Fspan>\ndocument\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">querySelectorAll\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-str\">'[data-track]'\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">forEach\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>el \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> impressionObserver\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">observe\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>el\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>","\u003Cdiv class=\"shj shj-lang-js shj-multiline\" data-lang=\"js\">\u003Cdiv class=\"shj-scroll\">\u003Cdiv class=\"shj-numbers\">\u003Cdiv>1\u003C\u002Fdiv>\u003Cdiv>2\u003C\u002Fdiv>\u003Cdiv>3\u003C\u002Fdiv>\u003Cdiv>4\u003C\u002Fdiv>\u003Cdiv>5\u003C\u002Fdiv>\u003Cdiv>6\u003C\u002Fdiv>\u003C\u002Fdiv>\u003Cdiv class=\"shj-code\">\u003Cspan class=\"shj-func\">useEffect\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">{\u003C\u002Fspan>\n  \u003Cspan class=\"shj-kwd\">const\u003C\u002Fspan> observer \u003Cspan class=\"shj-oper\">=\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">new\u003C\u002Fspan> \u003Cspan class=\"shj-class\">IntersectionObserver\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>callback\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n  observer\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">observe\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>ref\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>current\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\n\n  \u003Cspan class=\"shj-kwd\">return\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan> \u003Cspan class=\"shj-kwd\">=&gt;\u003C\u002Fspan> observer\u003Cspan class=\"shj-oper\">.\u003C\u002Fspan>\u003Cspan class=\"shj-func\">disconnect\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">(\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>; \u003Cspan class=\"shj-cmnt\">\u002F\u002F stops watching all elements\n\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">}\u003C\u002Fspan>\u003Cspan class=\"shj-oper\">,\u003C\u002Fspan> \u003Cspan class=\"shj-bracket\">[\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">]\u003C\u002Fspan>\u003Cspan class=\"shj-bracket\">)\u003C\u002Fspan>;\u003C\u002Fdiv>\u003C\u002Fdiv>\u003C\u002Fdiv>"]