From b84e5b27c69984f6623defa9c1e8b3de9f72b6a1 Mon Sep 17 00:00:00 2001 From: soulgalore Date: Sat, 6 Jan 2024 07:10:59 +0100 Subject: [PATCH] Update interaction to next paint. Simplify how to get interaction to next paint and add more attribution following in the lines of Google Web Vitals script. --- .../pageinfo/interactionToNextPaintInfo.js | 313 ++++++++++-------- .../timings/interactionToNextPaint.js | 186 +++-------- 2 files changed, 219 insertions(+), 280 deletions(-) diff --git a/browserscripts/pageinfo/interactionToNextPaintInfo.js b/browserscripts/pageinfo/interactionToNextPaintInfo.js index f64325304..460112d94 100644 --- a/browserscripts/pageinfo/interactionToNextPaintInfo.js +++ b/browserscripts/pageinfo/interactionToNextPaintInfo.js @@ -1,152 +1,179 @@ (function () { + // https://github.com/GoogleChrome/web-vitals/blob/main/src/lib/getLoadState.ts#L20 - // This is an updated version of - // https://github.com/GoogleChrome/web-vitals/blob/64f133590fcac72c1bc042bf7b4ab729d7e03316/src/onINP.ts - // It was reworked the 19/5-2023 + function getLoadState(timestamp) { + if (document.readyState === 'loading') { + // If the `readyState` is 'loading' there's no need to look at timestamps + // since the timestamp has to be the current time or earlier. + return 'loading'; + } else { + const navigationEntry = performance.getEntriesByType('navigation')[0]; + if (navigationEntry) { + if (timestamp < navigationEntry.domInteractive) { + return 'loading'; + } else if ( + navigationEntry.domContentLoadedEventStart === 0 || + timestamp < navigationEntry.domContentLoadedEventStart + ) { + // If the `domContentLoadedEventStart` timestamp has not yet been + // set, or if the given timestamp is less than that value. + return 'dom-interactive'; + } else if ( + navigationEntry.domComplete === 0 || + timestamp < navigationEntry.domComplete + ) { + // If the `domComplete` timestamp has not yet been + // set, or if the given timestamp is less than that value. + return 'dom-content-loaded'; + } + } + } + // If any of the above fail, default to loaded. This could really only + // happy if the browser doesn't support the performance timeline, which + // most likely means this code would never run anyway. + return 'complete'; + } - const supported = PerformanceObserver.supportedEntryTypes; - if (!supported || supported.indexOf('event') === -1 ||  supported.indexOf('first-input') === -1) { - return; + // https://github.com/GoogleChrome/web-vitals/blob/main/src/lib/getSelector.ts#L24 + function getName(node) { + const name = node.nodeName; + return node.nodeType === 1 + ? name.toLowerCase() + : name.toUpperCase().replace(/^#/, ''); } - var observe = function observe(type, callback, opts) { - try { - if (PerformanceObserver.supportedEntryTypes.includes(type)) { - var po = new PerformanceObserver(function (list) { - Promise.resolve().then(function () { - callback(list.getEntries()); - }); - }); - po.observe(Object.assign({type: type, buffered: true}, opts || {})); - return po; - } - } catch (e) {} - return; - }; - var interactionCountEstimate = 0; - var minKnownInteractionId = Infinity; - var maxKnownInteractionId = 0; - var updateEstimate = function updateEstimate(entries) { - entries.forEach(function (e) { - if (e.interactionId) { - minKnownInteractionId = Math.min( - minKnownInteractionId, - e.interactionId - ); - maxKnownInteractionId = Math.max( - maxKnownInteractionId, - e.interactionId - ); - interactionCountEstimate = maxKnownInteractionId - ? (maxKnownInteractionId - minKnownInteractionId) / 7 + 1 - : 0; - } - }); - }; - var po; - var getInteractionCount = function getInteractionCount() { - return po ? interactionCountEstimate : performance.interactionCount || 0; - }; - var initInteractionCountPolyfill = function initInteractionCountPolyfill() { - if ('interactionCount' in performance || po) return; - po = observe('event', updateEstimate, { - type: 'event', - buffered: true, - durationThreshold: 0, - }); - }; - var prevInteractionCount = 0; - var getInteractionCountForNavigation = - function getInteractionCountForNavigation() { - return getInteractionCount() - prevInteractionCount; - }; - var MAX_INTERACTIONS_TO_CONSIDER = 10; - var longestInteractionList = []; - var longestInteractionMap = {}; - var processEntry = function processEntry(entry) { - var minLongestInteraction = - longestInteractionList[longestInteractionList.length - 1]; - var existingInteraction = longestInteractionMap[entry.interactionId]; - if ( - existingInteraction || - longestInteractionList.length < MAX_INTERACTIONS_TO_CONSIDER || - entry.duration > minLongestInteraction.latency - ) { - if (existingInteraction) { - existingInteraction.entries.push(entry); - existingInteraction.latency = Math.max( - existingInteraction.latency, - entry.duration - ); - } else { - var interaction = { - id: entry.interactionId, - latency: entry.duration, - entries: [entry], - }; - longestInteractionMap[interaction.id] = interaction; - longestInteractionList.push(interaction); - } - longestInteractionList.sort(function (a, b) { - return b.latency - a.latency; - }); - longestInteractionList - .splice(MAX_INTERACTIONS_TO_CONSIDER) - .forEach(function (i) { - delete longestInteractionMap[i.id]; - }); + + function getSelector(node) { + let sel = ''; + + try { + while (node && node.nodeType !== 9) { + const el = node; + const part = el.id + ? '#' + el.id + : getName(el) + + (el.classList && + el.classList.value && + el.classList.value.trim() && + el.classList.value.trim().length + ? '.' + el.classList.value.trim().replace(/\s+/g, '.') + : ''); + if (sel.length + part.length > 100 - 1) return sel || part; + sel = sel ? part + '>' + sel : part; + if (el.id) break; + node = el.parentNode; } - }; - var estimateP98LongestInteraction = function estimateP98LongestInteraction() { - var candidateInteractionIndex = Math.min( - longestInteractionList.length - 1, - Math.floor(getInteractionCountForNavigation() / 50) - ); - return longestInteractionList[candidateInteractionIndex]; - }; - - var opts = {}; - var metric = {}; - initInteractionCountPolyfill(); - var handleEntries = function handleEntries(entries) { - entries.forEach(function (entry) { - if (entry.interactionId) { - processEntry(entry); - } - if (entry.entryType === 'first-input') { - var noMatchingEntry = !longestInteractionList.some(function ( - interaction - ) { - return interaction.entries.some(function (prevEntry) { - return ( - entry.duration === prevEntry.duration && - entry.startTime === prevEntry.startTime - ); - }); - }); - if (noMatchingEntry) { - processEntry(entry); + } catch (err) { + // Do nothing... + } + return sel; + } + + // https://gist.github.com/karlgroves/7544592 + function getDomPath(el) { + const stack = []; + while (el.parentNode != null) { + let sibCount = 0; + let sibIndex = 0; + for (let i = 0; i < el.parentNode.childNodes.length; i++) { + let sib = el.parentNode.childNodes[i]; + if (sib.nodeName == el.nodeName) { + if (sib === el) { + sibIndex = sibCount; } + sibCount++; } - }); - var inp = estimateP98LongestInteraction(); - if (inp && inp.latency !== metric.value) { - var cleanedEntries = []; - for (var entry of inp.entries) { - console.log(entry); - cleanedEntries.push({ - id: entry.interactionId, - latency: entry.duration, - name: entry.name - }); - } - return {latency: inp.latency, entries: cleanedEntries}; } - }; - var po = observe('event', handleEntries, { - durationThreshold: opts.durationThreshold || 40, - }); - if (po) { - po.observe({type: 'first-input', buffered: true}); + if (el.hasAttribute && el.hasAttribute('id') && el.id != '') { + stack.unshift(el.nodeName.toLowerCase() + '#' + el.id); + } else if (sibCount > 1) { + stack.unshift(el.nodeName.toLowerCase() + ':eq(' + sibIndex + ')'); + } else { + stack.unshift(el.nodeName.toLowerCase()); + } + el = el.parentNode; + } + + return stack.slice(1); + } + + // https://github.com/GoogleChrome/web-vitals/blob/main/src/onINP.ts + const observer = new PerformanceObserver(list => {}); + observer.observe({type: 'event', buffered: true}); + observer.observe({type: 'first-input', buffered: true}); + const entries = observer.takeRecords(); + + const MAX_INTERACTIONS_TO_CONSIDER = 10; + const longestInteractionList = []; + const longestInteractionMap = {}; + for (let entry of entries) { + var minLongestInteraction = + longestInteractionList[longestInteractionList.length - 1]; + var existingInteraction = longestInteractionMap[entry.interactionId]; + if ( + existingInteraction || + longestInteractionList.length < MAX_INTERACTIONS_TO_CONSIDER || + entry.duration > minLongestInteraction.latency + ) { + if (existingInteraction) { + existingInteraction.entries.push(entry); + existingInteraction.latency = Math.max( + existingInteraction.latency, + entry.duration + ); + } else { + var interaction = { + id: entry.interactionId, + latency: entry.duration, + entries: [entry] + }; + longestInteractionMap[interaction.id] = interaction; + longestInteractionList.push(interaction); + } + longestInteractionList.sort(function (a, b) { + return b.latency - a.latency; + }); + longestInteractionList + .splice(MAX_INTERACTIONS_TO_CONSIDER) + .forEach(function (i) { + delete longestInteractionMap[i.id]; + }); + } + } + + const inp = longestInteractionList[longestInteractionList.length - 1]; + if (inp) { + const cleanedEntries = []; + + for (let entry of inp.entries) { + cleanedEntries.push({ + id: entry.interactionId, + latency: entry.duration, + name: entry.name + }); } - })({}); - \ No newline at end of file + + const longestEntry = inp.entries.sort((a, b) => { + // Sort by: 1) duration (DESC), then 2) processing time (DESC) + return ( + b.duration - a.duration || + b.processingEnd - + b.processingStart - + (a.processingEnd - a.processingStart) + ); + })[0]; + let element = longestEntry.target; + + return { + latency: inp.latency, + entries: cleanedEntries, + eventType: longestEntry.name, + eventTime: longestEntry.startTime, + eventTarget: getSelector(element), + loadState: getLoadState(longestEntry.startTime), + domPath: element ? getDomPath(element).join(' > ') : '', + tagName: element ? element.tagName : '', + className: element ? element.className : '', + tag: element ? element.cloneNode(false).outerHTML : '' + }; + } +})({}); diff --git a/browserscripts/timings/interactionToNextPaint.js b/browserscripts/timings/interactionToNextPaint.js index 7de48f779..d2e6c13a2 100644 --- a/browserscripts/timings/interactionToNextPaint.js +++ b/browserscripts/timings/interactionToNextPaint.js @@ -1,142 +1,54 @@ (function () { + // https://github.com/GoogleChrome/web-vitals/blob/main/src/onINP.ts + const observer = new PerformanceObserver(list => {}); + observer.observe({type: 'event', buffered: true}); + observer.observe({type: 'first-input', buffered: true}); + const entries = observer.takeRecords(); - // This is an updated version of - // https://github.com/GoogleChrome/web-vitals/blob/64f133590fcac72c1bc042bf7b4ab729d7e03316/src/onINP.ts - // It was reworked the 19/5-2023 - - const supported = PerformanceObserver.supportedEntryTypes; - if (!supported || supported.indexOf('event') === -1 ||  supported.indexOf('first-input') === -1) { - return; - } - var observe = function observe(type, callback, opts) { - try { - if (PerformanceObserver.supportedEntryTypes.includes(type)) { - var po = new PerformanceObserver(function (list) { - Promise.resolve().then(function () { - callback(list.getEntries()); - }); - }); - po.observe(Object.assign({type: type, buffered: true}, opts || {})); - return po; - } - } catch (e) {} - return; - }; - var interactionCountEstimate = 0; - var minKnownInteractionId = Infinity; - var maxKnownInteractionId = 0; - var updateEstimate = function updateEstimate(entries) { - entries.forEach(function (e) { - if (e.interactionId) { - minKnownInteractionId = Math.min( - minKnownInteractionId, - e.interactionId - ); - maxKnownInteractionId = Math.max( - maxKnownInteractionId, - e.interactionId - ); - interactionCountEstimate = maxKnownInteractionId - ? (maxKnownInteractionId - minKnownInteractionId) / 7 + 1 - : 0; - } - }); - }; - var po; - var getInteractionCount = function getInteractionCount() { - return po ? interactionCountEstimate : performance.interactionCount || 0; - }; - var initInteractionCountPolyfill = function initInteractionCountPolyfill() { - if ('interactionCount' in performance || po) return; - po = observe('event', updateEstimate, { - type: 'event', - buffered: true, - durationThreshold: 0, - }); - }; - var prevInteractionCount = 0; - var getInteractionCountForNavigation = - function getInteractionCountForNavigation() { - return getInteractionCount() - prevInteractionCount; - }; - var MAX_INTERACTIONS_TO_CONSIDER = 10; - var longestInteractionList = []; - var longestInteractionMap = {}; - var processEntry = function processEntry(entry) { - var minLongestInteraction = - longestInteractionList[longestInteractionList.length - 1]; - var existingInteraction = longestInteractionMap[entry.interactionId]; - if ( - existingInteraction || - longestInteractionList.length < MAX_INTERACTIONS_TO_CONSIDER || - entry.duration > minLongestInteraction.latency - ) { - if (existingInteraction) { - existingInteraction.entries.push(entry); - existingInteraction.latency = Math.max( - existingInteraction.latency, - entry.duration - ); - } else { - var interaction = { - id: entry.interactionId, - latency: entry.duration, - entries: [entry], - }; - longestInteractionMap[interaction.id] = interaction; - longestInteractionList.push(interaction); - } - longestInteractionList.sort(function (a, b) { - return b.latency - a.latency; - }); - longestInteractionList - .splice(MAX_INTERACTIONS_TO_CONSIDER) - .forEach(function (i) { - delete longestInteractionMap[i.id]; - }); + const MAX_INTERACTIONS_TO_CONSIDER = 10; + const longestInteractionList = []; + const longestInteractionMap = {}; + for (let entry of entries) { + var minLongestInteraction = + longestInteractionList[longestInteractionList.length - 1]; + var existingInteraction = longestInteractionMap[entry.interactionId]; + if ( + existingInteraction || + longestInteractionList.length < MAX_INTERACTIONS_TO_CONSIDER || + entry.duration > minLongestInteraction.latency + ) { + if (existingInteraction) { + existingInteraction.entries.push(entry); + existingInteraction.latency = Math.max( + existingInteraction.latency, + entry.duration + ); + } else { + var interaction = { + id: entry.interactionId, + latency: entry.duration, + entries: [entry] + }; + longestInteractionMap[interaction.id] = interaction; + longestInteractionList.push(interaction); } - }; - var estimateP98LongestInteraction = function estimateP98LongestInteraction() { - var candidateInteractionIndex = Math.min( - longestInteractionList.length - 1, - Math.floor(getInteractionCountForNavigation() / 50) - ); - return longestInteractionList[candidateInteractionIndex]; - }; - - var opts = {}; - initInteractionCountPolyfill(); - var handleEntries = function handleEntries(entries) { - entries.forEach(function (entry) { - if (entry.interactionId) { - processEntry(entry); - } - if (entry.entryType === 'first-input') { - var noMatchingEntry = !longestInteractionList.some(function ( - interaction - ) { - return interaction.entries.some(function (prevEntry) { - return ( - entry.duration === prevEntry.duration && - entry.startTime === prevEntry.startTime - ); - }); - }); - if (noMatchingEntry) { - processEntry(entry); - } - } + longestInteractionList.sort(function (a, b) { + return b.latency - a.latency; }); - var inp = estimateP98LongestInteraction(); - if (inp && inp.latency !== metric.value) { - return inp.latency; - } - }; - var po = observe('event', handleEntries, { - durationThreshold: opts.durationThreshold || 40, - }); - if (po) { - po.observe({type: 'first-input', buffered: true}); + longestInteractionList + .splice(MAX_INTERACTIONS_TO_CONSIDER) + .forEach(function (i) { + delete longestInteractionMap[i.id]; + }); } - })({}); - \ No newline at end of file + } + + const inp = longestInteractionList[longestInteractionList.length - 1]; + if (inp) { + return inp.latency; + } else { + if (performance.interactionCount > 0) { + return 0; + } + } +})({});