GoogleChrome / web-vitals

Essential metrics for a healthy site.
https://web.dev/vitals
Apache License 2.0
7.59k stars 415 forks source link

Target in FID and INP is null #335

Open markusBurda opened 1 year ago

markusBurda commented 1 year ago

Hi guys,

Im using your nice script to measuer web vitals on our website. To make optimization easier, we are also persisting the target-elemtent. It works fine with CLS and LCP but in FID and INP I get "null" for the target Element as you can see in this screenshot: image

This is how I use it:

        <script type="module">
            import {onCLS, onFID, onLCP, onINP, onFCP, onTTFB} from "https://unpkg.com/web-vitals@3/dist/web-vitals.attribution.js?module";
            function getIdOrNodeNamePlusClassList(node) {
                return (node && node.nodeType !== 9) ? (node.id ?
                    "#" + node.id :
                    node.nodeName.toLowerCase() +
                    ((node.className && node.className.length) ?
                        "." + Array.from(node.classList.values()).join(".") :
                        "")) : "";
            }
            function getLargestLayoutShiftEntry(entries) {
              return entries.reduce((a, b) => a && a.value > b.value ? a : b);
            }

            function getLargestLayoutShiftSource(sources) {
              return sources.reduce((a, b) => {
                return a.node && a.previousRect.width * a.previousRect.height >
                    b.previousRect.width * b.previousRect.height ? a : b;
              });
            }

            function getCausingElementIdentifier(name, entries = []) {
                // In some cases there won't be any entries (e.g. if CLS is 0,
                // or for LCP after a bfcache restore), so we have to check first.
                if (entries.length && name !== "FCP" && name !== "TTFB") {
                    if (name === "LCP") {
                        const lastEntry = entries[entries.length - 1];
                        return getIdOrNodeNamePlusClassList(lastEntry.element);
                    } else if (name === "FID" || name === "INP") {
                        const firstEntry = entries[0];
                        return getIdOrNodeNamePlusClassList(firstEntry.target);
                    } else if (name === "CLS") {
                        const largestEntry = getLargestLayoutShiftEntry(entries);
                        if (largestEntry && largestEntry.sources && largestEntry.sources.length) {
                            const largestSource = getLargestLayoutShiftSource(largestEntry.sources);
                            if (largestSource) {
                                return getIdOrNodeNamePlusClassList(largestSource.node);
                            }
                        }
                    }
                }
                return "";
            }

            function sendRUMData({name, value, attribution, entries}) {
                   if (name === "FID" || name === "INP") {
                       console.log("--- " + name + "(" + value +") ---");
                       console.log("- Entries -");
                       console.log(entries);
                       console.log("- Attribution -");
                       console.log(attribution);
                       console.log("- Causing -");
                       console.log(getCausingElementIdentifier(name, entries));
                    }
}
            onCLS(sendRUMData);
            onFCP(sendRUMData);
            onFID(sendRUMData);
            onINP(sendRUMData);
            onLCP(sendRUMData);
            onTTFB(sendRUMData);
        </script>

Do you know why? Am I doing something wrong?

tunetheweb commented 1 year ago

Hi @markusBurda this is a known limitation with PerformanceEventTiming when the target element is removed from the DOM as part of the interaction, or for any other reason before the entry fires. For example if the interaction is a button click that dismisses a modal and so the button that was click is no longer in the DOM.

You can see that document here in MDN: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEventTiming/target

Value A Node onto which the event was last dispatched.

Or null if the Node is disconnected from the document's DOM or is in the shadow DOM.

There's more discussion of the issue here: https://github.com/w3c/event-timing/issues/126

Would that explain your issue? You should still get FID/INP node details for those interactions where the node is not removed from the DOM.

Barry

P.S. I noticed you've a number of helper classes to obtain the the attribution information, but you're already pulling in the attribution build which provides APIs to give that information directly. Any reason you're not using the in-built ones? Details here: https://github.com/GoogleChrome/web-vitals#send-attribution-data

markusBurda commented 1 year ago

Hi @tunetheweb, thanks for your quick answer. Actually I get the target: null in all the cases. Also when its just a simple a-tag click (with no js on it) or on a burger-icon (which stays in DOM after clicked). The example above is from a click on a burger menu. And i compared these values I get INP vs FID and I get this result: image

Do you know why I get a target in INP but no target on the FID entries? Its the same click-event.

P.S.: The helper classes where necessary in previous versions. I'm very happy that they are not needed anymore.

tunetheweb commented 1 year ago

Interesting. This looks like a bug in Chrome. I've opened this to track: https://bugs.chromium.org/p/chromium/issues/detail?id=1428899

JuniorTour commented 1 year ago

How about use xpath string for target?

I got a lot of null from target by our log system too, it is an annoyed problem, we can't locate where the source of FID, CLS and INP is.

If we use xpath, even if the element is disconnected from DOM tree, we can get what it is by investigate xptah string .

eg: target: //*[@id="discussion_bucket"]/div/div[1]/div/div[1]/div[2]/div[5]/div/div[2]/div[2]/div[2]/span.State.State--merged.State--small.d-flex.flex-items-center

If we get this xpath sting from CLS target prop, we can know its className, DOM tree path, and finally location which element it is.

tunetheweb commented 1 year ago

This issue is when an element is is disconnected from the DOM tree the Performance Observer entry returns null. So it's not that we can't look up the element, it's that we don't have the element to look up. This needs to be fixed in Chrome before this library can return it.

However, there are a number of different issue here that are easy to confuse:

kashifshamaz21 commented 1 year ago

Hi @tunetheweb ,

We recently shifted to using the attribution build to capture the debug target elements for LCP & CLS metrics on our pages.

Even we are noticing that occassionally we are seeing a null value as the element in both onCLS and onLCP callback events, even though the LCP / CLS elements are intact and not being removed by any user interaction.

The web-page below is one where I'm able to reproduce the issue quite consistently for null value of LCP element: https://www.carousell.com.hk/p/jabra-headset-1198007120/

Attaching screenshots for reference:

  1. Below we received null for the LCP element in the onLCP callback (metric.attribution.element field):

    Screenshot 2023-08-31 at 5 53 50 PM
  2. And for the same url, in same browser (Chrome), sometimes we do receive the LCP element (the first image in the Carousel):

    Screenshot 2023-08-31 at 5 46 54 PM

The LCP element expected is one of the 3 images displayed in the Carousel at the top. The images in the above link are jpg format.

Is there anything I could do, to investigate further on why the LCP element is coming as null sometimes?

tunetheweb commented 1 year ago

When I block https://mweb-cdn.karousell.com/build/0eb384e38adc64a5.min.js on that page I consistently get an element. So I would guess the JavaScript is somehow deleting and re-adding the element to the DOM. I'm not sure exactly how it does this as the JavaScript is all minified so impossible to read, but would guess as part of the page hydration? As the element no longer exists when the LCP event is reported (which will be later), this currently is reported as null. This is the spec issue mentioned above.

kashifshamaz21 commented 1 year ago

@tunetheweb thanks for quick reply. So, at the moment, if there is any re-mounting of a Node (delete and recreate) which happens to be the LCP element, then the element reported in metric.attribution object could be null ? cc: @AbhiPanseriya @girdhariag @jsliang @simon-paris

tunetheweb commented 1 year ago

Yes because effectively that is a new element and if the metric is reported on the previous element, then it's no longer in existence to reference. See the Spec discussion.

Of course, if the remount results in a new metric (e.g. it's an ever so slightly larger LCP element) then it would issue a new LCP observation and so would have the node.

tunetheweb commented 9 months ago

Update on this issue:

Element node is not reported for some FID/INP entries. Chrome bug - 1428899 Element node is not reported when element is disconnected from the DOM https://github.com/w3c/event-timing/issues/126

We've released 3.5.2 which attempts to get the first non-null element for the INP event which should work around around the first issue. Will be interesting to see how much this improves the situation and how much the second issue is still a pain point.

oguzhanaslan commented 9 months ago

@tunetheweb I upgraded to version 3.5.2, but the result has not changed much. I see null data. There is one issue I am curious about. What exactly do the inp events reported as null correspond to? For example; when a click is made on the product photo, it is reported correctly. can there be a product photo among the data reported as null?

tunetheweb commented 9 months ago

As noted in https://github.com/GoogleChrome/web-vitals/issues/335#issuecomment-1625311650 there were two main outstanding issues that we were aware of - both issues in Chromium rather than anything this library can necessarily resolve.

1) Certain events in did not have the target attached in Chrome. We’ve particularly send this with pointer or touch events. The reason is a little tricky but my understanding is basically that

The change we made with 3.5.2 to workaround this was to look at other events in the same interaction. For example, and interaction might be made up of a pointer down, pointer up, and click events. If either of the pointer events are the longest events they may be reported as the “INP event” and its target given. As of 3.5.2, if that target is null, we will look at the other events in that interaction to try to find a non-null target.

This is a workaround and though and in some cases there may not be a target even then. This particularly affects short events (where the FID event is reported as INP and it only has the first). We were unable to estimate how much this workaround would help but it was definitely known this would not solve all the issues.

The good news is we’ve made significant progress on the underlying bug where and think these may be reported in a future version of Chrome (probably Chrome 123). This is the better fix rather than the workaround we tried in this library.

2) The second issue is tougher to solve. When an element is removed from the DOM it is not longer available as a target either. Think of a dialog where to click the button to dismiss it and that button does a lot of work. But the time INP is reported the dialog has been dismissed and so the target is null.

This is technically correct according to spec, but obviously painful for those trying to identify INP causes!

We have further good news here, in that we think we have a way forward here too. Basically we will grab some details at the time of the interaction to use later even if the element doesn’t exist. This may not be the same selector that the web-vitals library would have used, but hopefully will be enough to identify the element (particularly if a unique id, or a class that makes it identifiable is present on the element!).

Not sure when exactly this will land as still some details to be worked out but at earliest Chrome 123.

so to your example, yes in theory it could happen to any element, but it’s more likely to happen to specific ones (ones that have pointer events, very low INP, or for elements which are then removed from the DOM as part of the interaction or shortly afterwards).

Hopefully the workaround in 3.5.2 has helped a little (and certainly shouldn’t have made it any worse), but we need those two fixes implemented to really resolve this (and hopefully not end up revealing other issues we have not considered so far!).

I’ll keep this issue open and update it as the situation changes.

mmocny commented 9 months ago

FWIW the first issue, the "null" target fix is in the latest Chrome Canary, and so the latest web-vitals.js could be tested against it.

guoyunhe commented 7 months ago

FWIW the first issue, the "null" target fix is in the latest Chrome Canary, and so the latest web-vitals.js could be tested against it.

@mmocny does it mean that we still cannot get the element if the user is using an older version of Chrome or Chromium based browsers? (Like Samsung Internet)

mmocny commented 7 months ago

There are a few reasons why target could be null. The issues related to pointer events sometimes not having a target, do require Chrome m123 release.

Separately web-vitals.js has made changes to report not just the target of the first event timing entry, but the first non-null target of any event timing entry. That change might help even older browser versions.