GoogleChrome / web-vitals

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

Is there a way to find which elements are contributing to CLS? #123

Closed raja-anbazhagan closed 3 years ago

raja-anbazhagan commented 3 years ago

The reports from chrome devtool, GPSI and the web.dev/measure reports show 0 CLS. But the production results are always more than 0.

I understand that the lab settings and production data vary a lot.

But how can I identify which element causing CLS? The report doesn't add any value unless the data about CLS contributing elements are also provided.

Please advise

philipwalton commented 3 years ago

Each layout-shift entry object contributing to CLS will have a sources property that consists of LayoutShiftAttribution objects. These objects contain a list of elements on the page that shifted the most during each shift. (See the spec explainer on attribution for more details.)

Note that these elements may or may not have been the cause of the shift, but knowing which elements shifted can greatly help in determining the cause of the shift (e.g. if element #top-bar shifted you might be able to determine it was because element #banner was inserted into the DOM above it).

Here's some example code I've used in the past (e.g. this code from the Web Vitals Report source) to identify the "largest offender" contributing to CLS on the page. This code basically loops through all LayoutShiftAttribution objects from all layout-shift entries to identify the largest one and then it reports a CSS selector path to the node along with the CLS value.

function getNodePath(node) {
  let name = node.nodeName.toLowerCase();
  if (name === 'body') {
    return 'html>body';
  }
  if (node.id) {
    return `${name}#${node.id}`;
  }
  if (node.className && node.className.length) {
    name += `.${[...node.classList.values()].join('.')}`;
  }
  return `${getNodePath(node.parentElement)}>${name}`;
}

getCLS(({name, value, delta, entries}) => {
  let largestShiftedNode;

  if (entries.length) {
    const largestShift = entries.reduce((a, b) => {
      return a && a.value > b.value ? a : b;
    });
    if (largestShift && largestShift.sources) {
      const largestSource = largestShift.sources.reduce((a, b) => {
        return a.node && a.previousRect.width * a.previousRect.height >
            b.previousRect.width * b.previousRect.height ? a : b;
      });
      if (largestSource) {
        largestShiftedNode = getNodePath(largestSource.node);
      }
    }
  }

  sendToAnalytics('CLS', value, largestShiftedNode)
});

Then, in my analytics, and I can see—amongst all users—what elements on the page are most commonly the largest contributors to CLS, for most of my users.

I hope that's helpful!

Note: I'm planning to publish some official guidance on how to debug Web Vitals issues using an analytics tool in the near future on web.dev, so stay tuned for that.

raja-anbazhagan commented 3 years ago

@philipwalton This is gold. I'm currently converting it to ES5 so that I can use it in my Google Tag Manager setup.

However, I wonder why this crucial information is hard to come by. I hope this approach get's implemented within the module itself.

Also, When can we expect your post about these debugging guidance?

raja-anbazhagan commented 3 years ago

Here is how I setup my Tag in GTM.

    <script>
        function getNodePath(node) {
            var name = node.nodeName.toLowerCase();
            if (name === 'body') {
                return 'html>body';
            }
            if (node.id) {
                return name + '#' + node.id;
            }
            if (node.className && node.className.length) {
                name += '.' + node.classList.value.split(/[ ,]+/).join('.');
            }
            return getNodePath(node.parentElement) + '>' + name;
        }

        function largestNode(a, b) {
            return a && a.value > b.value ? a : b;
        }

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

        function identifyLargestShift(entries) {
            var largestShiftedNode = null;
            if (entries.length) {
                var largestShift = entries.reduce(largestNode);

                if (largestShift && largestShift.sources) {
                    var largestSource = largestShift.sources.reduce(findLargestFromSource);

                    if (largestSource) {
                        largestShiftedNode = getNodePath(largestSource.node);
                    }
                }
            }
            return largestShiftedNode;
        }

        function sendToGTM(metric) {
            if (metric.name === 'CLS') {
                var problematicNode = identifyLargestShift(metric.entries);
                dataLayer.push({
                    event: 'web-vitals',
                    event_category: 'Web Vitals',
                    event_action: "Largest CLS",
                    event_value: problematicNode,
                });
            }
            dataLayer.push({
                event: 'web-vitals',
                event_category: 'Web Vitals',
                event_action: metric.name,
                event_value: Math.round(metric.name === 'CLS' ? metric.delta * 1000 : metric.delta),
                event_label: metric.id,
            });
        }

        (function () {
            var script = document.createElement('script');
            script.src = 'https://unpkg.com/web-vitals';
            script.onload = function () {
                webVitals.getCLS(sendToGTM);
                webVitals.getFID(sendToGTM);
                webVitals.getLCP(sendToGTM);
            }
            document.head.appendChild(script);
        }())
    </script>

The code needs a bit of cleanup of course.

philipwalton commented 3 years ago

Also, When can we expect your post about these debugging guidance?

Most likely by the end of March.

philipwalton commented 3 years ago

The guide for this has been published: https://web.dev/debug-web-vitals-in-the-field/