Lazy loading content, tracking impressions, animation triggers
- for many years we have utilised scroll handlers to detect when
to trigger such actions. Now modern browsers are supporting
IntersectionObserver, a new API that comes with better performance, and results in cleaner code.
In this talk I will show you what Intersection Observer is,
what type of code it replaces and why it is a better alternative.
Slides from talk given at:
- FEL in London on September 28th, 2017
- Paris.js on September 27th, 2017
- BerlinJS on September 21st, 2017
----
Snips are hiring - join us for an amazing adventure working with AI in the heart of Paris (no remote).
Currently looking for one frontend dev, as well as for many other non web roles:
https://snips.ai/jobs
2. Open source, web perf, tooling, React
Critical css: Penthouse, criticalcss.com
About me
@pocketjoso pocketjoso
Jonas Ohlsson Aden
https://jonassebastianohlsson.com
Lead Front end team
Using Voice AI to make technology disappear
3. Today’s talk
• What is IntersectionObserver❔
• Why you need it 👊
• How to use it (today) 🚀
7. Why is it needed?
We’ve been doing these things for years!
8. Why is it needed?
..without it you end up with
• complex code
• performance issues
more on why: https://github.com/w3c/IntersectionObserver/blob/gh-pages/explainer.md#observing-position
9. Why complex and badly
performing? (historically)
• ‘scroll’ fires too often, needs to be
throttled
useful: https://css-tricks.com/debouncing-throttling-explained-examples/
why getBoundingClientRect is slow https://gist.github.com/paulirish/5d52fb081b3570c81e3a
• useful DOM methods are too slow 🐌, f.e.
DOMElement.getBoundingClientRect
• workarounds requires caching expensive DOM
read values, and then cache invalidation..
11. const observer = new
window.IntersectionObserver(onIntersect)
function onIntersect (entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('element intersecting!')
}
})
}
observer.observe(DOMElement)
👍
in-view monitoring
with IntersectionObserver
12. Why is it needed?
recap
• avoid complex code
• avoid performance issues
more on why: https://github.com/w3c/IntersectionObserver/blob/gh-pages/explainer.md#observing-position
✅
13. IntersectionObserver
performance built in 🏇
• callback doesn’t fire continuously
- and internally uses requestIdleCallback*
• not magic: do keep handler execution quick
• no need for getBoundingClientRect()
- intersection checks handled and optimised by
browser
*so far only in Chrome
14. Why is it needed?
recap
• avoid complex code
• avoid performance issues
✅
✅
15. IntersectionObserver
performance built in 🏇
• callback doesn’t fire continuously
- and internally uses requestIdleCallback*
• not magic: do keep handler execution quick
• no need for getBoundingClientRect()
- intersection checks handled and optimised by
browser
*so far only in Chrome
16. IntersectionObserver
for animations
• don’t use if need continuous updates; tracing
scroll position
-> Parallax effects ❌
• can use if okay with delayed (async) updates
-> enter/exit animation trigger ✅
✅ Just test your use case
17. IntersectionObserver
what else do we need to know?
full API docs: https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
18. in-view detection
via entry.isIntersecting
const observer = new
window.IntersectionObserver(onIntersect)
function onIntersect (entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log('element intersecting!')
}
})
}
observer.observe(DOMElement)
In my opinion the easiest way to do this
19. entry.isIntersecting
• Not whether entry is intersecting or not
• whether the element now intersects more or
less than before
⚠ in earlier version of spec the behaviour was the former; and it’s still the case in polyfills, and early Edge
20. in view detection
via entry.intersectionRatio
const observer = new
window.IntersectionObserver(onIntersect)
function onIntersect (entries) {
entries.forEach(entry => {
if (entry.intersectionRatio > 0) {
console.log('element intersecting!')
}
})
}
observer.observe(DOMElement)
(not enough to just use intersectionRatio)
23. in view detection
via intersectionRatio
const observer = new IntersectionObserver(
onIntersect,
{threshold: [0.01]} // default: [0]
)
function onIntersect (entries) {
entries.forEach(entry => {
if (entry.intersectionRatio > 0) {
console.log('element intersecting!')
}
})
}
observer.observe(DOMElement)
You need to add a custom threshold
24. in view detection
via intersectionRatio
const observer = new IntersectionObserver(
onIntersect,
{threshold: [0.01]} // default: [0]
)
function onIntersect (entries) {
entries.forEach(entry => {
if (entry.intersectionRatio > 0) {
console.log('element intersecting!')
}
})
}
observer.observe(DOMElement)
threshold + intersectionRatio demo: https://codepen.io/pocketjoso/pen/jGqgPB
threshold: array of intersectionRatio values
when crossed triggers onIntersect callback
25. in view detection
via intersectionRatio
const observer = new IntersectionObserver(
onIntersect,
{threshold: [0.01]} // Note: above 0
)
function onIntersect (entries) {
entries.forEach(entry => {
// without 0.01 threshold
// we can get false negatives here
if (entry.intersectionRatio > 0) {
console.log('element intersecting!')
}
})
}
⚠
To avoid false negatives
26. thresholds and callbacks
• onIntersect also fires when observer.observe() is called
• No guarantee of one onIntersect call per threshold -
callback is ~ throttled, time-overlapping calls omitted
• ⚠ Possible to get no callbacks is scrolling past too
quickly
• ✅ Should not be a problem for most use cases
27. determine direction
• entry.boundingClientRect: static read-
only value - we get it for free! 🎉
• together with entry.isIntersecting we
can determine direction
function onIntersect (entries) {
entries.forEach(entry => {
// entry.boundingClientRect
// entry.isIntersecting
}
28. re-use observer
const observer = new
window.IntersectionObserver(onIntersect)
[…document.querySelectorAll('.myClass')]
.forEach(DOMElement => observer.observe(DOMElement))
also possible using multiple observers,
if need different observer configuration
29. observer.unobserve & entry.target
const observer = new
window.IntersectionObserver(onIntersect)
function onIntersect (entries) {
entries.forEach(entry => {
if (entry.isIntersecting) {
// f.e. lazy load image here
// then stop observing.
// entry.target is the DOMElement
// we called observer.observe with
observer.unobserve(entry.target)
}
})
}
[…document.querySelectorAll(‘.lazy-load’)]
.forEach(DOMElement => observer.observe(DOMElement))
30. disconnect
const observer = new
window.IntersectionObserver(onIntersect)
// later - tears down all observing
observer.disconnect()
use case: on navigation (leaving page) in
Single Page Application
31. rootMargin
px or percent
Can use negative margins
const observer = new IntersectionObserver(
onIntersect,
{ rootMargin: '0px 0px 0px 0px' }
)
observer.observe(DOMElement)
rootMargin demo: https://codepen.io/pocketjoso/pen/GMqJYx?editors=0110
32. rootMargin
• Use positive margin to fire onIntersect earlier
- for lazy loading logic
• Could use negative margin to delay onIntersect,
to have more of the element in-view already…
..but better define such “margin” relative to the element:
const observer = new IntersectionObserver(
onIntersect,
{threshold: [0.2]} // update when 20% in-view instead of 0%
)
34. Support strategies
1. Progressive enhancement
2. Polyfill
https://cdn.polyfill.io/v2/polyfill.js?features=IntersectionObserver)
Configure polyfill to poll for changes from css (:hover, animations) and other user interactions:
https://github.com/w3c/IntersectionObserver/tree/master/polyfill#configuring-the-polyfill
41. How we use at
• declarative & easy to use
• Uses IntersectionObserver under the hood
Open source! 🎉
https://snipsco.github.io/react-inview-monitor/
react-inview-monitor
42. Summary
• IntersectionObserver is here -
ready to use today (with a polyfill)
• For in-view detection, it is an easier and
better performing alternative than scroll
listeners