Skip to content

Instantly share code, notes, and snippets.

@brendankenny
Last active January 4, 2024 23:51
Show Gist options
  • Save brendankenny/09539c993a87ab2c2a435f1f9fe99c93 to your computer and use it in GitHub Desktop.
Save brendankenny/09539c993a87ab2c2a435f1f9fe99c93 to your computer and use it in GitHub Desktop.
prioritize
diff --git a/core/audits/prioritize-lcp-image.js b/core/audits/prioritize-lcp-image.js
index ca78681bf..a0b479467 100644
--- a/core/audits/prioritize-lcp-image.js
+++ b/core/audits/prioritize-lcp-image.js
@@ -13,19 +13,25 @@ import {LoadSimulator} from '../computed/load-simulator.js';
import {ByteEfficiencyAudit} from './byte-efficiency/byte-efficiency-audit.js';
import {LCPImageRecord} from '../computed/lcp-image-record.js';
+/* eslint-disable max-len */
const UIStrings = {
- /** Title of a lighthouse audit that tells a user to preload an image in order to improve their LCP time. */
- title: 'Preload Largest Contentful Paint image',
- /** Description of a lighthouse audit that tells a user to preload an image in order to improve their LCP time. */
- description: 'If the LCP element is dynamically added to the page, you should preload the ' +
- 'image in order to improve LCP. [Learn more about preloading LCP elements](https://web.dev/optimize-lcp/#optimize-when-the-resource-is-discovered).',
+ /** Title of a lighthouse audit that tells a user to prioritize an image in order to improve their LCP time. */
+ title: 'Prioritize the Largest Contentful Paint image',
+ /** Description of a lighthouse audit that tells a user to prioritize an image in order to improve their LCP time. */
+ description: 'The LCP image should be discoverable in the initial HTML response and and ' +
+ 'prioritized to load quickly in order to improve LCP. [Learn more about prioritizing LCP elements](https://web.dev/optimize-lcp/#optimize-when-the-resource-is-discovered).',
+ /** The heading of a list of the files loaded by a webpage leading up to the main image shown by the page. "LCP" refers to the Largest Contentful Paint. */
+ multipleSteps: 'The browser had to load multiple resources to discover the page\'s LCP image, including at least:',
+ /** The heading of a column indicating the type of action that initiated (or "caused") a resource to be loaded by a web page. Examples: 'parser', 'script', and 'other'. */
+ initiatorType: 'Initiator Type',
};
+/* eslint-enable max-len */
const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);
/**
* @typedef {LH.Crdp.Network.Initiator['type']|'redirect'|'fallbackToMain'} InitiatorType
- * @typedef {Array<{url: string, initiatorType: InitiatorType}>} InitiatorPath
+ * @typedef {Array<{request: LH.Artifacts.NetworkRequest, initiatorType: InitiatorType}>} InitiatorPath
*/
class PrioritizeLcpImage extends Audit {
@@ -105,7 +111,7 @@ class PrioritizeLcpImage extends Audit {
initiatorType = 'fallbackToMain';
}
- initiatorPath.push({url: request.url, initiatorType});
+ initiatorPath.push({request, initiatorType});
// Can't preload before the main resource, so break off initiator path there.
if (mainResourceReached) break;
@@ -228,6 +234,77 @@ class PrioritizeLcpImage extends Audit {
};
}
+ /**
+ * @param {InitiatorPath} initiatorPath
+ * @return {boolean}
+ */
+ static containsMissingInitiator(initiatorPath) {
+ return initiatorPath.some(item => item.initiatorType === 'fallbackToMain');
+ }
+
+ /**
+ * @param {InitiatorPath} [initiatorPath]
+ * @return {LH.Audit.Details.List}
+ */
+ static constructDetails(initiatorPath) {
+ if (!initiatorPath) {
+ return {type: 'list', items: []};
+ }
+
+ /** @type {LH.Audit.Details.List['items']} */
+ const items = [];
+
+ // Discoverability. Show initiator path if interesting.
+ if (initiatorPath.length > 2) {
+ items.push({
+ type: 'code',
+ value: str_(UIStrings.multipleSteps),
+ });
+
+ /** @type {LH.Audit.Details.CriticalRequestChain['chains']} */
+ const chains = {};
+ let node = chains;
+ for (const {request} of [...initiatorPath].reverse()) {
+ const child = {
+ request: {
+ url: request.url,
+ startTime: request.networkRequestTime,
+ endTime: request.networkEndTime,
+ responseReceivedTime: request.responseHeadersEndTime,
+ transferSize: request.transferSize,
+ },
+ children: {},
+ };
+
+ node[request.requestId] = child;
+ node = child.children;
+ }
+
+ // Create critical-request-chains standin.
+ /** @type {LH.Audit.Details.CriticalRequestChain} */
+ const crcDetails = {
+ type: 'criticalrequestchain',
+ longestChain: {
+ duration: 555,
+ length: initiatorPath.length,
+ transferSize: 666,
+ },
+ chains,
+ };
+ items.push(crcDetails);
+ }
+
+ // Three cases where we recommend inlining/preloading:
+ // - requests in between main document and LCP image
+ // - TODO(bckenny): no initiator information but heuristics indicate not in main document
+ // - a css image (even if inline, not found by preload scanner)
+
+ return {
+ type: 'list',
+ items,
+ };
+ }
+
/**
* @param {LH.Artifacts} artifacts
* @param {LH.Audit.Context} context
@@ -260,22 +337,30 @@ class PrioritizeLcpImage extends Audit {
const {results, wastedMs} =
PrioritizeLcpImage.computeWasteWithGraph(lcpElement, lcpNodeToPreload, graph, simulator);
+ const details = PrioritizeLcpImage.constructDetails(initiatorPath);
+
/** @type {LH.Audit.Details.Opportunity['headings']} */
const headings = [
{key: 'node', valueType: 'node', label: ''},
{key: 'url', valueType: 'url', label: str_(i18n.UIStrings.columnURL)},
{key: 'wastedMs', valueType: 'timespanMs', label: str_(i18n.UIStrings.columnWastedMs)},
];
- const details = Audit.makeOpportunityDetails(headings, results,
- {overallSavingsMs: wastedMs, sortedBy: ['wastedMs']});
+ const tableDetails = Audit.makeTableDetails(headings, results, {sortedBy: ['wastedMs']});
+
+ details.items.push(tableDetails);
+ details.overallSavingsMs = wastedMs;
// If LCP element was an image and had valid network records (regardless of
// if it should be preloaded), it will be found first in the `initiatorPath`.
// Otherwise path and length will be undefined.
if (initiatorPath) {
+ const debugInitiatorPath = initiatorPath.map(item => {
+ return {url: item.request.url, initiatorType: item.initiatorType};
+ });
+
details.debugData = {
type: 'debugdata',
- initiatorPath,
+ initiatorPath: debugInitiatorPath,
pathLength: initiatorPath.length,
};
}
diff --git a/core/test/fixtures/fraggle-rock/reports/sample-flow-result.json b/core/test/fixtures/fraggle-rock/reports/sample-flow-result.json
index 9d0b62713..6cba89dd1 100644
--- a/core/test/fixtures/fraggle-rock/reports/sample-flow-result.json
+++ b/core/test/fixtures/fraggle-rock/reports/sample-flow-result.json
@@ -1884,21 +1884,23 @@
},
"prioritize-lcp-image": {
"id": "prioritize-lcp-image",
- "title": "Preload Largest Contentful Paint image",
- "description": "If the LCP element is dynamically added to the page, you should preload the image in order to improve LCP. [Learn more about preloading LCP elements](https://web.dev/optimize-lcp/#optimize-when-the-resource-is-discovered).",
+ "title": "Prioritize the Largest Contentful Paint image",
+ "description": "The LCP image should be discoverable in the initial HTML response and and prioritized to load quickly in order to improve LCP. [Learn more about prioritizing LCP elements](https://web.dev/optimize-lcp/#optimize-when-the-resource-is-discovered).",
"score": 1,
"scoreDisplayMode": "numeric",
"numericValue": 0,
"numericUnit": "millisecond",
"displayValue": "",
"details": {
- "type": "opportunity",
- "headings": [],
- "items": [],
- "overallSavingsMs": 0,
- "sortedBy": [
- "wastedMs"
+ "type": "list",
+ "items": [
+ {
+ "type": "table",
+ "headings": [],
+ "items": []
+ }
],
+ "overallSavingsMs": 0,
"debugData": {
"type": "debugdata",
"initiatorPath": [
@@ -18422,21 +18424,23 @@
},
"prioritize-lcp-image": {
"id": "prioritize-lcp-image",
- "title": "Preload Largest Contentful Paint image",
- "description": "If the LCP element is dynamically added to the page, you should preload the image in order to improve LCP. [Learn more about preloading LCP elements](https://web.dev/optimize-lcp/#optimize-when-the-resource-is-discovered).",
+ "title": "Prioritize the Largest Contentful Paint image",
+ "description": "The LCP image should be discoverable in the initial HTML response and and prioritized to load quickly in order to improve LCP. [Learn more about prioritizing LCP elements](https://web.dev/optimize-lcp/#optimize-when-the-resource-is-discovered).",
"score": 1,
"scoreDisplayMode": "numeric",
"numericValue": 0,
"numericUnit": "millisecond",
"displayValue": "",
"details": {
- "type": "opportunity",
- "headings": [],
- "items": [],
- "overallSavingsMs": 0,
- "sortedBy": [
- "wastedMs"
+ "type": "list",
+ "items": [
+ {
+ "type": "table",
+ "headings": [],
+ "items": []
+ }
],
+ "overallSavingsMs": 0,
"debugData": {
"type": "debugdata",
"initiatorPath": [
diff --git a/core/test/results/sample_v2.json b/core/test/results/sample_v2.json
index 73efc6dcc..40a8b6f17 100644
--- a/core/test/results/sample_v2.json
+++ b/core/test/results/sample_v2.json
@@ -2702,58 +2702,120 @@
},
"prioritize-lcp-image": {
"id": "prioritize-lcp-image",
- "title": "Preload Largest Contentful Paint image",
- "description": "If the LCP element is dynamically added to the page, you should preload the image in order to improve LCP. [Learn more about preloading LCP elements](https://web.dev/optimize-lcp/#optimize-when-the-resource-is-discovered).",
+ "title": "Prioritize the Largest Contentful Paint image",
+ "description": "The LCP image should be discoverable in the initial HTML response and and prioritized to load quickly in order to improve LCP. [Learn more about prioritizing LCP elements](https://web.dev/optimize-lcp/#optimize-when-the-resource-is-discovered).",
"score": 1,
"scoreDisplayMode": "numeric",
"numericValue": 0,
"numericUnit": "millisecond",
"displayValue": "",
"details": {
- "type": "opportunity",
- "headings": [
+ "type": "list",
+ "items": [
{
- "key": "node",
- "valueType": "node",
- "label": ""
+ "type": "code",
+ "value": "The browser had to load multiple resources to discover the page's LCP image, including at least:"
},
{
- "key": "url",
- "valueType": "url",
- "label": "URL"
+ "type": "criticalrequestchain",
+ "longestChain": {
+ "duration": 555,
+ "length": 4,
+ "transferSize": 666
+ },
+ "chains": {
+ "6913DB840A4B952A23AAEB21C42F130A": {
+ "request": {
+ "url": "http://localhost:10200/dobetterweb/dbw_tester.html",
+ "startTime": 8696703.416000001,
+ "endTime": 8697360.242999999,
+ "responseReceivedTime": 8697272.590000002,
+ "transferSize": 17609
+ },
+ "children": {
+ "31161.7": {
+ "request": {
+ "url": "http://localhost:10200/dobetterweb/dbw_tester.css?delay=2000&async=true",
+ "startTime": 8697311.565,
+ "endTime": 8699362.754,
+ "responseReceivedTime": 8699362.496,
+ "transferSize": 903
+ },
+ "children": {
+ "31161.42": {
+ "request": {
+ "url": "http://localhost:10200/dobetterweb/lighthouse-1024x680.jpg?lcp&redirect=lighthouse-1024x680.jpg%3Fredirected-lcp",
+ "startTime": 8703815.525,
+ "endTime": 8707549.529,
+ "responseReceivedTime": 8706264.225,
+ "transferSize": 184
+ },
+ "children": {
+ "31161.42:redirect": {
+ "request": {
+ "url": "http://localhost:10200/dobetterweb/lighthouse-1024x680.jpg?redirected-lcp",
+ "startTime": 8707549.69,
+ "endTime": 8709600.339,
+ "responseReceivedTime": 8708137.202,
+ "transferSize": 112939
+ },
+ "children": {}
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
},
{
- "key": "wastedMs",
- "valueType": "timespanMs",
- "label": "Potential Savings"
- }
- ],
- "items": [
- {
- "node": {
- "type": "node",
- "lhId": "page-13-H2",
- "path": "3,HTML,1,BODY,9,DIV,0,H2",
- "selector": "body > div > h2#toppy",
- "boundingRect": {
- "top": 336,
- "bottom": 364,
- "left": 8,
- "right": 352,
- "width": 344,
- "height": 28
+ "type": "table",
+ "headings": [
+ {
+ "key": "node",
+ "valueType": "node",
+ "label": ""
},
- "snippet": "<h2 id=\"toppy\" style=\"background-image:url('');\">",
- "nodeLabel": "Do better web tester page"
- },
- "url": "http://localhost:10200/dobetterweb/lighthouse-1024x680.jpg?redirected-lcp",
- "wastedMs": 0
+ {
+ "key": "url",
+ "valueType": "url",
+ "label": "URL"
+ },
+ {
+ "key": "wastedMs",
+ "valueType": "timespanMs",
+ "label": "Potential Savings"
+ }
+ ],
+ "items": [
+ {
+ "node": {
+ "type": "node",
+ "lhId": "page-13-H2",
+ "path": "3,HTML,1,BODY,9,DIV,0,H2",
+ "selector": "body > div > h2#toppy",
+ "boundingRect": {
+ "top": 336,
+ "bottom": 364,
+ "left": 8,
+ "right": 352,
+ "width": 344,
+ "height": 28
+ },
+ "snippet": "<h2 id=\"toppy\" style=\"background-image:url('');\">",
+ "nodeLabel": "Do better web tester page"
+ },
+ "url": "http://localhost:10200/dobetterweb/lighthouse-1024x680.jpg?redirected-lcp",
+ "wastedMs": 0
+ }
+ ],
+ "sortedBy": [
+ "wastedMs"
+ ]
}
],
"overallSavingsMs": 0,
- "sortedBy": [
- "wastedMs"
- ],
"debugData": {
"type": "debugdata",
"initiatorPath": [
@@ -9040,7 +9102,7 @@
"audits[network-server-latency].details.headings[0].label",
"audits[long-tasks].details.headings[0].label",
"audits[unsized-images].details.headings[1].label",
- "audits[prioritize-lcp-image].details.headings[1].label",
+ "audits[prioritize-lcp-image].details.items[2].headings[1].label",
"audits[uses-long-cache-ttl].details.headings[0].label",
"audits[total-byte-weight].details.headings[0].label",
"audits[render-blocking-resources].details.headings[0].label",
@@ -9520,8 +9582,11 @@
"core/audits/prioritize-lcp-image.js | description": [
"audits[prioritize-lcp-image].description"
],
+ "core/audits/prioritize-lcp-image.js | multipleSteps": [
+ "audits[prioritize-lcp-image].details.items[0].value"
+ ],
"core/lib/i18n/i18n.js | columnWastedBytes": [
- "audits[prioritize-lcp-image].details.headings[2].label",
+ "audits[prioritize-lcp-image].details.items[2].headings[2].label",
"audits[render-blocking-resources].details.headings[2].label",
"audits[unminified-javascript].details.headings[2].label",
"audits[unused-javascript].details.headings[2].label",
diff --git a/package.json b/package.json
index 0f52d1ca9..3a518f3ba 100644
--- a/package.json
+++ b/package.json
@@ -78,7 +78,7 @@
"i18n:collect-strings": "node core/scripts/i18n/collect-strings.js",
"update:lantern-baseline": "node core/scripts/lantern/update-baseline-lantern-values.js",
"update:sample-artifacts": "node core/scripts/update-report-fixtures.js",
- "update:sample-json": "yarn i18n:collect-strings && node ./cli -A=./core/test/results/artifacts --config-path=./core/test/results/sample-config.js --output=json --output-path=./core/test/results/sample_v2.json && node core/scripts/cleanup-LHR-for-diff.js ./core/test/results/sample_v2.json --only-remove-timing && node ./core/scripts/update-flow-fixtures.js",
+ "update:sample-json": "node ./cli -A=./core/test/results/artifacts --config-path=./core/test/results/sample-config.js --output=json --output-path=./core/test/results/sample_v2.json && node core/scripts/cleanup-LHR-for-diff.js ./core/test/results/sample_v2.json --only-remove-timing",
"update:flow-sample-json": "yarn i18n:collect-strings && node ./core/scripts/update-flow-fixtures.js",
"test-devtools": "bash core/test/devtools-tests/test-locally.sh",
"open-devtools": "bash core/scripts/open-devtools.sh",
diff --git a/report/renderer/details-renderer.js b/report/renderer/details-renderer.js
index 577e21910..a620eb3d2 100644
--- a/report/renderer/details-renderer.js
+++ b/report/renderer/details-renderer.js
@@ -553,7 +553,15 @@ export class DetailsRenderer {
const listContainer = this._dom.createElement('div', 'lh-list');
details.items.forEach(item => {
- const listItem = this.render(item);
+ let listItem;
+ if (item.type === 'code') {
+ listItem = this._renderCode(item.value);
+ } else if (item.type === 'url') {
+ listItem = this.renderTextURL(item.value);
+ } else {
+ listItem = this.render(item);
+ }
+
if (!listItem) return;
listContainer.append(listItem);
});
diff --git a/shared/localization/locales/en-US.json b/shared/localization/locales/en-US.json
index f6ee0981d..ec57e0916 100644
--- a/shared/localization/locales/en-US.json
+++ b/shared/localization/locales/en-US.json
@@ -1203,10 +1203,13 @@
"message": "Fonts with `font-display: optional` are preloaded"
},
"core/audits/prioritize-lcp-image.js | description": {
- "message": "If the LCP element is dynamically added to the page, you should preload the image in order to improve LCP. [Learn more about preloading LCP elements](https://web.dev/optimize-lcp/#optimize-when-the-resource-is-discovered)."
+ "message": "The LCP image should be discoverable in the initial HTML response and and prioritized to load quickly in order to improve LCP. [Learn more about prioritizing LCP elements](https://web.dev/optimize-lcp/#optimize-when-the-resource-is-discovered)."
+ },
+ "core/audits/prioritize-lcp-image.js | multipleSteps": {
+ "message": "The browser had to load multiple resources to discover the page's LCP image, including at least:"
},
"core/audits/prioritize-lcp-image.js | title": {
- "message": "Preload Largest Contentful Paint image"
+ "message": "Prioritize the Largest Contentful Paint image"
},
"core/audits/redirects.js | description": {
"message": "Redirects introduce additional delays before the page can be loaded. [Learn how to avoid page redirects](https://developer.chrome.com/docs/lighthouse/performance/redirects/)."
diff --git a/shared/localization/locales/en-XL.json b/shared/localization/locales/en-XL.json
index bafb88435..1d0ebd928 100644
--- a/shared/localization/locales/en-XL.json
+++ b/shared/localization/locales/en-XL.json
@@ -1203,10 +1203,13 @@
"message": "F̂ón̂t́ŝ ẃît́ĥ `font-display: optional` ár̂é p̂ŕêĺôád̂éd̂"
},
"core/audits/prioritize-lcp-image.js | description": {
- "message": "Îf́ t̂h́ê ĹĈṔ êĺêḿêńt̂ íŝ d́ŷńâḿîćâĺl̂ý âd́d̂éd̂ t́ô t́ĥé p̂áĝé, ŷóû śĥóûĺd̂ ṕr̂él̂óâd́ t̂h́ê ím̂áĝé îń ôŕd̂ér̂ t́ô ím̂ṕr̂óv̂é L̂ĆP̂. [Ĺêár̂ń m̂ór̂é âb́ôút̂ ṕr̂él̂óâd́îńĝ ĹĈṔ êĺêḿêńt̂ś](https://web.dev/optimize-lcp/#optimize-when-the-resource-is-discovered)."
+ "message": "T̂h́ê ĹĈṔ îḿâǵê śĥóûĺd̂ b́ê d́îśĉóv̂ér̂áb̂ĺê ín̂ t́ĥé îńît́îál̂ H́T̂ḾL̂ ŕêśp̂ón̂śê án̂d́ âńd̂ ṕr̂íôŕît́îźêd́ t̂ó l̂óâd́ q̂úîćk̂ĺŷ ín̂ ór̂d́êŕ t̂ó îḿp̂ŕôv́ê ĹĈṔ. [L̂éâŕn̂ ḿôŕê áb̂óût́ p̂ŕîór̂ít̂íẑín̂ǵ L̂ĆP̂ él̂ém̂én̂t́ŝ](https://web.dev/optimize-lcp/#optimize-when-the-resource-is-discovered)."
+ },
+ "core/audits/prioritize-lcp-image.js | multipleSteps": {
+ "message": "T̂h́ê b́r̂óŵśêŕ ĥád̂ t́ô ĺôád̂ ḿûĺt̂íp̂ĺê ŕêśôúr̂ćêś t̂ó d̂íŝćôv́êŕ t̂h́ê ṕâǵê'ś L̂ĆP̂ ím̂áĝé, îńĉĺûd́îńĝ át̂ ĺêáŝt́:"
},
"core/audits/prioritize-lcp-image.js | title": {
- "message": "P̂ŕêĺôád̂ Ĺâŕĝéŝt́ Ĉón̂t́êńt̂f́ûĺ P̂áîńt̂ ím̂áĝé"
+ "message": "P̂ŕîór̂ít̂íẑé t̂h́ê Ĺâŕĝéŝt́ Ĉón̂t́êńt̂f́ûĺ P̂áîńt̂ ím̂áĝé"
},
"core/audits/redirects.js | description": {
"message": "R̂éd̂ír̂éĉt́ŝ ín̂t́r̂ód̂úĉé âd́d̂ít̂íôńâĺ d̂él̂áŷś b̂éf̂ór̂é t̂h́ê ṕâǵê ćâń b̂é l̂óâd́êd́. [L̂éâŕn̂ h́ôẃ t̂ó âv́ôíd̂ ṕâǵê ŕêd́îŕêćt̂ś](https://developer.chrome.com/docs/lighthouse/performance/redirects/)."
diff --git a/types/lhr/audit-details.d.ts b/types/lhr/audit-details.d.ts
index 78634a97b..1fb2db117 100644
--- a/types/lhr/audit-details.d.ts
+++ b/types/lhr/audit-details.d.ts
@@ -69,7 +69,7 @@ declare module Details {
type: 'list';
// NOTE: any `Details` type *should* be usable in `items`, but check
// styles/report-ui-features are good before adding.
- items: Array<Table | DebugData>;
+ items: Array<Table | DebugData | CodeValue | UrlValue | CriticalRequestChain>;
}
interface Opportunity extends BaseDetails {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment