Closed jonathanmayer closed 4 years ago
Here's a code snippet using Intersection Observer
that seems to work well provided we know a priori which links we want to observe (and we define an isVisible
function that checks for visibility: hidden
and friends):
function handleIntersection(entries, observer) {
entries.forEach(entry => {
const {isIntersecting, target} = entry;
if (isIntersecting
&& !observedLinks.has(target)
&& isVisible(target)
) {
observedLinks.add(target);
if (testForMatch(urlMatcher, target.href, target)) {
const {width, height} = entry.boundingClientRect;
sendMessageToBg("WebScience.linkExposureInitial", {
href: target.href,
size: {
width,
height
}
});
}
observer.unobserve(target);
}
});
}
const options = { threshold: 1 };
const observedLinks = new WeakSet();
const observer = new IntersectionObserver(handleIntersection, options);
const aElements = Array.from(document.body.querySelectorAll("a[href]"));
for (const aElement of aElements) {
observer.observe(aElement);
}
I don't have any good ideas at the moment for <a>
elements that are added dynamically like in a Twitter feed, but I'll think on it more, and I've asked some others for their thoughts on it.
@biancadanforth Thanks for the code snippet. I have previously tried a very similar approach but it didn't trigger all the intersection events I expected from Twitter feed. Following is the function that I used to observe elements of interest. It returns a promise that resolves to first entry:
/**
* Function to observe intersection of dom elements with viewport
* @param {DOMElement} targetElement element to observe for intersection with viewport
* @param {*} threshold intersection ratio
* @returns promise that resolves to element when it intersects with viewport
*/
function observeElement(targetElement, threshold) {
new Promise((resolve, reject) => {
const observerOptions = {
root: null, // Document viewport
rootMargin: "0px",
threshold // Visible amount of item shown in relation to root. 1.0 dictates that every pixel of element is visible.
};
const observer = new IntersectionObserver((entries, observer) => {
targetElement.isObserved = true;
if (
!entries[0].isIntersecting ||
entries[0].intersectionRatio < threshold
) {
return;
}
observer.disconnect();
return resolve(entries[0]);
}, observerOptions);
observer.observe(targetElement);
});
}
A few thoughts on @PranayAnchuri's snippet, in roughly linear order:
threshold
parameter and the return value, and the type of the targetElement
parameter should be Element
or HTMLElement
.IntersectionObserver
with multiple observed Element
s seems clearly preferable to the approach of one IntersectionObserver
per observed Element
. The latter approach has unnecessary memory and computation overhead.Promise
for each observed element also seems clunky. Using one callback function and data structure (as @biancadanforth implemented) is lighter weight and more readable.return
in the IntersectionObserver
callback is unnecessary.Edit: chatted with @PranayAnchuri. Here's a sketch of an overall approach.
WeakMap
to associate data with link elements, rather than using DOM expando attributes.document.body.getElementsByTagName
. (I think this might be a bit better performance than periodically calling document.body.querySelectorAll
. That should also work fine.)WeakMap
.
b. Test the link URL for a match. If there is no match, store in the WeakMap
that this is a link to ignore, and move to the next link element.
c. Store in the WeakMap
that the visibility for this link is unknown.
d. Add the link element to the IntersectionObserver
.IntersectionObserver
callback fires:
a. If a link element changes visibility state to visible, mark that it’s currently visible and the timestamp.
b. If a link element changes visibility state from visible to not visible, mark that it’s currently not visible, calculate the visibility duration, and update the total visibility duration in the WeakMap
.
WeakMap
, do steps 3.a-3.d.
b. If there is a link element that has been visible above a certain threshold period of time, send the link URL and element dimensions to the background page, remove the link from the IntersectionObserver
, and mark the link as a link to ignore.This hybrid approach would get us the native efficiency and accuracy of using an IntersectionObserver
and the DOM update handling of a periodic timer.
A couple relevant API quirks that I stumbled across while thinking this through:
IntersectionObserverCallback
function is queued when an element first gets observed, providing an initial IntersectionObserverEntry
for that element. We don't have to separately check a link element's visibility before adding it to an IntersectionObserver
.It's a good idea to only observe link elements whose href
attribute is a match. That would be an improvement on my implementation.
I only skimmed over your revised approach, Jonathan, but on the surface it seems like a pretty good one.
Re: dynamically added link elements; I like your idea of a live list approach if that will work, as unfortunately it looks like either polling or setting up a Mutation Observer
for newly added links are the only alternatives there without modifying native code (HTMLAnchorElement::BindToTree
) (thanks to @emilio).
Also, a related but separate issue: I don't think any of these approaches will cover anchor elements in the shadow DOM. Perhaps it's worth checking to see how prevalent use of a shadow DOM is for some of the most bleeding edge news sites of interest (e.g. Twitter, Facebook, ...)? My gut tells me this is pretty experimental still and not in wide use, but in case you want to check: @emilio recommended using openOrClosedShadowRoot
to see if an element has a shadow root attached. If it does, you can call querySelectorAll
on the shadow root to get all the anchor elements in that subtree.
Also, a related but separate issue: I don't think any of these approaches will cover anchor elements in the shadow DOM. Perhaps it's worth checking to see how prevalent use of a shadow DOM is for some of the most bleeding edge news sites of interest (e.g. Twitter, Facebook, ...)? My gut tells me this is pretty experimental still and not in wide use, but in case you want to check: @emilio recommended using
openOrClosedShadowRoot
to see if an element has a shadow root attached. If it does, you can callquerySelectorAll
on the shadow root to get all the anchor elements in that subtree.
Very good point. I just checked Facebook's feed, Twitter's feed, and Google's search results page to make sure there isn't any use of shadow DOM. The Chrome usage statistics for Element.attachShadow()
also reflect low adoption so far. I agree that this isn't an issue for now.
I was just profiling master
, and it looks like this has been implemented? Mostly in this commit.
If I zoom in on one of the red areas (that's jank), the Intersection Observer
work (the yellow slivers on the bottom half of the screenshot) doesn't even coincide with the jank. In other words, this is an extremely performant solution!
Excellent! We try to move fast 😃
Done.
Recommendations from @biancadanforth.
https://github.com/citp/web-science/pull/43#pullrequestreview-331674942
https://github.com/citp/web-science/pull/43#discussion_r357494412