A curated list of snippets to get Web Performance metrics to use in the browser console
/** * 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()]; }
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.log(`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"
above the fold
function findATFLazyLoadedImages() { const lazy = document.querySelectorAll('[loading="lazy"]'); let flag = false; lazy.forEach((tag) => { const position = parseInt(tag.getBoundingClientRect().top); if (position < window.innerHeight && position !== 0) { console.log(tag, position); flag = true; } }); return flag; } console.log(findATFLazyLoadedImages());
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 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"));
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.log(`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 });