-
Notifications
You must be signed in to change notification settings - Fork 17
Make Fathom extraction more performant #319
Comments
Note: As is, Also, it may include elements for consideration that are offscreen and therefore not practically visible to the user. Fathom's Fathom v3.1.0 /**
* Return whether an element is practically visible, considing things like 0
* size or opacity, ``display: none``, and ``visibility: hidden``.
*/
export function isVisible(fnodeOrElement) {
const element = toDomElement(fnodeOrElement);
for (const ancestor of ancestors(element)) {
const style = getComputedStyle(ancestor);
if (style.visibility === 'hidden' ||
style.display === 'none' ||
style.opacity === '0' ||
style.width === '0' ||
style.height === '0') {
return false;
} else {
// It wasn't hidden based on a computed style. See if it's
// offscreen:
const rect = element.getBoundingClientRect();
const frame = element.ownerDocument.defaultView; // window or iframe
if ((rect.right + frame.scrollX < 0) ||
(rect.bottom + frame.scrollY < 0)) {
return false;
}
}
}
return true;
} Probably what this issue boils down to is to use the |
Compared to a baseline profile[1] of Price Tracker's current 'isVisible' implementation (on which Fathom's 'isVisible' method is based), this clickability approach offers a 53% (146 ms) reduction in Fathom-related jank[2] for the same locally hosted sample page. This is largely due to removing the use of the 'ancestors' Fathom method in 'isVisible'[3], which was performing a lot of redundant layout accesses (and triggering a lot of layout flushes) for the same elements. Also, at least in an extension application, DOM accesses (e.g. repeatedly getting the next 'parentNode' in 'ancestors') are very expensive due to X-Rays[4]. It should be noted that this implementation can still benefit from memoization, as the same element (e.g. 'div') could be considered for multiple different 'type's[5]. [1]: https://perfht.ml/30wkWT7 [2]: https://perfht.ml/2Y5FCQ1 [3]: mozilla/price-tracker#319 [4]: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/Xray_vision" [5]: https://mozilla.github.io/fathom/glossary.html
Compared to a baseline profile[1] of Price Tracker's current 'isVisible' implementation (on which Fathom's 'isVisible' method is based), this clickability approach offers a 53% (146 ms) reduction in Fathom-related jank[2] for the same locally hosted sample page. This is largely due to removing the use of the 'ancestors' Fathom method in 'isVisible'[3], which was performing a lot of redundant layout accesses (and triggering a lot of layout flushes) for the same elements. Also, at least in an extension application, DOM accesses (e.g. repeatedly getting the next 'parentNode' in 'ancestors') are very expensive due to X-Rays[4]. Notes: * If the proposal to the W3C CSS Working Group[5] is implemented, this clickability approach could forgo the workaround and see as much as 81% (374 ms) reduction in Fathom-related jank[3]. * This implementation can still benefit from memoization, as the same element (e.g. 'div') could be considered for multiple different 'type's[6]. [1]: https://perfht.ml/30wkWT7 [2]: https://perfht.ml/2Y5FCQ1 [3]: mozilla/price-tracker#319 [4]: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/Xray_vision" [5]: w3c/csswg-drafts#4122 [6]: https://mozilla.github.io/fathom/glossary.html
…rmance Compared to a baseline profile[1] of Price Tracker's current 'isVisible' implementation (on which Fathom's 'isVisible' method is based), this clickability approach offers a 53% (146 ms) reduction in Fathom-related jank[2] for the same locally hosted sample page. This is largely due to removing the use of the 'ancestors' Fathom method in 'isVisible'[3], which was performing a lot of redundant layout accesses (and triggering a lot of layout flushes) for the same elements. Also, at least in an extension application, DOM accesses (e.g. repeatedly getting the next 'parentNode' in 'ancestors') are very expensive due to X-Rays[4]. Notes: * If the proposal to the W3C CSS Working Group[5] is implemented, this clickability approach could forgo the workaround and see as much as 81% (374 ms) reduction in Fathom-related jank[3]. * This implementation can still benefit from memoization, as the same element (e.g. 'div') could be considered for multiple different 'type's[6]. [1]: https://perfht.ml/30wkWT7 [2]: https://perfht.ml/2Y5FCQ1 [3]: mozilla/price-tracker#319 [4]: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/Xray_vision" [5]: w3c/csswg-drafts#4122 [6]: https://mozilla.github.io/fathom/glossary.html
Updating
|
Hi @biancadanforth, I took a read through this and a quick peek at the Price Tracker code. A few questions:
It's possible to decrease the likelihood of causing synchronous layout flushes, and making
We're still going to get burned if the page somehow runs any JS that dirties the DOM before the setTimeout runs, but if it hasn't, then calls to Assuming you can get pretty cheap calls to getBoundingClientRect(), you might be able to use those values plus the scroll offset of the window to calculate if an item is collapsed (0x0 dimensions - this will also cover display: none), invisible (visibility: hidden, or opacity: 0), or off screen (x and y values along with width and height place it outside the display port). If that's still not cheap enough, then perhaps we could expose some kind of special power to your WebExtension content script to get the information it needs more directly from Layout - though we'd probably want to confer with the Layout team to see how easy that is.
This can add some interesting bugs where the document state might change underneath you during your scan, but if you're willing to live with those bugs, this might be a fallback plan worth considering. |
Thank you very much @mikeconley for taking the time to look at this and offer your thoughts.
This is a good observation. Certainly if we waited to run Fathom (i.e. Originally, the decision to run this at While in the case of the Price Tracker application of Fathom, we probably could wait until Additionally, when this extraction code is run is not handled directly by Fathom but by the application author, as how that is set up depends on the application context. While we can certainly make recommendations, I am especially interested in where we can make performance gains (and to maximal effect) in the Fathom code itself (such as Fathom's
It took me a while to get this approach working, as Updating
|
Unfortunately, the previous commit's approach based on clickability proved untenable.[1] A distant second sync solution is to early return where possible and use the cheapest styles first (the second implementation approach[2]), which reduces Fathom jank by 13%*, though there are a couple differences: * No 'getBoxQuads'. This is an experimental API only enabled on Nightly. * Checks if the element is off-screen. The Price Tracker implementation was missing this check. Unfortunately, this implementation still uses 'ancestors', which causes expensive XRays[3] work in extension applications and still triggers layout flushes at suboptimal times. This is something that can be avoided with an async solution to the tune of a 40% reduction in jank using 'requestAnimationFrame' and 'setTimeout'[4]. On the brighter side, it is more correct than the previous implementation, removing 'getComputedStyle().width' and 'getComputedStyle().height' completely and covering more valid cases than before. *: This is slightly worse than the expected 16%, because my original implementation in Price Tracker did not check for elements off-screen as the Fathom implementation does. Its profile[5] shows: - The largest unresponsive chunk is still caused by Fathom extraction, contributing 399 ms of jank right around page load. - `isVisible` made up 238 ms (60%) of this jank. - This change reduced overall Fathom-related jank by 61 ms (13%) compared to the original implementation of isVisible[2]. [1]: #116 (comment) [2]: mozilla/price-tracker#319 (comment) [3]: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/Xray_vision [4]: mozilla/price-tracker#319 (comment) [5]: https://perfht.ml/2T0oYQS
Compared to a baseline profile[1] of Price Tracker's current 'isVisible' implementation (on which Fathom's 'isVisible' method is based), this clickability approach offers a 53% (146 ms) reduction in Fathom-related jank[2] for the same locally hosted sample page. This is largely due to removing the use of the 'ancestors' Fathom method in 'isVisible'[3], which was performing a lot of redundant layout accesses (and triggering a lot of layout flushes) for the same elements. Also, at least in an extension application, DOM accesses (e.g. repeatedly getting the next 'parentNode' in 'ancestors') are very expensive due to X-Rays[4]. Notes: * If the proposal to the W3C CSS Working Group (see inline comment in patch) is implemented, this clickability approach could forgo the workaround and see as much as 81% (374 ms) reduction in Fathom-related jank[3]. * This implementation can still benefit from memoization, as the same element (e.g. 'div') could be considered for multiple different 'type's[6]. [1]: https://perfht.ml/30wkWT7 [2]: https://perfht.ml/2Y5FCQ1 [3]: mozilla/price-tracker#319 [4]: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/Xray_vision" [6]: https://mozilla.github.io/fathom/glossary.html
Unfortunately, the previous commit's approach based on clickability proved untenable.[1] A distant second sync solution is to early return where possible and use the cheapest styles first (the second implementation approach[2]), which reduces Fathom jank by 13%*, though there are a couple differences: * No 'getBoxQuads'. This is an experimental API only enabled on Nightly. * Checks if the element is off-screen. The Price Tracker implementation was missing this check. Unfortunately, this implementation still uses 'ancestors', which causes expensive XRays[3] work in extension applications and still triggers layout flushes at suboptimal times. This is something that can be avoided with an async solution to the tune of a 40% reduction in jank using 'requestAnimationFrame' and 'setTimeout'[4]. On the brighter side, it is more correct than the previous implementation, removing 'getComputedStyle().width' and 'getComputedStyle().height' completely and covering more valid cases than before. *: This is slightly worse than the expected 16%, because my original implementation in Price Tracker did not check for elements off-screen as the Fathom implementation does. Its profile[5] shows: - The largest unresponsive chunk is still caused by Fathom extraction, contributing 399 ms of jank right around page load. - `isVisible` made up 238 ms (60%) of this jank. - This change reduced overall Fathom-related jank by 61 ms (13%) compared to the original implementation of isVisible[2]. [1]: #116 (comment) [2]: mozilla/price-tracker#319 (comment) [3]: https://developer.mozilla.org/en-US/docs/Mozilla/Tech/Xray_vision [4]: mozilla/price-tracker#319 (comment) [5]: https://perfht.ml/2T0oYQS
TL;DR:
isVisible
, which will likely be a common rule in many Fathom applications, accounts for the majority of the 460 ms of Fathom-related jank (67%). It may be possible to reduce this overall Fathom jank by as much as 374 ms (81%) by reducing style and DOM property accesses inisVisible
, but this solution requires a privileged context.Is this a feature request or a bug?
Feature request
What is the current behavior?
Fathom currently causes considerable jank on page load. Given that Price Tracker was an experiment, performance was not a major consideration in development of the current ruleset used to extract a product from a page.
Initial profile
Initial profile[1]
Numbers
isVisible
calls Fathom’sancestors
function and a bunch ofCSS2Properties
getters, altogether taking up 311ms (67%) of this jank.What is the expected or desired behavior?
Fathom should be able to run much faster with much less jank.
I shared this profile with some Firefox engineers, including a layout (Emilio) and a performance engineer (Greg).
Why is
isVisible
taking so much time?getComputedStyle()
on adisplay: none
subtree to be very slow.display: none
.n
visible descendants of<html>
, we will check<html>
's stylesn
times).isVisible
result (e.g. in an HTML element => BooleanMap
or anote()
).getComputedStyle
triggers a layout flush (not a natural layout flush).getBoundingClientRect
) and early return when possible. After a flush, some style accesses are O(1).promiseDocumentFlushed
for privileged code.Updating
isVisible
: first attemptRevised profile: first attempt[1]
This focuses on the fix of using the cheapest style accesses first (e.g.
getBoundingClientRect
) and early return when possible inisVisible
.Numbers
isVisible
made up 244ms (63%) of this jank.isVisible
.Conclusions
isVisible
is spending most of its time (roughly 62%) accessing properties through X-ray vision. We can inspect this in the profile by filtering for Native code for "js::NativeLookupOwnProperty", "XRayTraits" or "WeakMap".isVisible
.Updating
isVisible
: second attemptRevised profile: second attempt[1]
This focuses on the fix of mitigating (actually, eliminating) redundant work, removing
ancestors
entirely to eliminate DOM accesses, and reducing the number of style accesses by checking if the element is clickable.Numbers
isVisible
made up 30 ms (34%) of this jank.isVisible
.Conclusions
getBoundingClientRect
andelementsFromPoint
work.aIgnoreRootScrollFrame
, so this solution may still be viable in Firefox applications. This needs further investigation.Version information
[1]: Profiled on a locally hosted product page with specs from the "Version information" section in this comment.
The text was updated successfully, but these errors were encountered: