A curated list of snippets to get Web Performance metrics to use in the browser console
- ⚡️💾 Web Performance Snippets
- Core Web Vitals
- Loading
- Time To First Byte
- Scripts Loading
- Resources hints
- Find Above The Fold Lazy Loaded Images
- Find non Lazy Loaded Images outside of the viewport
- Find render-blocking resources
- Image Info
- Fonts Preloaded, Loaded, and Used Above The Fold
- First And Third Party Script Info
- First And Third Party Script Timings
- Inline Script Info and Size
- Inline Script Info and Size Including
__NEXT_DATA__
- Get your
<head>
in order
- Interaction
/**
* PerformanceObserver
*/
const po = new PerformanceObserver((list) => {
let entries = list.getEntries();
entries = dedupe(entries, "startTime");
/**
* Print all entries of LCP
*/
entries.forEach((item, i) => {
console.dir(item);
console.log(
`${i + 1} current LCP item : ${item.element}: ${item.startTime}`
);
/**
* Highlight LCP elements on the page
*/
item.element ? (item.element.style = "border: 5px dotted blue;") : "";
});
/**
* LCP is the lastEntry in getEntries Array
*/
const lastEntry = entries[entries.length - 1];
/**
* Print final LCP
*/
console.log(`LCP is: ${lastEntry.startTime}`);
});
/**
* Start observing for largest-contentful-paint
* buffered true getEntries prior to this script execution
*/
po.observe({ type: "largest-contentful-paint", buffered: true });
function dedupe(arr, key) {
return [...new Map(arr.map((item) => [item[key], item])).values()];
}
This script it's part of the Web Vitals Chrome Extension and appear on the Optimize Largest Contentful Paint post.
const LCP_SUB_PARTS = [
'Time to first byte',
'Resource load delay',
'Resource load time',
'Element render delay',
];
new PerformanceObserver((list) => {
const lcpEntry = list.getEntries().at(-1);
const navEntry = performance.getEntriesByType('navigation')[0];
const lcpResEntry = performance
.getEntriesByType('resource')
.filter((e) => e.name === lcpEntry.url)[0];
const ttfb = navEntry.responseStart;
const lcpRequestStart = Math.max(
ttfb,
lcpResEntry ? lcpResEntry.requestStart || lcpResEntry.startTime : 0
);
const lcpResponseEnd = Math.max(
lcpRequestStart,
lcpResEntry ? lcpResEntry.responseEnd : 0
);
const lcpRenderTime = Math.max(
lcpResponseEnd,
lcpEntry ? lcpEntry.startTime : 0
);
LCP_SUB_PARTS.forEach((part) => performance.clearMeasures(part));
const lcpSubPartMeasures = [
performance.measure(LCP_SUB_PARTS[0], {
start: 0,
end: ttfb,
}),
performance.measure(LCP_SUB_PARTS[1], {
start: ttfb,
end: lcpRequestStart,
}),
performance.measure(LCP_SUB_PARTS[2], {
start: lcpRequestStart,
end: lcpResponseEnd,
}),
performance.measure(LCP_SUB_PARTS[3], {
start: lcpResponseEnd,
end: lcpRenderTime,
}),
];
// Log helpful debug information to the console.
console.log('LCP value: ', lcpRenderTime);
console.log('LCP element: ', lcpEntry.element, lcpEntry?.url);
console.table(
lcpSubPartMeasures.map((measure) => ({
'LCP sub-part': measure.name,
'Time (ms)': measure.duration,
'% of LCP': `${
Math.round((1000 * measure.duration) / lcpRenderTime) / 10
}%`,
}))
);
}).observe({type: 'largest-contentful-paint', buffered: true});
Context: Largest Contentful Paint change in Chrome 112 to ignore low-entropy images
This snippet is based on and with the permission Stoyan Stefanov, read his post here.
With the script you can get a list of the BPP of all(1) images loaded on the site.
(1) the images with source "data:image" and third-party images are ignored.
console.table(
[...document.images]
.filter(
(img) => img.currentSrc != "" && !img.currentSrc.includes("data:image")
)
.map((img) => [
img.currentSrc,
(performance.getEntriesByName(img.currentSrc)[0]?.encodedBodySize * 8) /
(img.width * img.height),
])
.filter((img) => img[1] !== 0)
);
try {
let cumulativeLayoutShiftScore = 0;
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
cumulativeLayoutShiftScore += entry.value;
}
}
});
observer.observe({ type: "layout-shift", buffered: true });
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "hidden") {
observer.takeRecords();
observer.disconnect();
console.log(`CLS: ${cumulativeLayoutShiftScore}`);
}
});
} catch (e) {
console.error(`Browser doesn't support this API`);
}
Measure the time to first byte, from the document
new PerformanceObserver((entryList) => {
const [pageNav] = entryList.getEntriesByType("navigation");
console.log(`TTFB (ms): ${pageNav.responseStart}`);
}).observe({
type: "navigation",
buffered: true,
});
Measure the time to first byte of all the resources loaded
new PerformanceObserver((entryList) => {
const entries = entryList.getEntries();
const resourcesLoaded = [...entries].map((entry) => {
let obj = {};
// Some resources may have a responseStart value of 0, due
// to the resource being cached, or a cross-origin resource
// being served without a Timing-Allow-Origin header set.
if (entry.responseStart > 0) {
obj = {
"TTFB (ms)": entry.responseStart,
Resource: entry.name,
};
}
return obj;
});
console.table(resourcesLoaded);
}).observe({
type: "resource",
buffered: true,
});
List all the <scripts>
in the DOM and show a table to see if are loaded async
and/or defer
const scripts = document.querySelectorAll("script[src]");
const scriptsLoading = [...scripts].map((obj) => {
let newObj = {};
newObj = {
src: obj.src,
async: obj.async,
defer: obj.defer,
"render blocking": obj.async || obj.defer ? "" : "🟥",
};
return newObj;
});
console.table(scriptsLoading);
Check is the page has resources hints
const rels = [
"preload",
"prefetch",
"preconnect",
"dns-prefetch",
"preconnect dns-prefetch",
"prerender",
"modulepreload",
];
rels.forEach((element) => {
const linkElements = document.querySelectorAll(`link[rel="${element}"]`);
const dot = linkElements.length > 0 ? "🟩" : "🟥";
console.log(`${dot} ${element}`);
linkElements.forEach((el) => console.log(el));
});
List all images that have loading="lazy"
or [data-src]
(lazy loading via JS) above the fold
function findATFLazyLoadedImages() {
const lazy = document.querySelectorAll('[loading="lazy"], [data-src]');
let lazyImages = [];
lazy.forEach((tag) => {
const position = parseInt(tag.getBoundingClientRect().top);
if (position < window.innerHeight && position !== 0) {
lazyImages = [...lazyImages, tag];
}
});
return lazyImages.length > 0 ? lazyImages : false;
}
console.log(findATFLazyLoadedImages());
List all images that don't have loading="lazy"
or [data-src]
(lazy loading via JS) and are not in the viewport when the page loads. This script will help you find candidates for lazy loading.
// Execute it after the page has loaded without any user interaction (Scroll, click, etc)
function findImgCanidatesForLazyLoading() {
let notLazyImages = document.querySelectorAll(
'img:not([data-src]):not([loading="lazy"])'
);
return Array.from(notLazyImages).filter((tag) => !isInViewport(tag));
}
function isInViewport(tag) {
let rect = tag.getBoundingClientRect();
return (
rect.bottom >= 0 &&
rect.right >= 0 &&
rect.top <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.left <= (window.innerWidth || document.documentElement.clientWidth)
);
}
console.log(
"Consider lazyloading the following images: ",
findImgCanidatesForLazyLoading()
);
List all resources that are blocking rendering.
It's currently Chromium only
function RenderBlocking({startTime, duration, responseEnd, name, initiatorType}) {
this.startTime = startTime
this.duration = duration
this.responseEnd = responseEnd
this.name = name
this.initiatorType = initiatorType
}
function findRenderBlockingResources() {
return window.performance.getEntriesByType('resource')
.filter(({renderBlockingStatus}) => renderBlockingStatus === 'blocking')
.map(({startTime, duration, responseEnd, name, initiatorType}) => new RenderBlocking({startTime, duration, responseEnd, name, initiatorType}));
}
console.table(findRenderBlockingResources())
List all image resources and sort by (name, transferSize, encodedBodySize, decodedBodySize, initiatorType
)
function getImgs(sortBy) {
const imgs = [];
const resourceListEntries = performance.getEntriesByType("resource");
resourceListEntries.forEach(
({
name,
transferSize,
encodedBodySize,
decodedBodySize,
initiatorType,
}) => {
if (initiatorType == "img") {
imgs.push({
name,
transferSize,
decodedBodySize,
encodedBodySize,
});
}
}
);
const imgList = imgs.sort((a, b) => {
return b[sortBy] - a[sortBy];
});
return imgList;
}
console.table(getImgs("encodedBodySize"));
List all the fonts preloaded via resources hints, all the fonts loaded via CSS, and all the fonts used in the viewport above the fold.
const linkElements = document.querySelectorAll(`link[rel="preload"]`);
const arrayLinks = Array.from(linkElements);
const preloadedFonts = arrayLinks.filter((link) => link.as === "font");
console.log("Fonts Preloaded via Resources Hints");
preloadedFonts.forEach((font) => console.log(`▸ ${font.href}`));
console.log("");
const loadedFonts = [
...new Set(
Array.from(document.fonts.values())
.map((font) => font)
.filter((font) => font.status === "loaded")
.map((font) => `${font.family} - ${font.weight} - ${font.style}`)
),
];
console.log("Fonts and Weights Loaded in the Document");
loadedFonts.forEach((font) => console.log(`▸ ${font}`));
console.log("");
const childrenSlector =
"body * > *:not(script):not(style):not(link):not(source)";
const aboveFoldElements = Array.from(
document.querySelectorAll(childrenSlector)
).filter((elm) => {
const rect = elm.getBoundingClientRect();
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <=
(window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
);
});
const usedFonts = Array.from(
new Set(
[...aboveFoldElements].map(
(e) =>
`${getComputedStyle(e).fontFamily} | ${
getComputedStyle(e).fontWeight
} | ${getComputedStyle(e).fontStyle}`
)
)
);
console.log("Fonts and Weights Used Above the Fold");
usedFonts.forEach((font) => console.log(`▸ ${font}`));
List all scripts using PerformanceResourceTiming API and separating them by first and third party
// ex: katespade.com - list firsty party subdomains in HOSTS array
const HOSTS = ["assets.katespade.com"];
function getScriptInfo() {
const resourceListEntries = performance.getEntriesByType("resource");
// set for first party scripts
const first = [];
// set for third party scripts
const third = [];
resourceListEntries.forEach((resource) => {
// check for initiator type
const value = "initiatorType" in resource;
if (value) {
if (resource.initiatorType === "script") {
const { host } = new URL(resource.name);
// check if resource url host matches location.host = first party script
if (host === location.host || HOSTS.includes(host)) {
const json = resource.toJSON();
first.push({ ...json, type: "First Party" });
} else {
// add to third party script
const json = resource.toJSON();
third.push({ ...json, type: "Third Party" });
}
}
}
});
const scripts = {
firstParty: [{ name: "no data" }],
thirdParty: [{ name: "no data" }],
};
if (first.length) {
scripts.firstParty = first;
}
if (third.length) {
scripts.thirdParty = third;
}
return scripts;
}
const { firstParty, thirdParty } = getScriptInfo();
console.groupCollapsed("FIRST PARTY SCRIPTS");
console.table(firstParty);
console.groupEnd();
console.groupCollapsed("THIRD PARTY SCRIPTS");
console.table(thirdParty);
console.groupEnd();
/*
Choose which properties to display
https://developer.mozilla.org/en-US/docs/Web/API/console/table
console.groupCollapsed("FIRST PARTY SCRIPTS");
console.table(firstParty, ["name", "nextHopProtocol"]);
console.groupEnd();
console.groupCollapsed("THIRD PARTY SCRIPTS", ["name", "nextHopProtocol"]);
console.table(thirdParty);
console.groupEnd();
*/
This relies on the above script
Run First And Third Party Script Info in the console first, then run this
Info on CORS (why some values are 0)
Note: The properties which are returned as 0 by default when loading a resource from a domain other than the one of the web page itself: redirectStart, redirectEnd, domainLookupStart, domainLookupEnd, connectStart, connectEnd, secureConnectionStart, requestStart, and responseStart.
More Info on TAO header - Akamai Developer Resources
function createUniqueLists(firstParty, thirdParty) {
function getUniqueListBy(arr, key) {
return [...new Map(arr.map((item) => [item[key], item])).values()];
}
const firstPartyList = getUniqueListBy(firstParty, ["name"]);
const thirdPartyList = getUniqueListBy(thirdParty, ["name"]);
return { firstPartyList, thirdPartyList };
}
const { firstPartyList, thirdPartyList } = createUniqueLists(
firstParty,
thirdParty
);
function calculateTimings(party, type) {
const partyChoice = party === "first" ? firstParty : thirdParty;
const timingChoices = {
DNS_TIME: ["domainLookupEnd", "domainLookupStart"],
TCP_HANDSHAKE: ["connectEnd", "connectStart"],
RESPONSE_TIME: ["responseEnd", "responseStart"],
SECURE_CONNECTION_TIME: ["connectEnd", "secureConnectionStart", 0],
FETCH_UNTIL_RESPONSE: ["responseEnd", "fetchStart", 0],
REQ_START_UNTIL_RES_END: ["responseEnd", "requestStart", 0],
START_UNTIL_RES_END: ["responseEnd", "startTime", 0],
REDIRECT_TIME: ["redirectEnd", "redirectStart"],
};
function handleChoices(timingEnd, timingStart, num) {
if (!num) {
return timingEnd - timingStart;
}
if (timingStart > 0) {
return timingEnd - timingStart;
}
return 0;
}
const timings = partyChoice.map((script) => {
const [timingEnd, timingStart, num] = timingChoices[type];
const endValue = script[timingEnd];
const startValue = script[timingStart];
return {
name: script.name,
[type]: handleChoices(endValue, startValue, num),
};
});
return timings;
}
// Available Options
const timingOptions = [
"DNS_TIME",
"TCP_HANDSHAKE",
"RESPONSE_TIME",
"SECURE_CONNECTION_TIME",
"FETCH_UNTIL_RESPONSE",
"REQ_START_UNTIL_RES_END",
"START_UNTIL_RES_END",
"REDIRECT_TIME",
];
// run em all!
// https://developer.mozilla.org/en-US/docs/Web/API/Resource_Timing_API/Using_the_Resource_Timing_API#timing_resource_loading_phases
timingOptions.forEach((timing) => {
console.groupCollapsed(`FIRST PARTY: ${timing}`);
console.table(calculateTimings("first", timing));
console.groupEnd();
console.groupCollapsed(`THIRD PARTY: ${timing}`);
console.table(calculateTimings("third", timing));
console.groupEnd();
});
// choose your battle - arg1 is string either "first" or "third", arg2 is string timing option listed above.
console.table(calculateTimings("first", "REQ_START_UNTIL_RES_END"));
Find all inline scripts on the page and list the scripts and count. Find the total byte size of all the inline scripts in the console.
function findInlineScripts() {
const inlineScripts = document.querySelectorAll(["script:not([async]):not([defer]):not([src])"])
console.log(inlineScripts)
console.log(`COUNT: ${inlineScripts.length}`)
let totalByteSize = 0
let NEXT_DATA_SIZE = 0
for (const script of [...inlineScripts]) {
const html = script.innerHTML
const size = new Blob([html]).size
if (script.id === "__NEXT_DATA__") {
NEXT_DATA_SIZE += size
}
totalByteSize += size
return {
totalByteSize: (totalByteSize / 1000) + " kb",
NEXT_DATA_SIZE: NEXT_DATA_SIZE === 0 ? (NEXT_DATA_SIZE / 1000) : 0
}
}
}
console.log(findInlineScripts())
Find all inline scripts and their total size separately from __NEXT_DATA__
serialized JSON inline Script
function findInlineScriptsWithNextData() {
const inlineScripts = document.querySelectorAll([
"script:not([async]):not([defer]):not([src])"
]);
console.log(inlineScripts);
console.log(`COUNT: ${inlineScripts.length}`);
const byteSize = {
NEXT_DATA_SIZE: 0,
OTHER_SIZE: 0
};
function getSize(script) {
const html = script.innerHTML;
return new Blob([html]).size;
}
function convertToKb(bytes) {
return bytes / 1000;
}
for (const script of [...inlineScripts]) {
if (script.id == "__NEXT_DATA__") {
byteSize.NEXT_DATA_SIZE += getSize(script);
} else {
byteSize.OTHER_SIZE += getSize(script);
}
}
return {
NEXT_DATA_SIZE: convertToKb(byteSize.NEXT_DATA_SIZE) + " kb",
OTHER_SIZE: convertToKb(byteSize.OTHER_SIZE) + " kb",
totalByteSize:
convertToKb(byteSize.NEXT_DATA_SIZE) +
convertToKb(byteSize.OTHER_SIZE) +
" kb"
};
}
console.log(findInlineScriptsWithNextData());
How you order elements in the can have an effect on the (perceived) performance of the page.
Use capo.js the Rick Viscomi script
To determine when long tasks happen, you can use PerformanceObserver and register to observe entries of type longtask
:
try {
// Create the performance observer.
const po = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// Log the entry and all associated details.
console.table(entry.toJSON());
}
});
// Start listening for `longtask` entries to be dispatched.
po.observe({ type: "longtask", buffered: true });
} catch (e) {
console.error(`The browser doesn't support this API`);
}
To find more specific information about layout shifts, you can use PerformanceObserver and register to observe entries of type layout-shift
:
function genColor() {
let n = (Math.random() * 0xfffff * 1000000).toString(16);
return "#" + n.slice(0, 6);
}
// console.log(shifts) to see full list of shifts above threshold
const shifts = [];
// threshold ex: 0.05
// Layout Shifts will be grouped by color.
// All nodes attributed to the shift will have a border with the corresponding color
// Shift value will be added above parent node.
// Will have all details related to that shift in dropdown
// Useful for single page applications and finding shifts after initial load
function findShifts(threshold) {
return new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.value > threshold && !entry.hadRecentInput) {
const color = genColor();
shifts.push(entry);
console.log(shifts);
const valueNode = document.createElement("details");
valueNode.innerHTML = `
<summary>Layout Shift: ${entry.value}</summary>
<pre>${JSON.stringify(entry, null, 2)}</pre>
`;
valueNode.style = `color: ${color};`;
entry.sources.forEach((source) => {
source.node.parentNode.insertBefore(valueNode, source.node);
source.node.style = `border: 2px ${color} solid`;
});
}
});
});
}
findShifts(0.05).observe({ entryTypes: ["layout-shift"] });
Print al the CLS metrics when load the page and the user interactive with the page:
new PerformanceObserver((entryList) => {
console.log(entryList.getEntries());
}).observe({ type: "layout-shift", buffered: true });