Sometimes it makes sense for sites want to treat things differently
based on whether the user has seen them. For example, I like sites
that highlight new comments
(ex: EA Forum and LessWrong) and I'd like them even better if comments
didn't lose their "highlighted" status in cases where I hadn't
scrolled them into view. In writing my Mastodon client,
Shrubgrazer, I wanted a version
of this so it would show me posts that I hadn't seen before. The
implementation is a bit fussy, so I wanted to write up a bit on what
approach I took.
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.
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:
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.
Sometimes it makes sense for sites want to treat things differently based on whether the user has seen them. For example, I like sites that highlight new comments (ex: EA Forum and LessWrong) and I'd like them even better if comments didn't lose their "highlighted" status in cases where I hadn't scrolled them into view. In writing my Mastodon client, Shrubgrazer, I wanted a version of this so it would show me posts that I hadn't seen before. The implementation is a bit fussy, so I wanted to write up a bit on what approach I took.
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
: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:
What does our
handle_intersect
callback do? We maintain two sets of element IDs,onscreen_top
andonscreen_bottom
for keeping track of what is currently onscreen. The callback keeps those sets current, and also starts the 500ms timer: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: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.