Skip to content

Instantly share code, notes, and snippets.

@domtronn
Created November 23, 2023 12:39
Show Gist options
  • Save domtronn/b1253105d79a75eae4a8078e9759c5aa to your computer and use it in GitHub Desktop.
Save domtronn/b1253105d79a75eae4a8078e9759c5aa to your computer and use it in GitHub Desktop.
INP Inlined debugger

This script is designed to be used within BrowersStack as a "quick" way to get the WebVitals reporting on INP for mobile devices

It uses the web-vitals library and provides a slimmed down version of the official web-vitals chrome extension

Usage

Copy and paste the script "as is" once the page is loaded and then begin interacting with the page to see the reported INP values

Building

bunx minify index.js | pbcopy
/*
Copyright 2020 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
(async () => {
const webVitals = await import('https://cdn.jsdelivr.net/npm/[email protected]/+esm');
// Core Web Vitals thresholds
const INP_THRESHOLD = webVitals.INPThresholds[0];
const COLOR_GOOD = '#0CCE6A';
const COLOR_NEEDS_IMPROVEMENT = '#FFA400';
const COLOR_POOR = '#FF4E42';
const RATING_COLORS = {
'good': COLOR_GOOD,
'needs-improvement': COLOR_NEEDS_IMPROVEMENT,
'poor': COLOR_POOR
};
// Identifiable prefix for console logging
const LOG_PREFIX = '[Web Vitals Inline]';
/*
Copyright 2023 Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
function onEachInteraction(callback) {
const valueToRating = (score) => score <= 200 ? 'good' : score <= 500 ? 'needs-improvement' : 'poor';
const observer = new PerformanceObserver((list) => {
const interactions = {};
for (const entry of list.getEntries().filter((entry) => entry.interactionId)) {
interactions[entry.interactionId] = interactions[entry.interactionId] || [];
interactions[entry.interactionId].push(entry);
}
// Will report as a single interaction even if parts are in separate frames.
// Consider splitting by animation frame.
for (const interaction of Object.values(interactions)) {
const entry = interaction.reduce((prev, curr) => prev.duration >= curr.duration ? prev : curr);
const value = entry.duration;
callback({
attribution: {
eventEntry: entry,
eventTime: entry.startTime,
eventType: entry.name,
},
entries: interaction,
name: 'Interaction',
rating: valueToRating(value),
value,
});
}
});
observer.observe({
type: 'event',
durationThreshold: 0,
buffered: true,
});
}
/**
* Very simple classifier for metrics values
* @param {Object} metrics
* @return {String} overall metrics score
*/
function scoreBadgeMetrics(metrics) {
// Note: overallScore is treated as a string rather than
// a boolean to give us the flexibility of introducing a
// 'NEEDS IMPROVEMENT' option here in the future.
let overallScore = 'GOOD';
if (metrics.inp.value > INP_THRESHOLD) {
// INP does not affect overall score
metrics.inp.pass = false;
}
return overallScore;
}
/**
*
* Broadcasts metrics updates using chrome.runtime(), triggering
* updates to the badge. Will also update the overlay if this option
* is enabled.
* @param {Object} metric
*/
function broadcastMetricsUpdates(metric) {
logSummaryInfo(metric);
}
async function logSummaryInfo(metric) {
const formattedValue = metric.name === 'CLS' ? metric.value.toFixed(2) : `${metric.value.toFixed(0)} ms`;
console.groupCollapsed(
`${LOG_PREFIX} ${metric.name} %c${formattedValue} (${metric.rating})`,
`color: ${RATING_COLORS[metric.rating] || 'inherit'}`
);
if ((metric.name == 'INP'|| metric.name == 'Interaction') &&
metric.attribution &&
metric.attribution.eventEntry) {
const subPartString = `${metric.name} sub-part`;
const eventEntry = metric.attribution.eventEntry;
console.log('Interaction target:', eventEntry.target);
for (let entry of metric.entries) {
console.log(`Interaction event type: %c${entry.name}`, 'font-family: monospace');
// RenderTime is an estimate, because duration is rounded, and may get rounded down.
// In rare cases it can be less than processingEnd and that breaks performance.measure().
// Lets make sure its at least 4ms in those cases so you can just barely see it.
const adjustedPresentationTime = Math.max(entry.processingEnd + 4, entry.startTime + entry.duration);
console.table([{
subPartString: 'Input delay',
'Time (ms)': Math.round(entry.processingStart - entry.startTime, 0),
},
{
subPartString: 'Processing time',
'Time (ms)': Math.round(entry.processingEnd - entry.processingStart, 0),
},
{
subPartString: 'Presentation delay',
'Time (ms)': Math.round(adjustedPresentationTime - entry.processingEnd, 0),
}]);
}
}
console.log(metric);
console.groupEnd();
}
/**
*
* Fetches Web Vitals metrics via WebVitals.js
*/
function fetchWebPerfMetrics() {
// web-vitals.js doesn't have a way to remove previous listeners, so we'll save whether
// we've already installed the listeners before installing them again.
// See https://github.com/GoogleChrome/web-vitals/issues/55.
if (self._hasInstalledPerfMetrics) return;
self._hasInstalledPerfMetrics = true;
webVitals.onINP((metric) => {
broadcastMetricsUpdates(metric)
}, { reportAllChanges: true });
onEachInteraction((metric) => {
logSummaryInfo(metric, false);
});
}
fetchWebPerfMetrics();
})();
(async ()=>{var a=await import('https://cdn.jsdelivr.net/npm/[email protected]/+esm');var b='#0CCE6A',c='#FFA400',d='#FF4E42',e={'good':b,'needs-improvement':c,'poor':d},f='[Web Vitals Inline]';function g(A){var _=score=>score<=200?'good':score<=500?'needs-improvement':'poor',B=new PerformanceObserver(C=>{const _b={};for(const _a of C.getEntries().filter(entry=>entry.interactionId)){_b[_a.interactionId]=_b[_a.interactionId]||[];_b[_a.interactionId].push(_a)}for(const D of Object.values(_b)){let[_B]=D;for(const E of D.slice(1))_B=E.duration>=_B.duration?E:_B;const _A=_B.duration;A({attribution:{eventEntry:_B,eventTime:_B.startTime,eventType:_B.name},entries:D,name:'Interaction',rating:_(_A),value:_A})}});B.observe({type:'event',durationThreshold:0,buffered:!0})}function h(aA){i(aA)}function i(aB){var aC=aB.name=='CLS'?aB.value.toFixed(2):`${aB.value.toFixed(0)} ms`;console.groupCollapsed(`${f} ${aB.name} %c${aC} (${aB.rating})`,`color: ${e[aB.rating]||'inherit'}`);if((aB.name=='INP'||aB.name=='Interaction')&&aB.attribution&&aB.attribution.eventEntry){var aD=aB.attribution.eventEntry;console.log('Interaction target:',aD.target);for(let aE of aB.entries){console.log(`Interaction event type: %c${aE.name}`,'font-family: monospace');var aF=Math.max(aE.processingEnd+4,aE.startTime+aE.duration);console.table([{subPartString:'Input delay','Time (ms)':Math.round(aE.processingStart-aE.startTime,0)},{subPartString:'Processing time','Time (ms)':Math.round(aE.processingEnd-aE.processingStart,0)},{subPartString:'Presentation delay','Time (ms)':Math.round(aF-aE.processingEnd,0)}])}}console.log(aB);console.groupEnd()}function j(){if(self._hasInstalledPerfMetrics)return;self._hasInstalledPerfMetrics=!0;a.onINP(metric=>h(metric),{reportAllChanges:!0});g(metric=>i(metric,!1))}j()})();
@danielehrhardt
Copy link

Thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment