Detecting What's Been Seen |
November 23rd, 2023 |
shrubgrazer, tech |
The code is on github, and it counts posts as viewed if both the top of the post and bottom have been on screen for at least half a second. Specifically, whenever the top or bottom of a post enters the viewport it sets a 500ms timer, and if when the timer fires it's still within the viewport it keeps a record client side. If this now means that both the top and bottom have met the criteria it sends a beacon back so the server can track the entry as viewed.
Go back 4-7 years and this would have required a scroll listener, using a ton of CPU, but modern browsers now support the IntersectionObserver API. This lets us get callbacks whenever an entry enters or leaves the viewport.
I start by creating an IntersectionObserver
:
const observer = new IntersectionObserver( handle_intersect, { root: null, threshold: [0, 1], });
We haven't told it which elements to observe yet, but once we do it will call
the handle_intersect
callback anytime those elements
fully enter or fully exit the viewport.
Each entry has an element at the very top and very bottom, and to start tracking the entry we tell our observer about them:
observer.observe(entry_top); observer.observe(entry_bottom);
What does our handle_intersect
callback do? We maintain
two sets of element IDs, onscreen_top
and
onscreen_bottom
for keeping track of what is currently
onscreen. The callback keeps those sets current, and also starts the
500ms timer:
function handle_intersect(entries, observer) { for (let entry of entries) { const target = entry.target; const id = target.getAttribute("id"); const is_bottom = target.classList.contains("bottom"); const onscreen_set = is_bottom ? onscreen_bottom : onscreen_top; if (entry.intersectionRatio > 0.99) { onscreen_set.add(id); window.setTimeout(function() { onscreen_timeout( target, post_id, is_bottom, onscreen_set); }, 500); start_observation_timer(target); } else if (entry.intersectionRatio < 0.01) { onscreen_set.delete(id); } } }
What does onscreen_timeout
do? It checks whether the
element is still onscreen, and if it's not then it does nothing. This
covers things like fling scrolling where something has been onscreen
for such a short time that it really hasn't been seen. Otherwise, if
the element is still onscreen, it marks the element as viewed and
stops tracking it. And if now both the top and bottom of the entry
have been viewed it tells the server about it:
function onscreen_timeout( target, post_id, is_bottom, onscreen_set) { if (!onscreen_set.has(post_id)) { // Element left the screen too quickly, // don't track it as being onscreen. return; } observer.unobserve(target); if (is_bottom) { viewed_bottom.add(post_id); } else { viewed_top.add(post_id); } if (viewed_top.has(post_id) && viewed_bottom.has(post_id)) { send_view_ping(post_id); viewed_top.delete(post_id); viewed_bottom.delete(post_id); } }
While Shrubgrazer hasn't had wide usage (I suspect I'm the only user, since it takes some work to host and I'm not hosting for anyone else) this has worked well for me. It makes the browser do almost all the work, so it's very fast.
Comment via: facebook, lesswrong