|
/* |
|
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(); |
|
})(); |
Thank you