coach-core
Version:
Core package for the Coach.
1,595 lines (1,423 loc) • 97.1 kB
JavaScript
(function() {
if (typeof window !== 'undefined') {
'use strict';
const util = {
/**
* Make your URL absolute.
* @memberof util
* @param {String} url The URL to convert to absolute.
* @returns {String} the absolute URL including host/protocol.
*/
getAbsoluteURL: function (url) {
const a = globalThis.document.createElement('a');
a.href = url;
return a.href;
},
/**
* Get the hostname from a URL
* @memberof util
* @param {String} url The URL.
* @returns {String} the hostname from the URL.
*/
getHostname: function (url) {
const a = globalThis.document.createElement('a');
a.href = url;
return a.hostname;
},
/**
* Checks if an element exist in an array.
*
* @memberof util
* @param {String} element the element.
* @param {Array} array the array.
* @returns {Boolean} true if the element exist.
*/
exists: function (element, array) {
return array.includes(element);
},
/**
* Returns an array filter function for finding tags with an attribute specified value, matching case insensitive.
* @param attributeName the name of the attribute to look for (html attribute values are always case insensitive).
* @param attributeValue the value to match against, ignoring case.
* @returns {Function} function that can be passed to Array#filter
*/
caseInsensitiveAttributeValueFilter: function (
attributeName,
attributeValue
) {
return function (item) {
const attribute = item.getAttribute(attributeName) || '';
if (attribute.toLowerCase() === attributeValue.toLowerCase()) {
return item;
}
return;
};
},
/**
* Is the connection used for the main document using HTTP/2?
* Works in Chrome, Firefox, Edge and other browsers that supports Resource Timing
* API v2 (not Safari yet).
* @memberof util
* @returns {Boolean} true if the connection is HTTP/2
*/
isHTTP2: function () {
const type = util.getConnectionType().toLowerCase();
return type === 'h2';
},
/**
* Is the connection used for the main document using HTTP/3?
* @memberof util
* @returns {Boolean} true if the connection is HTTP/3
*/
isHTTP3: function () {
const type = util.getConnectionType().toLowerCase();
return type.startsWith('h3');
},
/**
* Get the connection type used for the main document. Works in Chrome, Firefox,
* Edge and + browsers that support Resource Timing
* API v2.
* @memberof util
* @returns {String} http/1 or h2 for http 1 and 2 respectively. 'unknown' if browser lacks api to determine it.
*/
getConnectionType: function () {
// it's easy in Chrome
if (
globalThis.performance.getEntriesByType('navigation') &&
globalThis.performance.getEntriesByType('navigation')[0] &&
globalThis.performance.getEntriesByType('navigation')[0].nextHopProtocol
) {
return globalThis.performance.getEntriesByType('navigation')[0]
.nextHopProtocol;
} else if (
globalThis.performance &&
globalThis.performance.getEntriesByType &&
globalThis.performance.getEntriesByType('resource')
) {
// if you support resource timing v2
// it's kind of easy too
const resources = globalThis.performance.getEntriesByType('resource');
// now we "only" need to know if it is v2
if (resources.length > 1 && resources[0].nextHopProtocol) {
// if it's the same domain, say it's OK
const host = document.domain;
for (let i = 0, len = resources.length; i < len; i++) {
if (host === util.getHostname(resources[i].name)) {
return resources[i].nextHopProtocol;
}
}
}
}
return 'unknown';
},
/**
* Get JavaScript requests that are loaded synchronously. All URLs are absolute.
* @memberof util
* @param {Object} parentElement the parent element that has all the scripts.
* @returns {Array} an array with the URL to each JavaScript file that is loaded synchronously.
*/
getSynchJSFiles: function (parentElement) {
const scripts = Array.prototype.slice.call(
parentElement.querySelectorAll('script')
);
return scripts
.filter(function (s) {
return !s.async && s.src && !s.defer;
})
.map(function (s) {
return util.getAbsoluteURL(s.src);
});
},
/**
* Get JavaScript requests that are loaded asynchronously. All URLs are absolute.
* @memberof util
* @param {Object} parentElement the parent element that has all the scripts.
* @returns {Array} an array with the URL to each JavaScript file that are loaded asynchronously.
*/
getAsynchJSFiles: function (parentElement) {
const scripts = Array.prototype.slice.call(
parentElement.querySelectorAll('script')
);
return scripts
.filter(function (s) {
return s.async && s.src;
})
.map(function (s) {
return util.getAbsoluteURL(s.src);
});
},
/**
* Get Resource Hints hrefs by type
* @memberof util
* @param {String} type the name of the Resources hint: dns-prefetch, preconnect, prefetch, prerender
* @returns {Array} an array of matching hrefs
*/
getResourceHintsHrefs: function (type) {
const links = Array.prototype.slice.call(
globalThis.document.head.querySelectorAll('link')
);
return links
.filter(function (link) {
return link.rel === type;
})
.map(function (link) {
return link.href;
});
},
/**
* Get CSS requests. All URLs are absolute.
* @memberof util
* @param {Object} parentElement the parent element that has all the scripts.
* @returns {Array} an array with the URL to each CSS file that is loaded synchronously.
*/
getCSSFiles: function (parentElement) {
const links = Array.prototype.slice.call(
parentElement.querySelectorAll('link')
);
return links
.filter(function (link) {
// make sure we skip data:
return link.rel === 'stylesheet' && !link.href.startsWith('data:');
})
.map(function (link) {
return util.getAbsoluteURL(link.href);
});
},
plural: function (number, text) {
if (number > 1) {
text += 's';
}
return `${number} ${text}`;
},
/**
* Get the size of an asset. Will try to use the Resource Timing V2. If that's
* not available or the asset size is unknown we report 0.
**/
getTransferSize: function (url) {
const entries = globalThis.performance.getEntriesByName(url, 'resource');
return entries.length === 1 && typeof entries[0].transferSize === 'number'
? entries[0].transferSize
: 0;
},
ms(ms) {
return ms < 1000 ? ms + ' ms' : Number(ms / 1000).toFixed(3) + ' s';
}
};
return (function(util) {
var advice = {},
errors = {};
var bestpracticeResults = {},
bestpracticeErrors = {};
try {
bestpracticeResults["amp"] = (function () {
'use strict';
const offending = [];
const html = document.querySelectorAll('html')[0];
let score = 100;
if ((html && html.getAttribute('amp-version')) || globalThis.AMP) {
score = 0;
}
return {
id: 'amp',
title: 'Avoid using AMP',
description:
"AMP was one of Google attempts to strengthen its monopoly in the Interente advertising market. You can read more about it here: https://storage.courtlistener.com/recap/gov.uscourts.nysd.564903/gov.uscourts.nysd.564903.152.0_1.pdf Using AMP you also share private user information with Google that your user hasn't agreed on sharing.",
advice:
score === 0
? 'The page is using AMP, that makes you share private user information with Google.'
: '',
score: score,
weight: 1,
severity: 'info',
offending: offending,
tags: ['bestpractice']
};
})();
} catch(err) {
bestpracticeErrors["amp"] = err.message;
}
try {
bestpracticeResults["charset"] = (function () {
'use strict';
let score = 100;
let message = '';
const charSet = document.characterSet;
if (charSet === null) {
message =
'The page is missing a character set. If you use Chrome/Firefox we know you are missing it, if you use another browser, it could be an implementation problem.';
score = 0;
// eslint-disable-next-line unicorn/text-encoding-identifier-case
} else if (charSet !== 'UTF-8') {
message = 'You are not using charset UTF-8?';
score = 50;
}
return {
id: 'charset',
title: 'Declare a charset in your document',
description:
'The Unicode Standard (UTF-8) covers (almost) all the characters, punctuations, and symbols in the world. Please use that.',
advice: message,
score: score,
weight: 2,
severity: 'info',
offending: [],
tags: ['bestpractice']
};
})();
} catch(err) {
bestpracticeErrors["charset"] = err.message;
}
try {
bestpracticeResults["cumulativeLayoutShift"] = (function () {
'use strict';
const offending = [];
let advice = 'There is no Layout Shift on the page.';
let score = 0;
let max = 0;
const supported = PerformanceObserver.supportedEntryTypes;
if (!supported || !supported.includes('layout-shift')) {
advice = 'Layout Shift is not supported in this browser';
} else {
// See https://web.dev/layout-instability-api
// https://github.com/mmocny/web-vitals/wiki/Snippets-for-LSN-using-PerformanceObserver#max-session-gap1s-limit5s
let curr = 0;
let firstTs = Number.NEGATIVE_INFINITY;
let prevTs = Number.NEGATIVE_INFINITY;
const observer = new PerformanceObserver(() => {});
observer.observe({ type: 'layout-shift', buffered: true });
const list = observer.takeRecords();
for (let entry of list) {
if (entry.hadRecentInput) {
continue;
}
if (entry.startTime - firstTs > 5000 || entry.startTime - prevTs > 1000) {
firstTs = entry.startTime;
curr = 0;
}
prevTs = entry.startTime;
curr += entry.value;
max = Math.max(max, curr);
}
if (max <= 0.1) {
score = 100;
} else if (max > 0.25) {
score = 0;
advice = `You have a poor cumulative layout shift score (${max.toFixed(
4
)}). It is in the Google Web Vitals poor range, with a shift higher than 0.25. You should manually check the filmstrip or video and check if it will affect the user.`;
} else {
score = 50;
advice = `You have a cumulative layout shift score (${max.toFixed(
4
)}) that needs improvements. It is in the Google Web Vitals needs improvement range, shift higher than 0.1. You should manually check the filmstrip or video and check if it will affect the user.`;
}
}
return {
id: 'cumulativeLayoutShift',
title: 'Cumulative Layout Shift',
description:
'Cumulative Layout Shift measures the sum total of all individual layout shift scores for unexpected layout shift that occur. The metric is measuring visual stability by quantify how often users experience unexpected layout shifts. It is one of Google Web Vitals.',
advice,
score,
weight: 8,
severity: 'error',
offending,
tags: ['bestpractice']
};
})();
} catch(err) {
bestpracticeErrors["cumulativeLayoutShift"] = err.message;
}
try {
bestpracticeResults["doctype"] = (function () {
'use strict';
let score = 100;
let message = '';
const docType = document.doctype;
if (docType === null) {
message = 'The page is missing a doctype. Please use <!DOCTYPE html>.';
score = 0;
} else if (
!(
docType.name.toLowerCase() === 'html' &&
(docType.systemId === '' ||
docType.systemId.toLowerCase() === 'about:legacy-compat')
)
) {
message =
'Just do yourself a favor and use the HTML5 doctype declaration: <!DOCTYPE html>';
score = 25;
}
return {
id: 'doctype',
title: 'Declare a doctype in your document',
description:
'The <!DOCTYPE> declaration is not an HTML tag; it is an instruction to the web browser about what version of HTML the page is written in.',
advice: message,
score: score,
weight: 2,
severity: 'warn',
offending: [],
tags: ['bestpractice']
};
})();
} catch(err) {
bestpracticeErrors["doctype"] = err.message;
}
try {
bestpracticeResults["imageAltText"] = (function (util) {
'use strict';
// An <img> needs a textual alternative for assistive tech. The accepted
// shapes are: alt="meaningful text" for content images, or alt="" (decorative)
// / role="presentation" / aria-hidden="true" for purely decorative images.
// A missing alt attribute entirely is the failure case.
const offending = [];
const images = document.querySelectorAll('img');
for (let i = 0, len = images.length; i < len; i++) {
const img = images[i];
const hasAlt = img.hasAttribute('alt');
const ariaHidden = img.getAttribute('aria-hidden');
const role = img.getAttribute('role');
const isMarkedDecorative =
(ariaHidden && ariaHidden.toLowerCase() === 'true') ||
(role && role.toLowerCase() === 'presentation') ||
(role && role.toLowerCase() === 'none');
if (!hasAlt && !isMarkedDecorative) {
offending.push(util.getAbsoluteURL(img.currentSrc || img.src || ''));
}
}
const score =
offending.length === 0 ? 100 : Math.max(0, 100 - offending.length * 10);
return {
id: 'imageAltText',
title: 'Give every image a textual alternative',
description:
'Every <img> needs an alt attribute. Use alt="meaningful description" for content images so assistive technologies can announce them, or alt="" (or role="presentation" / aria-hidden="true") for purely decorative images so they are skipped. A missing alt attribute leaves screen reader users with no information at all. https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#alt',
advice:
offending.length > 0
? 'The page has ' +
util.plural(offending.length, 'image') +
' without an alt attribute. Add alt="..." with a description, or alt="" if the image is purely decorative.'
: '',
score: score,
weight: 4,
severity: 'info',
offending: offending,
tags: ['bestpractice', 'accessibility']
};
})(util);
} catch(err) {
bestpracticeErrors["imageAltText"] = err.message;
}
try {
bestpracticeResults["language"] = (function () {
'use strict';
const html = document.querySelectorAll('html');
const language = html[0].getAttribute('lang');
let score = 100;
let message = '';
if (html.length > 0) {
if (language === null) {
score = 0;
message =
'The page is missing a language definition in the HTML tag. Define it with <html lang="YOUR_LANGUAGE_CODE">';
}
} else {
score = 0;
message = 'What! The page is missing the HTML tag!';
}
return {
id: 'language',
title: 'Declare the language code for your document',
description:
'According to the W3C recommendation you should declare the primary language for each Web page with the lang attribute inside the <html> tag https://www.w3.org/International/questions/qa-html-language-declarations#basics.',
advice: message,
score: score,
weight: 3,
severity: 'warn',
offending: [],
tags: ['bestpractice']
};
})();
} catch(err) {
bestpracticeErrors["language"] = err.message;
}
try {
bestpracticeResults["metaDescription"] = (function (util) {
'use strict';
const maxLength = 160;
let score = 100;
let message = '';
let metas = Array.prototype.slice.call(
document.querySelectorAll('meta[name][content]')
);
metas = metas.filter(
util.caseInsensitiveAttributeValueFilter('name', 'description')
);
const description = metas.length > 0 ? metas[0].getAttribute('content') : '';
if (description.length === 0) {
message = 'The page is missing a meta description.';
score = 0;
} else if (description.length > maxLength) {
message =
'The meta description is too long. It has ' +
description.length +
' characters, the recommended max is ' +
maxLength;
score = 50;
}
// http://static.googleusercontent.com/media/www.google.com/en//webmasters/docs/search-engine-optimization-starter-guide.pdf
// https://d2eeipcrcdle6.cloudfront.net/seo-cheat-sheet.pdf
return {
id: 'metaDescription',
title: 'Meta description',
description:
'Use a page description to make the page more relevant to search engines.',
advice: message,
score: score,
weight: 5,
severity: 'info',
offending: [],
tags: ['bestpractice']
};
})(util);
} catch(err) {
bestpracticeErrors["metaDescription"] = err.message;
}
try {
bestpracticeResults["optimizely"] = (function (util) {
'use strict';
const scripts = util.getSynchJSFiles(document.head);
const offending = [];
let score = 100;
let advice = '';
for (const script of scripts) {
if (util.getHostname(script) === 'cdn.optimizely.com') {
offending.push(script);
score = 0;
advice =
'The page is using Optimizely. Use it with care because it hurts your performance. Only turn it on (= load the JavaScript) when you run your A/B tests. Then when you are finished make sure to turn it off.';
}
}
return {
id: 'optimizely',
title: 'Only use Optimizely when you need it',
description:
'Use Optimizely with care because it hurts your performance since JavaScript is loaded synchronously inside of the head tag, making the first paint happen later. Only turn on Optimzely (= load the javascript) when you run your A/B tests.',
advice: advice,
score: score,
weight: 2,
severity: 'info',
offending: offending,
tags: ['bestpractice']
};
})(util);
} catch(err) {
bestpracticeErrors["optimizely"] = err.message;
}
try {
bestpracticeResults["pageTitle"] = (function () {
'use strict';
const max = 60;
const title = document.title;
let score = 100;
let message = '';
if (title.length === 0) {
message = 'The page is missing a title.';
score = 0;
} else if (title.length > max) {
message =
'The title is too long by ' +
(title.length - max) +
' characters. The recommended max is ' +
max;
score = 50;
}
return {
id: 'pageTitle',
title: 'Page title',
description:
'Use a title to make the page more relevant to search engines.',
advice: message,
score: score,
weight: 5,
severity: 'warn',
offending: [],
tags: ['bestpractice']
};
})();
} catch(err) {
bestpracticeErrors["pageTitle"] = err.message;
}
try {
bestpracticeResults["url"] = (function () {
'use strict';
const url = document.URL;
let score = 100;
let message = '';
// ok all Java lovers, please do not use the sessionid in your URLs
if (url.includes('?') && url.indexOf('jsessionid') > url.indexOf('?')) {
score = 0;
message =
'The page has the session id for the user as a parameter, please change so the session handling is done only with cookies. ';
}
var parameters = (url.match(/&/g) || []).length;
if (parameters > 1) {
score -= 50;
message +=
'The page is using more than two request parameters. You should really rethink and try to minimize the number of parameters. ';
}
if (url.length > 100) {
score -= 10;
message +=
'The URL is ' +
url.length +
' characters long. Try to make it less than 100 characters. ';
}
if (url.includes(' ') || url.includes('%20')) {
score -= 10;
message +=
'Could the developer or the CMS be on Windows? Avoid using spaces in the URLs, use hyphens or underscores. ';
}
return {
id: 'url',
title: 'Have a good URL format',
description:
'A clean URL is good for the user and for SEO. Make them human readable, avoid too long URLs, spaces in the URL, too many request parameters, and never ever have the session id in your URL.',
advice: message,
score: Math.max(score, 0),
weight: 2,
severity: 'info',
offending: [],
tags: ['bestpractice']
};
})();
} catch(err) {
bestpracticeErrors["url"] = err.message;
}
try {
bestpracticeResults["viewport"] = (function () {
'use strict';
// A correct viewport meta tag is the baseline for responsive layout on
// mobile. Without it the browser uses a desktop-width fallback (typically
// 980px) and scales the page down, which makes text unreadable. Locking
// user-scalable=no or maximum-scale=1 also breaks pinch-to-zoom and is an
// accessibility regression, so we flag those values too.
let content = '';
const metas = document.querySelectorAll('meta');
for (let i = 0, len = metas.length; i < len; i++) {
const name = metas[i].getAttribute('name');
if (name && name.toLowerCase() === 'viewport') {
content = (metas[i].getAttribute('content') || '').trim();
break;
}
}
let score = 100;
let advice = '';
const lower = content.toLowerCase();
if (content.length === 0) {
score = 0;
advice =
'The page is missing a viewport meta tag. Add <meta name="viewport" content="width=device-width, initial-scale=1"> so the browser lays the page out at the device width.';
} else {
if (!lower.includes('width=device-width')) {
score -= 50;
advice +=
'The viewport meta tag does not contain width=device-width, the browser may use a desktop-width fallback. ';
}
if (
lower.includes('user-scalable=no') ||
lower.includes('user-scalable=0')
) {
score -= 30;
advice +=
'The viewport meta tag disables user-scalable, which breaks pinch-to-zoom and is an accessibility problem. ';
}
if (/maximum-scale\s*=\s*(0?\.\d+|1(\.0+)?)\b/.test(lower)) {
score -= 20;
advice +=
'The viewport meta tag sets maximum-scale to 1 or less, which prevents the user from zooming in. Remove it. ';
}
}
return {
id: 'viewport',
title: 'Set a sensible viewport meta tag',
description:
'The viewport meta tag tells the browser how to lay out the page on small screens. Without it (or without width=device-width) the page is rendered at a desktop fallback width and scaled down, which makes text unreadable on mobile. Disabling zoom (user-scalable=no, maximum-scale<=1) is also an accessibility regression. https://developer.mozilla.org/en-US/docs/Web/HTML/Viewport_meta_tag',
advice: advice.trim(),
score: Math.max(0, score),
weight: 5,
severity: 'warn',
offending: [],
tags: ['bestpractice']
};
})();
} catch(err) {
bestpracticeErrors["viewport"] = err.message;
}
advice["bestpractice"] = {
'adviceList': bestpracticeResults
};
if (Object.keys(bestpracticeErrors).length > 0) {
errors["bestpractice"] = bestpracticeErrors;
}
var infoResults = {},
infoErrors = {};
try {
infoResults["amp"] = (function () {
'use strict';
const html = document.querySelectorAll('html')[0];
return (html && html.getAttribute('amp-version')) || globalThis.AMP
? html.getAttribute('amp-version') || true
: false;
})();
} catch(err) {
infoErrors["amp"] = err.message;
}
try {
infoResults["browser"] = (function () {
'use strict';
const { userAgent } = navigator;
if (userAgent.includes('Firefox/')) {
return `Firefox ${userAgent.split('Firefox/')[1]}`;
} else if (userAgent.includes('Edg/')) {
return `Edge ${userAgent.split('Edg/')[1]}`;
} else if (userAgent.includes('Chrome/')) {
return `Chrome ${userAgent.match(/(Chrome)\/(\S+)/)[2]}`;
} else if (userAgent.includes('Safari/')) {
return `Safari ${userAgent.match(/(Version)\/(\S+)/)[2]}`;
} else return 'Unknown';
})();
} catch(err) {
infoErrors["browser"] = err.message;
}
try {
infoResults["connectionType"] = (function (util) {
'use strict';
return util.getConnectionType();
})(util);
} catch(err) {
infoErrors["connectionType"] = err.message;
}
try {
infoResults["documentHeight"] = (function () {
'use strict';
return Math.max(
document.body.scrollHeight,
document.body.offsetHeight,
document.documentElement.clientHeight,
document.documentElement.scrollHeight,
document.documentElement.offsetHeight
);
})();
} catch(err) {
infoErrors["documentHeight"] = err.message;
}
try {
infoResults["documentTitle"] = (function () {
'use strict';
return document.title;
})();
} catch(err) {
infoErrors["documentTitle"] = err.message;
}
try {
infoResults["documentWidth"] = (function () {
'use strict';
return Math.max(
document.body.scrollWidth,
document.body.offsetWidth,
document.documentElement.clientWidth,
document.documentElement.scrollWidth,
document.documentElement.offsetWidth
);
})();
} catch(err) {
infoErrors["documentWidth"] = err.message;
}
try {
infoResults["domDepth"] = (function () {
'use strict';
function domDepth(document) {
const allElems = document.querySelectorAll('*');
let allElemsLen = allElems.length;
let totalParents = 0;
let maxParents = 0;
while (allElemsLen--) {
let parents = numParents(allElems[allElemsLen]);
if (parents > maxParents) {
maxParents = parents;
}
totalParents += parents;
}
const average = totalParents / allElems.length;
return {
avg: average,
max: maxParents
};
}
function numParents(elem) {
let n = 0;
if (elem.parentNode) {
while ((elem = elem.parentNode)) {
n++;
}
}
return n;
}
const depth = domDepth(document);
return {
avg: Math.round(depth.avg),
max: depth.max
};
})();
} catch(err) {
infoErrors["domDepth"] = err.message;
}
try {
infoResults["domElements"] = (function () {
'use strict';
return document.querySelectorAll('*').length;
})();
} catch(err) {
infoErrors["domElements"] = err.message;
}
try {
infoResults["generator"] = (function () {
'use strict';
const description = document.querySelector('meta[name="generator"]');
if (description) {
return description.getAttribute('content');
}
})();
} catch(err) {
infoErrors["generator"] = err.message;
}
try {
infoResults["head"] = (function (util) {
'use strict';
/*
Get requests inside of head that will influence the start render
*/
return {
jssync: util.getSynchJSFiles(document.head),
jsasync: util.getAsynchJSFiles(document.head),
css: util.getCSSFiles(document.head)
};
})(util);
} catch(err) {
infoErrors["head"] = err.message;
}
try {
infoResults["iframes"] = (function () {
'use strict';
return document.querySelectorAll('iframe').length;
})();
} catch(err) {
infoErrors["iframes"] = err.message;
}
try {
infoResults["localStorageSize"] = (function () {
'use strict';
function storageSize(storage) {
// if local storage is disabled
if (storage) {
const keys = storage.length || Object.keys(storage).length;
let bytes = 0;
for (let i = 0; i < keys; i++) {
const key = storage.key(i);
const val = storage.getItem(key);
bytes += key.length + val.length;
}
return bytes;
} else {
return 0;
}
}
try {
return storageSize(globalThis.localStorage);
} catch {
return 'Could not access localStorage.';
}
})();
} catch(err) {
infoErrors["localStorageSize"] = err.message;
}
try {
infoResults["metaDescription"] = (function () {
'use strict';
const description = document.querySelector('meta[name="description"]');
const og = document.querySelector('meta[property="og:description"]');
if (description) {
return description.getAttribute('content');
} else if (og) {
return og.getAttribute('content');
} else {
return '';
}
})();
} catch(err) {
infoErrors["metaDescription"] = err.message;
}
try {
infoResults["networkConnectionType"] = (function () {
'use strict';
return globalThis.navigator.connection
? globalThis.navigator.connection.effectiveType
: 'unknown';
})();
} catch(err) {
infoErrors["networkConnectionType"] = err.message;
}
try {
infoResults["resourceHints"] = (function (util) {
'use strict';
return {
'dns-prefetch': util.getResourceHintsHrefs('dns-prefetch'),
preconnect: util.getResourceHintsHrefs('preconnect'),
prefetch: util.getResourceHintsHrefs('prefetch'),
prerender: util.getResourceHintsHrefs('prerender')
};
})(util);
} catch(err) {
infoErrors["resourceHints"] = err.message;
}
try {
infoResults["responsive"] = (function () {
'use strict';
// we now do the same check as WebPageTest
let isResponsive = true;
const bodyScrollWidth = document.body.scrollWidth;
const windowInnerWidth = window.innerWidth;
const nodes = document.body.children;
if (bodyScrollWidth > windowInnerWidth) {
isResponsive = false;
}
for (var i in nodes) {
if (nodes[i].scrollWidth > windowInnerWidth) {
isResponsive = false;
}
}
return isResponsive;
})();
} catch(err) {
infoErrors["responsive"] = err.message;
}
try {
infoResults["scripts"] = (function () {
'use strict';
return document.querySelectorAll('script').length;
})();
} catch(err) {
infoErrors["scripts"] = err.message;
}
try {
infoResults["serializedDomSize"] = (function () {
'use strict';
return document.body.innerHTML.length;
})();
} catch(err) {
infoErrors["serializedDomSize"] = err.message;
}
try {
infoResults["serviceWorker"] = (function () {
'use strict';
if ('serviceWorker' in navigator) {
// Only report activated service workers
if (navigator.serviceWorker.controller) {
return navigator.serviceWorker.controller.state === 'activated'
? navigator.serviceWorker.controller.scriptURL
: false;
} else {
return false;
}
} else {
return false;
}
})();
} catch(err) {
infoErrors["serviceWorker"] = err.message;
}
try {
infoResults["sessionStorageSize"] = (function () {
'use strict';
function storageSize(storage) {
const keys = storage.length || Object.keys(storage).length;
let bytes = 0;
for (let i = 0; i < keys; i++) {
const key = storage.key(i);
const val = storage.getItem(key);
bytes += key.length + val.length;
}
return bytes;
}
try {
return storageSize(globalThis.sessionStorage);
} catch {
return 'Could not access sessionStorage';
}
})();
} catch(err) {
infoErrors["sessionStorageSize"] = err.message;
}
try {
infoResults["userTiming"] = (function () {
'use strict';
let mark = 0;
let measure = 0;
if (globalThis.performance && globalThis.performance.getEntriesByType) {
measure = globalThis.performance.getEntriesByType('measure').length;
mark = globalThis.performance.getEntriesByType('mark').length;
}
return {
marks: mark,
measures: measure
};
})();
} catch(err) {
infoErrors["userTiming"] = err.message;
}
try {
infoResults["windowSize"] = (function () {
'use strict';
const width =
window.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth;
const height =
window.innerHeight ||
document.documentElement.clientHeight ||
document.body.clientHeight;
return width + 'x' + height;
})();
} catch(err) {
infoErrors["windowSize"] = err.message;
}
advice["info"] = infoResults;
if (Object.keys(infoErrors).length > 0) {
errors["info"] = infoErrors;
}
var performanceResults = {},
performanceErrors = {};
try {
performanceResults["avoidRenderBlocking"] = (function (util) {
'use strict';
const offending = [];
const styles = util.getCSSFiles(document.head);
const scripts = util.getSynchJSFiles(document.head);
const docDomain = document.domain;
const domains = [];
// TODO does preconnect really matter when you are inside of head?
const preconnects = util.getResourceHintsHrefs('preconnect');
const preconnectDomains = preconnects.map(function (preconnect) {
return util.getHostname(preconnect);
});
let blockingCSS = 0;
let blockingJS = 0;
let message = '';
let score = 0;
function testByType(assetUrl) {
const domain = util.getHostname(assetUrl);
// if it is from different domain or not
if (domain === docDomain) {
offending.push(assetUrl);
score += 5;
} else {
offending.push(assetUrl);
// is this the first time this domain is used?
if (!util.exists(domain, domains)) {
// hurt depending on if it's preconnected or not
score += util.exists(domain, preconnectDomains) ? 5 : 10;
domains.push(domain);
}
score += 5;
}
}
// TODO do we have a way to check if we different domains act as one for H2?
// know we don't even check it
if (util.isHTTP2()) {
if (styles.length > 0) {
message = '';
// check the size
for (const url of styles) {
if (util.getTransferSize(url) > 14_500) {
offending.push(url);
score += 5;
blockingCSS++;
message +=
'The style ' +
url +
' is larger than the magic number TCP window size 14.5 kB. Make the file smaller and the page will render faster. ';
}
}
}
if (scripts.length > 0) {
score += scripts.length * 10;
for (const url of scripts) {
offending.push(url);
blockingJS++;
}
message +=
"Avoid loading synchronously JavaScript inside of head, you shouldn't need JavaScript to render your page! ";
}
} else if (util.isHTTP3()) {
// Recommendations for HTTP3 to come
} else {
// we are using HTTP/1
for (const style of styles) {
testByType(style);
}
blockingCSS = styles.length;
for (const script of scripts) {
testByType(script);
}
blockingJS = scripts.length;
}
if (offending.length > 0) {
message += `The page has ${util.plural(
blockingCSS,
'render blocking CSS request'
)} and ${util.plural(
blockingJS,
'blocking JavaScript request'
)} inside of head.`;
}
return {
id: 'avoidRenderBlocking',
title: 'Avoid slowing down the critical rendering path',
description:
'The critical rendering path is what the browser needs to do to start rendering the page. Every file requested inside of the head element will postpone the rendering of the page, because the browser need to do the request. Avoid loading JavaScript synchronously inside of the head (you should not need JavaScript to render the page), request files from the same domain as the main document (to avoid DNS lookups) and inline CSS for really fast rendering and a short rendering path.',
advice: message,
score: Math.max(0, 100 - score),
weight: 10,
severity: 'warn',
offending: offending,
tags: ['performance']
};
})(util);
} catch(err) {
performanceErrors["avoidRenderBlocking"] = err.message;
}
try {
performanceResults["avoidScalingImages"] = (function (util) {
'use strict';
const minLimit = 100;
const offending = [];
const images = Array.prototype.slice.call(document.querySelectorAll('img'));
let score = 0;
let message = '';
for (let i = 0, len = images.length; i < len; i++) {
const img = images[i];
// skip svg images and images that are 0 (carousel etc)
if (img.clientWidth + minLimit < img.naturalWidth && img.clientWidth > 0) {
// message = message + ' ' + util.getAbsoluteURL(img.currentSrc) + ' [browserWidth:' + img.clientWidth + ' naturalWidth: ' + img.naturalWidth +']';
offending.push(util.getAbsoluteURL(img.currentSrc));
score += 10;
}
}
if (score > 0) {
message = `The page has ${util.plural(
score / 10,
'image'
)} that are scaled more than ${minLimit} pixels. It would be better if those images are sent so the browser don't need to scale them.`;
}
return {
id: 'avoidScalingImages',
title: "Don't scale images in the browser",
description:
"It's easy to scale images in the browser and make sure they look good in different devices, however that is bad for performance! Scaling images in the browser takes extra CPU time and will hurt performance on mobile. And the user will download extra kilobytes (sometimes megabytes) of data that could be avoided. Don't do that, make sure you create multiple version of the same image server-side and serve the appropriate one.",
advice: message,
score: Math.max(0, 100 - score),
weight: 5,
severity: 'warn',
offending: offending,
tags: ['performance', 'image']
};
})(util);
} catch(err) {
performanceErrors["avoidScalingImages"] = err.message;
}
try {
performanceResults["cssPrint"] = (function (util) {
'use strict';
const offending = [];
const links = document.querySelectorAll('link');
for (let i = 0, len = links.length; i < len; i++) {
if (links[i].media === 'print') {
offending.push(util.getAbsoluteURL(links[i].href));
}
}
const score = offending.length * 10;
return {
id: 'cssPrint',
title: 'Do not load specific print stylesheets.',
description:
'Loading a specific stylesheet for printing slows down the page, even though it is not used. You can include the print styles inside your other CSS file(s) just by using an @media query targeting type print.',
advice:
offending.length > 0
? `The page has ${util.plural(
offending.length,
'print stylesheet'
)}. You should include that stylesheet using @media type print instead.`
: '',
score: Math.max(0, 100 - score),
weight: 1,
severity: 'info',
offending: offending,
tags: ['performance', 'css']
};
})(util);
} catch(err) {
performanceErrors["cssPrint"] = err.message;
}
try {
performanceResults["decodingAsync"] = (function (util) {
'use strict';
// `decoding="async"` lets the browser decode an image off the main
// thread, which keeps interaction (and other rendering work)
// responsive while the image is being processed. The default is
// `decoding="auto"` which leaves the choice to the browser; setting
// it explicitly to `async` is the safest hint for non-LCP images.
//
// We only flag images that don't have any decoding hint at all. We
// don't flag `decoding="sync"` (the author may have a reason, e.g. a
// first-paint image where they want to ensure synchronous decode) and
// we don't flag `decoding="auto"` (the explicit default).
const offending = [];
const images = document.querySelectorAll('img');
let unhinted = 0;
let total = 0;
for (let i = 0, len = images.length; i < len; i++) {
const img = images[i];
if (!img.src && !img.currentSrc) {
continue;
}
total++;
if (!img.hasAttribute('decoding')) {
unhinted++;
offending.push(util.getAbsoluteURL(img.currentSrc || img.src));
}
}
// Score linearly with the proportion of images that lack the hint.
// 0% unhinted → 100, 100% unhinted → 0. We don't want to fail a page
// that has a single un-hinted image hard, so this stays gentle.
let score = 100;
let advice = '';
if (total > 0 && unhinted > 0) {
const ratio = unhinted / total;
score = Math.round(100 * (1 - ratio));
advice =
'The page has ' +
util.plural(unhinted, 'image') +
' (out of ' +
total +
') without a decoding hint. Add decoding="async" to non-critical images so the browser can decode them off the main thread.';
}
return {
id: 'decodingAsync',
title: 'Add decoding="async" to non-critical images',
description:
'Setting decoding="async" on an <img> tells the browser it can decode the image off the main thread, which keeps the page responsive to user interactions while images are being processed. The default ("auto") leaves the choice to the browser. https://developer.mozilla.org/en-US/docs/Web/HTML/Element/img#decoding',
advice: advice,
score: score,
weight: 2,
severity: 'info',
offending: offending,
tags: ['performance', 'image']
};
})(util);
} catch(err) {
performanceErrors["decodingAsync"] = err.message;
}
try {
performanceResults["firstContentfulPaint"] = (function (util) {
'use strict';
let score = 100;
let advice;
const good = 1800;
const needImprovement = 3000;
const fcpArray = performance.getEntriesByName('first-contentful-paint');
if (fcpArray.length > 0) {
const fcp = fcpArray[0].startTime;
if (fcp <= good) {
advice = `First contentful paint is good ${util.ms(fcp)}.`;
} else if (fcp <= needImprovement) {
score = 50;
advice = `First contentful paint can be improved (${util.ms(
fcp
)}). It is in the Google Web Vitals needs improvement range, slower than 1.8 seconds.`;
} else {
score = 0;
advice = `First contentful paint is poor (${util.ms(
fcp
)}). It is in the Google Web Vitals poor range, slower than 3 seconds.`;
let t = globalThis.performance.getEntriesByType('navigation')[0];
if (t && Number(t.responseStart.toFixed(0)) > 1000) {
advice += `The page has a high time to first byte (TTFB) ${util.ms(
t.responseStart
)} that you should look into to improve first contentful paint.`;
}
}
} else {
advice = 'There is no first contentful paint for this paint.';
}
return {
id: 'firstContentfulPaint',
title: 'Have a fast first contentful paint',
description:
'The First Contentful Paint (FCP) metric measures the time from when the page starts loading to when any part of the page content is rendered on the screen. For this metric, "content" refers to text, images (including background images), <svg> elements, or non-white <canvas> elements.',
advice: advice,
score: Math.max(0, score),
weight: 7,
severity: 'error',
offending: [],
tags: ['performance']
};
})(util);
} catch(err) {
performanceErrors["firstContentfulPaint"] = err.message;
}
try {
performanceResults["googleTagManager"] = (function () {
'use strict';
var score = 100;
if (globalThis.google_tag_manager) {
score = 0;
}
return {
id: 'googleTagManager',
title: 'Avoid using Google Tag Manager.',
description:
'Google Tag Manager makes it possible for non tech users to add scripts to your page that will downgrade performance.',
advice:
score === 0
? 'The page is using Google Tag Manager, this is a performance risk since non-tech users can add JavaScript to your page.'
: '',
score: score,
weight: 5,
severity: 'warn',
offending: [],
tags: ['performance', 'js']
};
})();
} catch(err) {
performanceErrors["googleTagManager"] = err.message;
}
try {
performanceResults["inlineCss"] = (function (util) {
'use strict';
const offending = [];
const cssFilesInHead = util.getCSSFiles(document.head);
const styles = Array.prototype.slice.call(
globalThis.document.head.querySelectorAll('style')
);
let message = '';
let score = 0;
// If we use HTTP/2, do CSS request in head and inline CSS
if (util.isHTTP2() && cssFilesInHead.length > 0 && styles.length > 0) {
score += 5;
message =
'The page has both inline CSS and CSS requests even though it uses a HTTP/2-ish connection. If you have many users on slow connections, it can be better to only inline the CSS. Run your own tests and check the waterfall graph to see what happens.';
} else if (
util.isHTTP2() &&
styles.length > 0 &&
cssFilesInHead.length === 0
) {
// If we got inline styles with HTTP/2
message +=
'The page has inline CSS and uses HTTP/2. Do you have a lot of users with slow connections on the site? It is good to inline CSS when using HTTP/2.';
} else if (util.isHTTP2() && cssFilesInHead.length > 0) {
// we have HTTP/2 and do CSS requests in HEAD.
message +=
'It is always faster for the user if you inline CSS instead of making a CSS request.';
}
if (util.isHTTP3()) {
if (cssFilesInHead.length > 0 && styles.length === 0) {
score += 5;
message =
'The page uses HTTP/3 and loads ' +
util.plural(cssFilesInHead.length, 'CSS request') +
' in head. Inline the critical CSS to avoid render-blocking on the first byte and lazy load the rest.';
offending.push.apply(offending, cssFilesInHead);
} else if (cssFilesInHead.length > 0 && styles.length > 0) {
message =
'The page uses HTTP/3 with both inline CSS and CSS requests in head. That is fine if the inline CSS is the critical path; otherwise drop the external request.';
}
}
// If we have HTTP/1
else if (!util.isHTTP2()) {
// and files served inside of head, inline them instead
if (cssFilesInHead.length > 0 && styles.length === 0) {
score += 10 * cssFilesInHead.length;
message =
'The page loads ' +
util.plural(cssFilesInHead.length, 'CSS request') +
' inside of head, try to inline the CSS for the first render and lazy load the rest.';
offending.push.apply(offending, cssFilesInHead);
}
// If we inline CSS and request CSS files inside of head
if (styles.length > 0 && cssFilesInHead.length > 0) {
score += 10;
message +=
'The page has both inline styles as well as it is requesting ' +
util.plural(cssFilesInHead.length, 'CSS file') +
" inside of the head. Let's only inline CSS for really fast render.";
offending.push.apply(offending, cssFilesInHead);
}
}
return {
id: 'inlineCss',
title: 'Inline CSS for faster first render',
description:
'In the early days of the Internet, inlining CSS was one of the ugliest things you can do. That has changed if you want your page to start rendering fast for your user. Always inline the critical CSS when you use HTTP/1 and HTTP/2 (avoid doing CSS requests that block rendering) and lazy load and cache the rest of the CSS. It is a little more complicated when using HTTP/2. Does your server support HTTP push? Then maybe that can help. Do you have a lot of users on a slow connection and are serving large chunks of HTML? Then it could be better to use the inline technique, becasue some servers always prioritize HTML content over CSS so the user needs to download the HTML first, before the CSS is downloaded.',
advice: message,
score: Math.max(0, 100 - score),
weight: 7,
severity: 'warn',
offending: offending,
tags: ['performance', 'css']
};
})(util);
} catch(err) {
performanceErrors["inlineCss"] = err.message;
}
try {
performanceResults["interactionToNextPaint"] = (function (util) {
'use strict';
// Interaction to Next Paint (INP) is the Core Web Vital that replaced
// First Input Delay in March 2024. It measures the latency of the
// slowest user interaction (click, tap, keypress) on the page — from
// input to the next paint that reflects the response.
//
// Synthetic tests rarely fire interactions, so this rule reports
// whatever it can observe from PerformanceObserver buffered 'event'
// entries. For accurate INP you really want real-user monitoring; the
// advice text says so. The thresholds below match Google's published
// p75 cutoffs (https://web.dev/articles/inp):
// good <= 200 ms
// needs improvement 200–500 ms
// poor > 500 ms
let score = 100;
let advice;
const offending = [];
const good = 200;
const needImprovement = 500;
const supported = PerformanceObserver.supportedEntryTypes;
if (!supported || !supported.includes('event')) {
advice = 'Interaction to Next Paint is not supported in this browser.';
} else {
const observer = new PerformanceObserver(() => {});
// durationThreshold filters out the chatter of trivial events; 40 ms
// is the value web-vitals.js uses by default.
try {
observer.observe({
type: 'event',
buffered: true,
durationThreshold: 40
});
} catch {
observer.observe({ type: 'event', buffered: true });
}
const entries = observer.takeRecords();
if (entries.length === 0) {
advice =