Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update interaction to next paint. #2060

Merged
merged 1 commit into from
Jan 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
313 changes: 170 additions & 143 deletions browserscripts/pageinfo/interactionToNextPaintInfo.js
Original file line number Diff line number Diff line change
@@ -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
});
}
})({});


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 : ''
};
}
})({});
Loading