diff --git a/core/audits/audit.js b/core/audits/audit.js index 3eed70c44a61..51c93e815441 100644 --- a/core/audits/audit.js +++ b/core/audits/audit.js @@ -478,6 +478,7 @@ class Audit { details: product.details, guidanceLevel: audit.meta.guidanceLevel, + replacesAudits: audit.meta.replacesAudits, }; } diff --git a/core/audits/insights/cls-culprits-insight.js b/core/audits/insights/cls-culprits-insight.js index 71f7835c87ab..bb08bb37bee6 100644 --- a/core/audits/insights/cls-culprits-insight.js +++ b/core/audits/insights/cls-culprits-insight.js @@ -27,6 +27,7 @@ class CLSCulpritsInsight extends Audit { description: str_(UIStrings.description), guidanceLevel: 3, requiredArtifacts: ['traces', 'TraceElements'], + replacesAudits: ['layout-shifts', 'non-composited-animations', 'unsized-images'], }; } diff --git a/core/audits/insights/document-latency-insight.js b/core/audits/insights/document-latency-insight.js index 4d2f7d8209f6..281bbaa45a0d 100644 --- a/core/audits/insights/document-latency-insight.js +++ b/core/audits/insights/document-latency-insight.js @@ -25,6 +25,7 @@ class DocumentLatencyInsight extends Audit { description: str_(UIStrings.description), guidanceLevel: 3, requiredArtifacts: ['traces', 'TraceElements'], + replacesAudits: ['redirects', 'server-response-time', 'uses-text-compression'], }; } diff --git a/core/audits/insights/dom-size-insight.js b/core/audits/insights/dom-size-insight.js index adc1cf669dfc..4c16cdd72575 100644 --- a/core/audits/insights/dom-size-insight.js +++ b/core/audits/insights/dom-size-insight.js @@ -25,6 +25,7 @@ class DOMSizeInsight extends Audit { description: str_(UIStrings.description), guidanceLevel: 3, requiredArtifacts: ['traces', 'TraceElements'], + replacesAudits: ['dom-size'], }; } diff --git a/core/audits/insights/font-display-insight.js b/core/audits/insights/font-display-insight.js index c796ef774305..0085d090dd48 100644 --- a/core/audits/insights/font-display-insight.js +++ b/core/audits/insights/font-display-insight.js @@ -27,6 +27,7 @@ class FontDisplayInsight extends Audit { description: str_(UIStrings.description), guidanceLevel: 3, requiredArtifacts: ['traces', 'TraceElements'], + replacesAudits: ['font-display'], }; } diff --git a/core/audits/insights/image-delivery-insight.js b/core/audits/insights/image-delivery-insight.js index e62c9e16397d..5b057dd94dd9 100644 --- a/core/audits/insights/image-delivery-insight.js +++ b/core/audits/insights/image-delivery-insight.js @@ -25,6 +25,12 @@ class ImageDeliveryInsight extends Audit { description: str_(UIStrings.description), guidanceLevel: 3, requiredArtifacts: ['traces', 'TraceElements'], + replacesAudits: [ + 'modern-image-formats', + 'uses-optimized-images', + 'efficient-animated-content', + 'uses-responsive-images', + ], }; } diff --git a/core/audits/insights/interaction-to-next-paint-insight.js b/core/audits/insights/interaction-to-next-paint-insight.js index ac379604f18f..0ad27c224876 100644 --- a/core/audits/insights/interaction-to-next-paint-insight.js +++ b/core/audits/insights/interaction-to-next-paint-insight.js @@ -25,6 +25,7 @@ class InteractionToNextPaintInsight extends Audit { description: str_(UIStrings.description), guidanceLevel: 3, requiredArtifacts: ['traces', 'TraceElements'], + replacesAudits: ['work-during-interaction'], }; } diff --git a/core/audits/insights/lcp-discovery-insight.js b/core/audits/insights/lcp-discovery-insight.js index a161d41e508d..b8e97a680843 100644 --- a/core/audits/insights/lcp-discovery-insight.js +++ b/core/audits/insights/lcp-discovery-insight.js @@ -25,6 +25,7 @@ class LCPDiscoveryInsight extends Audit { description: str_(UIStrings.description), guidanceLevel: 3, requiredArtifacts: ['traces', 'TraceElements'], + replacesAudits: ['prioritize-lcp-image', 'lcp-lazy-loaded'], }; } diff --git a/core/audits/insights/lcp-phases-insight.js b/core/audits/insights/lcp-phases-insight.js index 062c8e3e2ac2..737270712f04 100644 --- a/core/audits/insights/lcp-phases-insight.js +++ b/core/audits/insights/lcp-phases-insight.js @@ -25,6 +25,7 @@ class LCPPhasesInsight extends Audit { description: str_(UIStrings.description), guidanceLevel: 3, requiredArtifacts: ['traces', 'TraceElements'], + replacesAudits: ['largest-contentful-paint-element'], }; } diff --git a/core/audits/insights/long-critical-network-tree-insight.js b/core/audits/insights/long-critical-network-tree-insight.js index f21de1398845..dacef83cf0ae 100644 --- a/core/audits/insights/long-critical-network-tree-insight.js +++ b/core/audits/insights/long-critical-network-tree-insight.js @@ -27,6 +27,7 @@ class LongCriticalNetworkTreeInsight extends Audit { description: str_(UIStrings.description), guidanceLevel: 3, requiredArtifacts: ['traces', 'TraceElements'], + replacesAudits: ['critical-request-chains'], }; } diff --git a/core/audits/insights/render-blocking-insight.js b/core/audits/insights/render-blocking-insight.js index 692b071fcc07..2176e6f178a3 100644 --- a/core/audits/insights/render-blocking-insight.js +++ b/core/audits/insights/render-blocking-insight.js @@ -25,6 +25,7 @@ class RenderBlockingInsight extends Audit { description: str_(UIStrings.description), guidanceLevel: 3, requiredArtifacts: ['traces', 'TraceElements'], + replacesAudits: ['render-blocking-resources'], }; } diff --git a/core/audits/insights/third-parties-insight.js b/core/audits/insights/third-parties-insight.js index 8a7ca42f17c0..475f316a7bae 100644 --- a/core/audits/insights/third-parties-insight.js +++ b/core/audits/insights/third-parties-insight.js @@ -32,6 +32,7 @@ class ThirdPartiesInsight extends Audit { description: str_(UIStrings.description), guidanceLevel: 3, requiredArtifacts: ['traces', 'TraceElements'], + replacesAudits: ['third-party-summary'], }; } diff --git a/core/audits/insights/viewport-insight.js b/core/audits/insights/viewport-insight.js index 2acdc05f40c2..514e4ad6c1cb 100644 --- a/core/audits/insights/viewport-insight.js +++ b/core/audits/insights/viewport-insight.js @@ -25,6 +25,7 @@ class ViewportInsight extends Audit { description: str_(UIStrings.description), guidanceLevel: 3, requiredArtifacts: ['traces', 'TraceElements'], + replacesAudits: ['viewport'], }; } diff --git a/core/test/fixtures/user-flows/reports/sample-flow-result.json b/core/test/fixtures/user-flows/reports/sample-flow-result.json index 3d36def9b32f..f2b2b5925358 100644 --- a/core/test/fixtures/user-flows/reports/sample-flow-result.json +++ b/core/test/fixtures/user-flows/reports/sample-flow-result.json @@ -3843,7 +3843,12 @@ "description": "Layout shifts occur when elements move absent any user interaction. [Investigate the causes of layout shifts](https://web.dev/articles/optimize-cls), such as elements being added, removed, or their fonts changing as the page loads.", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "layout-shifts", + "non-composited-animations", + "unsized-images" + ] }, "document-latency-insight": { "id": "document-latency-insight", @@ -3872,7 +3877,12 @@ } } }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "redirects", + "server-response-time", + "uses-text-compression" + ] }, "dom-size-insight": { "id": "dom-size-insight", @@ -3929,7 +3939,10 @@ } ] }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "dom-size" + ] }, "font-display-insight": { "id": "font-display-insight", @@ -3937,7 +3950,10 @@ "description": "Consider setting [font-display](https://developer.chrome.com/blog/font-display) to swap or optional to ensure text is consistently visible. swap can be further optimized to mitigate layout shifts with [font metric overrides](https://developer.chrome.com/blog/font-fallbacks).", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "font-display" + ] }, "forced-reflow-insight": { "id": "forced-reflow-insight", @@ -3953,7 +3969,13 @@ "description": "Reducing the download time of images can improve the perceived load time of the page and LCP. [Learn more about optimizing image size](https://developer.chrome.com/docs/lighthouse/performance/uses-optimized-images/)", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "modern-image-formats", + "uses-optimized-images", + "efficient-animated-content", + "uses-responsive-images" + ] }, "interaction-to-next-paint-insight": { "id": "interaction-to-next-paint-insight", @@ -3961,7 +3983,10 @@ "description": "Start investigating with the longest phase. [Delays can be minimized](https://web.dev/articles/optimize-inp#optimize_interactions). To reduce processing duration, [optimize the main-thread costs](https://web.dev/articles/optimize-long-tasks), often JS.", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "work-during-interaction" + ] }, "lcp-discovery-insight": { "id": "lcp-discovery-insight", @@ -3989,7 +4014,11 @@ } } }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "prioritize-lcp-image", + "lcp-lazy-loaded" + ] }, "lcp-phases-insight": { "id": "lcp-phases-insight", @@ -4042,7 +4071,10 @@ } ] }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "largest-contentful-paint-element" + ] }, "long-critical-network-tree-insight": { "id": "long-critical-network-tree-insight", @@ -4050,7 +4082,10 @@ "description": "[Avoid chaining critical requests](https://developer.chrome.com/docs/lighthouse/performance/critical-request-chains) by reducing the length of chains, reducing the download size of resources, or deferring the download of unnecessary resources to improve page load.", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "critical-request-chains" + ] }, "render-blocking-insight": { "id": "render-blocking-insight", @@ -4058,7 +4093,10 @@ "description": "Requests are blocking the page's initial render, which may delay LCP. [Deferring or inlining](https://web.dev/learn/performance/understanding-the-critical-path#render-blocking_resources/) can move these network requests out of the critical path.", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "render-blocking-resources" + ] }, "slow-css-selector-insight": { "id": "slow-css-selector-insight", @@ -4169,7 +4207,10 @@ ], "isEntityGrouped": true }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "third-party-summary" + ] }, "viewport-insight": { "id": "viewport-insight", @@ -4193,7 +4234,10 @@ {} ] }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "viewport" + ] } }, "configSettings": { @@ -11235,7 +11279,12 @@ "description": "Layout shifts occur when elements move absent any user interaction. [Investigate the causes of layout shifts](https://web.dev/articles/optimize-cls), such as elements being added, removed, or their fonts changing as the page loads.", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "layout-shifts", + "non-composited-animations", + "unsized-images" + ] }, "document-latency-insight": { "id": "document-latency-insight", @@ -11243,7 +11292,12 @@ "description": "Your first network request is the most important. Reduce its latency by avoiding redirects, ensuring a fast server response, and enabling text compression.", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "redirects", + "server-response-time", + "uses-text-compression" + ] }, "dom-size-insight": { "id": "dom-size-insight", @@ -11300,7 +11354,10 @@ } ] }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "dom-size" + ] }, "font-display-insight": { "id": "font-display-insight", @@ -11308,7 +11365,10 @@ "description": "Consider setting [font-display](https://developer.chrome.com/blog/font-display) to swap or optional to ensure text is consistently visible. swap can be further optimized to mitigate layout shifts with [font metric overrides](https://developer.chrome.com/blog/font-fallbacks).", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "font-display" + ] }, "forced-reflow-insight": { "id": "forced-reflow-insight", @@ -11324,7 +11384,13 @@ "description": "Reducing the download time of images can improve the perceived load time of the page and LCP. [Learn more about optimizing image size](https://developer.chrome.com/docs/lighthouse/performance/uses-optimized-images/)", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "modern-image-formats", + "uses-optimized-images", + "efficient-animated-content", + "uses-responsive-images" + ] }, "interaction-to-next-paint-insight": { "id": "interaction-to-next-paint-insight", @@ -11372,7 +11438,10 @@ } ] }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "work-during-interaction" + ] }, "lcp-discovery-insight": { "id": "lcp-discovery-insight", @@ -11380,7 +11449,11 @@ "description": "Optimize LCP by making the LCP image [discoverable](https://web.dev/articles/optimize-lcp#1_eliminate_resource_load_delay) from the HTML immediately, and [avoiding lazy-loading](https://web.dev/articles/lcp-lazy-loading)", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "prioritize-lcp-image", + "lcp-lazy-loaded" + ] }, "lcp-phases-insight": { "id": "lcp-phases-insight", @@ -11388,7 +11461,10 @@ "description": "Each [phase has specific improvement strategies](https://web.dev/articles/optimize-lcp#lcp-breakdown). Ideally, most of the LCP time should be spent on loading the resources, not within delays.", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "largest-contentful-paint-element" + ] }, "long-critical-network-tree-insight": { "id": "long-critical-network-tree-insight", @@ -11396,7 +11472,10 @@ "description": "[Avoid chaining critical requests](https://developer.chrome.com/docs/lighthouse/performance/critical-request-chains) by reducing the length of chains, reducing the download size of resources, or deferring the download of unnecessary resources to improve page load.", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "critical-request-chains" + ] }, "render-blocking-insight": { "id": "render-blocking-insight", @@ -11404,7 +11483,10 @@ "description": "Requests are blocking the page's initial render, which may delay LCP. [Deferring or inlining](https://web.dev/learn/performance/understanding-the-critical-path#render-blocking_resources/) can move these network requests out of the critical path.", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "render-blocking-resources" + ] }, "slow-css-selector-insight": { "id": "slow-css-selector-insight", @@ -11470,7 +11552,10 @@ ], "isEntityGrouped": true }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "third-party-summary" + ] }, "viewport-insight": { "id": "viewport-insight", @@ -11494,7 +11579,10 @@ {} ] }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "viewport" + ] } }, "configSettings": { @@ -22889,7 +22977,12 @@ "description": "Layout shifts occur when elements move absent any user interaction. [Investigate the causes of layout shifts](https://web.dev/articles/optimize-cls), such as elements being added, removed, or their fonts changing as the page loads.", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "layout-shifts", + "non-composited-animations", + "unsized-images" + ] }, "document-latency-insight": { "id": "document-latency-insight", @@ -22918,7 +23011,12 @@ } } }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "redirects", + "server-response-time", + "uses-text-compression" + ] }, "dom-size-insight": { "id": "dom-size-insight", @@ -22975,7 +23073,10 @@ } ] }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "dom-size" + ] }, "font-display-insight": { "id": "font-display-insight", @@ -22983,7 +23084,10 @@ "description": "Consider setting [font-display](https://developer.chrome.com/blog/font-display) to swap or optional to ensure text is consistently visible. swap can be further optimized to mitigate layout shifts with [font metric overrides](https://developer.chrome.com/blog/font-fallbacks).", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "font-display" + ] }, "forced-reflow-insight": { "id": "forced-reflow-insight", @@ -23051,7 +23155,13 @@ } ] }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "modern-image-formats", + "uses-optimized-images", + "efficient-animated-content", + "uses-responsive-images" + ] }, "interaction-to-next-paint-insight": { "id": "interaction-to-next-paint-insight", @@ -23059,7 +23169,10 @@ "description": "Start investigating with the longest phase. [Delays can be minimized](https://web.dev/articles/optimize-inp#optimize_interactions). To reduce processing duration, [optimize the main-thread costs](https://web.dev/articles/optimize-long-tasks), often JS.", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "work-during-interaction" + ] }, "lcp-discovery-insight": { "id": "lcp-discovery-insight", @@ -23087,7 +23200,11 @@ } } }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "prioritize-lcp-image", + "lcp-lazy-loaded" + ] }, "lcp-phases-insight": { "id": "lcp-phases-insight", @@ -23140,7 +23257,10 @@ } ] }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "largest-contentful-paint-element" + ] }, "long-critical-network-tree-insight": { "id": "long-critical-network-tree-insight", @@ -23148,7 +23268,10 @@ "description": "[Avoid chaining critical requests](https://developer.chrome.com/docs/lighthouse/performance/critical-request-chains) by reducing the length of chains, reducing the download size of resources, or deferring the download of unnecessary resources to improve page load.", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "critical-request-chains" + ] }, "render-blocking-insight": { "id": "render-blocking-insight", @@ -23156,7 +23279,10 @@ "description": "Requests are blocking the page's initial render, which may delay LCP. [Deferring or inlining](https://web.dev/learn/performance/understanding-the-critical-path#render-blocking_resources/) can move these network requests out of the critical path.", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "render-blocking-resources" + ] }, "slow-css-selector-insight": { "id": "slow-css-selector-insight", @@ -23262,7 +23388,10 @@ ], "isEntityGrouped": true }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "third-party-summary" + ] }, "viewport-insight": { "id": "viewport-insight", @@ -23286,7 +23415,10 @@ {} ] }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "viewport" + ] } }, "configSettings": { diff --git a/core/test/results/sample_v2.json b/core/test/results/sample_v2.json index 370e7cbeb163..fdb11501442a 100644 --- a/core/test/results/sample_v2.json +++ b/core/test/results/sample_v2.json @@ -5814,7 +5814,12 @@ "description": "Layout shifts occur when elements move absent any user interaction. [Investigate the causes of layout shifts](https://web.dev/articles/optimize-cls), such as elements being added, removed, or their fonts changing as the page loads.", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "layout-shifts", + "non-composited-animations", + "unsized-images" + ] }, "document-latency-insight": { "id": "document-latency-insight", @@ -5843,7 +5848,12 @@ } } }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "redirects", + "server-response-time", + "uses-text-compression" + ] }, "dom-size-insight": { "id": "dom-size-insight", @@ -5900,7 +5910,10 @@ } ] }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "dom-size" + ] }, "font-display-insight": { "id": "font-display-insight", @@ -5908,7 +5921,10 @@ "description": "Consider setting [font-display](https://developer.chrome.com/blog/font-display) to swap or optional to ensure text is consistently visible. swap can be further optimized to mitigate layout shifts with [font metric overrides](https://developer.chrome.com/blog/font-fallbacks).", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "font-display" + ] }, "forced-reflow-insight": { "id": "forced-reflow-insight", @@ -6042,7 +6058,13 @@ } ] }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "modern-image-formats", + "uses-optimized-images", + "efficient-animated-content", + "uses-responsive-images" + ] }, "interaction-to-next-paint-insight": { "id": "interaction-to-next-paint-insight", @@ -6050,7 +6072,10 @@ "description": "Start investigating with the longest phase. [Delays can be minimized](https://web.dev/articles/optimize-inp#optimize_interactions). To reduce processing duration, [optimize the main-thread costs](https://web.dev/articles/optimize-long-tasks), often JS.", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "work-during-interaction" + ] }, "lcp-discovery-insight": { "id": "lcp-discovery-insight", @@ -6078,7 +6103,11 @@ } } }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "prioritize-lcp-image", + "lcp-lazy-loaded" + ] }, "lcp-phases-insight": { "id": "lcp-phases-insight", @@ -6131,7 +6160,10 @@ } ] }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "largest-contentful-paint-element" + ] }, "long-critical-network-tree-insight": { "id": "long-critical-network-tree-insight", @@ -6139,7 +6171,10 @@ "description": "[Avoid chaining critical requests](https://developer.chrome.com/docs/lighthouse/performance/critical-request-chains) by reducing the length of chains, reducing the download size of resources, or deferring the download of unnecessary resources to improve page load.", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "critical-request-chains" + ] }, "render-blocking-insight": { "id": "render-blocking-insight", @@ -6203,7 +6238,10 @@ } ] }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "render-blocking-resources" + ] }, "slow-css-selector-insight": { "id": "slow-css-selector-insight", @@ -6219,7 +6257,10 @@ "description": "Third party code can significantly impact load performance. [Reduce and defer loading of third party code](https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/loading-third-party-javascript/) to prioritize your page's content.", "score": null, "scoreDisplayMode": "notApplicable", - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "third-party-summary" + ] }, "viewport-insight": { "id": "viewport-insight", @@ -6243,7 +6284,10 @@ {} ] }, - "guidanceLevel": 3 + "guidanceLevel": 3, + "replacesAudits": [ + "viewport" + ] } }, "configSettings": { diff --git a/proto/lighthouse-result.proto b/proto/lighthouse-result.proto index 457802756a9a..1e0cfcb694ce 100644 --- a/proto/lighthouse-result.proto +++ b/proto/lighthouse-result.proto @@ -445,6 +445,9 @@ message AuditResult { // An audit's guidance level. google.protobuf.DoubleValue guidanceLevel = 16; + + // Audits that this audit replaces. + google.protobuf.Value replacesAudits = 17; } // Message containing the i18n data for the LHR - Version 1 diff --git a/report/renderer/dom.js b/report/renderer/dom.js index 246724332fbb..8d7c15a329ce 100644 --- a/report/renderer/dom.js +++ b/report/renderer/dom.js @@ -28,6 +28,8 @@ export class DOM { /** @type {HTMLElement} */ // For legacy Report API users, this'll be undefined, but set in renderReport this.rootEl = rootEl; + /** @type {WeakMap} */ + this._swappableSections = new WeakMap(); } /** @@ -276,7 +278,7 @@ export class DOM { * @param {ParentNode} context * @return {ParseSelector | null} */ - maybeFind(query, context) { + maybeFind(query, context = this.rootEl ?? this._document) { const result = context.querySelector(query); // Because we control the report layout and templates, use the simpler @@ -323,4 +325,27 @@ export class DOM { this._document.body.removeChild(a); setTimeout(() => URL.revokeObjectURL(a.href), 500); } + + /** + * @param {Element} section1 + * @param {Element} section2 + */ + registerSwappableSections(section1, section2) { + this._swappableSections.set(section1, section2); + this._swappableSections.set(section2, section1); + } + + /** + * @param {Element} section + */ + swapSectionIfPossible(section) { + const newSection = this._swappableSections.get(section); + if (!newSection) return; + + const parent = section.parentNode; + if (!parent) return; + + parent.insertBefore(newSection, section); + section.remove(); + } } diff --git a/report/renderer/performance-category-renderer.js b/report/renderer/performance-category-renderer.js index 7b596dace5fa..3989d2802874 100644 --- a/report/renderer/performance-category-renderer.js +++ b/report/renderer/performance-category-renderer.js @@ -202,15 +202,24 @@ export class PerformanceCategoryRenderer extends CategoryRenderer { } const legacyAuditsSection = - this.renderFilterableSection(category, groups, 'diagnostics', metricAudits); - legacyAuditsSection?.classList.add('lh-perf-audits--legacy'); + this.renderFilterableSection(category, groups, ['diagnostics'], metricAudits); + legacyAuditsSection?.classList.add('lh-perf-audits--swappable', 'lh-perf-audits--legacy'); const experimentalInsightsSection = - this.renderFilterableSection(category, groups, 'insights', metricAudits); - experimentalInsightsSection?.classList.add('lh-perf-audits--experimental', 'lh-hidden'); - - if (legacyAuditsSection) element.append(legacyAuditsSection); - if (experimentalInsightsSection) element.append(experimentalInsightsSection); + this.renderFilterableSection(category, groups, ['insights', 'diagnostics'], metricAudits); + experimentalInsightsSection?.classList.add( + 'lh-perf-audits--swappable', 'lh-perf-audits--experimental'); + + if (legacyAuditsSection) { + element.append(legacyAuditsSection); + + // Many tests expect just one of these sections to be in the DOM at a given time. + // To prevent the hidden section from tripping up these tests, we will just remove the hidden + // section from the DOM and store it in memory. + if (experimentalInsightsSection) { + this.dom.registerSwappableSections(legacyAuditsSection, experimentalInsightsSection); + } + } const isNavigationMode = !options || options?.gatherMode === 'navigation'; if (isNavigationMode && category.score !== null) { @@ -225,18 +234,29 @@ export class PerformanceCategoryRenderer extends CategoryRenderer { /** * @param {LH.ReportResult.Category} category * @param {Object} groups - * @param {string} groupName + * @param {string[]} groupNames * @param {LH.ReportResult.AuditRef[]} metricAudits * @return {Element|null} */ - renderFilterableSection(category, groups, groupName, metricAudits) { - if (!groups[groupName]) return null; + renderFilterableSection(category, groups, groupNames, metricAudits) { + if (groupNames.some(groupName => !groups[groupName])) return null; const element = this.dom.createElement('div'); + /** @type {Set} */ + const replacedAuditIds = new Set(); + + const allGroupAudits = + category.auditRefs.filter(audit => audit.group && groupNames.includes(audit.group)); + for (const auditRef of allGroupAudits) { + auditRef.result.replacesAudits?.forEach(replacedAuditId => { + replacedAuditIds.add(replacedAuditId); + }); + } + // Diagnostics - const allDiagnostics = category.auditRefs - .filter(audit => audit.group === groupName) + const allFilterableAudits = allGroupAudits + .filter(audit => !replacedAuditIds.has(audit.id)) .map(auditRef => { const {overallImpact, overallLinearImpact} = this.overallImpact(auditRef, metricAudits); const guidanceLevel = auditRef.result.guidanceLevel || 1; @@ -245,20 +265,25 @@ export class PerformanceCategoryRenderer extends CategoryRenderer { return {auditRef, auditEl, overallImpact, overallLinearImpact, guidanceLevel}; }); - const diagnosticAudits = allDiagnostics + const filterableAudits = allFilterableAudits .filter(audit => !ReportUtils.showAsPassed(audit.auditRef.result)); - const passedAudits = allDiagnostics + const passedAudits = allFilterableAudits .filter(audit => ReportUtils.showAsPassed(audit.auditRef.result)); - const [diagnosticsGroupEl, diagnosticsFooterEl] = this.renderAuditGroup(groups[groupName]); - diagnosticsGroupEl.classList.add(`lh-audit-group--${groupName}`); + /** @type {Record} */ + const groupElsMap = {}; + for (const groupName of groupNames) { + const groupEls = this.renderAuditGroup(groups[groupName]); + groupEls[0].classList.add(`lh-audit-group--${groupName}`); + groupElsMap[groupName] = groupEls; + } /** * @param {string} acronym */ function refreshFilteredAudits(acronym) { - for (const audit of allDiagnostics) { + for (const audit of allFilterableAudits) { if (acronym === 'All') { audit.auditEl.hidden = false; } else { @@ -267,7 +292,7 @@ export class PerformanceCategoryRenderer extends CategoryRenderer { } } - diagnosticAudits.sort((a, b) => { + filterableAudits.sort((a, b) => { // Performance diagnostics should only have score display modes of "informative" and "metricSavings" // If the score display mode is "metricSavings", the `score` will be a coarse approximation of the overall impact. // Therefore, it makes sense to sort audits by score first to ensure visual clarity with the score icons. @@ -299,14 +324,20 @@ export class PerformanceCategoryRenderer extends CategoryRenderer { return b.guidanceLevel - a.guidanceLevel; }); - for (const audit of diagnosticAudits) { - diagnosticsGroupEl.insertBefore(audit.auditEl, diagnosticsFooterEl); + for (const audit of filterableAudits) { + if (!audit.auditRef.group) continue; + + const groupEls = groupElsMap[audit.auditRef.group]; + if (!groupEls) continue; + + const [groupEl, footerEl] = groupEls; + groupEl.insertBefore(audit.auditEl, footerEl); } } /** @type {Set} */ const filterableMetricAcronyms = new Set(); - for (const audit of diagnosticAudits) { + for (const audit of filterableAudits) { const metricSavings = audit.auditRef.result.metricSavings || {}; for (const [key, value] of Object.entries(metricSavings)) { if (typeof value === 'number') filterableMetricAcronyms.add(key); @@ -323,8 +354,12 @@ export class PerformanceCategoryRenderer extends CategoryRenderer { refreshFilteredAudits('All'); - if (diagnosticAudits.length) { - element.append(diagnosticsGroupEl); + for (const groupName of groupNames) { + if (filterableAudits.some(auditRef => auditRef.auditRef.group === groupName)) { + const groupEls = groupElsMap[groupName]; + if (!groupEls) continue; + element.append(groupEls[0]); + } } if (!passedAudits.length) return element; diff --git a/report/renderer/topbar-features.js b/report/renderer/topbar-features.js index 4b1efc686ee6..40ee0e867038 100644 --- a/report/renderer/topbar-features.js +++ b/report/renderer/topbar-features.js @@ -115,11 +115,10 @@ export class TopbarFeatures { break; } case 'toggle-insights': { - const insightsGroup = this._dom.find('.lh-perf-audits--experimental'); - insightsGroup.classList.toggle('lh-hidden'); - - const diagnosticsGroup = this._dom.find('.lh-perf-audits--legacy'); - diagnosticsGroup.classList.toggle('lh-hidden'); + const swappableSection = this._dom.maybeFind('.lh-perf-audits--swappable'); + if (swappableSection) { + this._dom.swapSectionIfPossible(swappableSection); + } break; } case 'view-unthrottled-trace': { diff --git a/report/test/renderer/performance-category-renderer-test.js b/report/test/renderer/performance-category-renderer-test.js index 631d6bb6d95d..6129d65e3c37 100644 --- a/report/test/renderer/performance-category-renderer-test.js +++ b/report/test/renderer/performance-category-renderer-test.js @@ -23,6 +23,11 @@ describe('PerfCategoryRenderer', () => { let renderer; let sampleResults; + function swapLegacyAndExperimentalPerfInsights(rootEl) { + const section = rootEl.querySelector('.lh-perf-audits--swappable'); + renderer.dom.swapSectionIfPossible(section); + } + before(() => { Globals.apply({ providedStrings: {}, @@ -62,8 +67,11 @@ describe('PerfCategoryRenderer', () => { it('renders the sections', () => { const categoryDOM = renderer.render(category, sampleResults.categoryGroups); const sections = categoryDOM.querySelectorAll('.lh-category .lh-audit-group'); - // Metrics, diagnostics, passed diagnostics, insights, passed insights - assert.equal(sections.length, 5); + // - Metrics + // Legacy view: + // - Diagnostics + // - Passed + assert.equal(sections.length, 3); }); it('renders the metrics', () => { @@ -115,8 +123,10 @@ describe('PerfCategoryRenderer', () => { const sections = categoryDOM.querySelectorAll('.lh-category .lh-audit-group'); const metricSection = categoryDOM.querySelector('.lh-audit-group--metrics'); assert.ok(!metricSection); - // diagnostics, passed diagnostics, insights, passed insights - assert.equal(sections.length, 4); + // Legacy view: + // - Diagnostics + // - Passed + assert.equal(sections.length, 2); }); it('renders the metrics variance disclaimer as markdown', () => { @@ -173,6 +183,7 @@ describe('PerfCategoryRenderer', () => { it('renders the failing insights', () => { const categoryDOM = renderer.render(category, sampleResults.categoryGroups); + swapLegacyAndExperimentalPerfInsights(categoryDOM); const insightSection = categoryDOM.querySelector( '.lh-category .lh-audit-group.lh-audit-group--insights'); @@ -198,16 +209,27 @@ describe('PerfCategoryRenderer', () => { assert.equal(passedElements.length, passedAudits.length); }); - it('renders the passed insight audits', () => { + it('renders the passed insight audits with passed diagnostics', () => { const categoryDOM = renderer.render(category, sampleResults.categoryGroups); + swapLegacyAndExperimentalPerfInsights(categoryDOM); const passedSection = categoryDOM.querySelector('.lh-perf-audits--experimental .lh-clump--passed'); - const passedAudits = category.auditRefs.filter(audit => + const passedInsights = category.auditRefs.filter(audit => audit.group === 'insights' && ReportUtils.showAsPassed(audit.result)); + + const replacedIds = new Set(); + for (const audit of category.auditRefs) { + audit.result.replacesAudits?.forEach(id => replacedIds.add(id)); + } + + const passedDiagnostics = category.auditRefs.filter(audit => + audit.group === 'diagnostics' && + !replacedIds.has(audit.id) && + ReportUtils.showAsPassed(audit.result)); const passedElements = passedSection.querySelectorAll('.lh-audit'); - assert.equal(passedElements.length, passedAudits.length); + assert.equal(passedElements.length, passedInsights.length + passedDiagnostics.length); }); // Unsupported by perf cat renderer right now. diff --git a/report/test/renderer/report-renderer-test.js b/report/test/renderer/report-renderer-test.js index 6e3814fb0c92..d9a0f0a56ac8 100644 --- a/report/test/renderer/report-renderer-test.js +++ b/report/test/renderer/report-renderer-test.js @@ -292,7 +292,8 @@ describe('ReportRenderer', () => { const hiddenAuditIds = new Set(); for (const category of Object.values(clonedSampleResult.categories)) { for (const auditRef of category.auditRefs) { - if (auditRef.group === 'hidden') { + // TODO: Remove insights condition when insights become the default + if (auditRef.group === 'hidden' || auditRef.group === 'insights') { hiddenAuditIds.add(auditRef.id); } } diff --git a/types/audit.d.ts b/types/audit.d.ts index f43c9e6198e2..43a360923356 100644 --- a/types/audit.d.ts +++ b/types/audit.d.ts @@ -64,6 +64,8 @@ declare module Audit { supportedModes?: Gatherer.GatherMode[], /** A number indicating how much guidance Lighthouse provides to solve the problem in this audit on a 1-3 scale. Higher means more guidance. */ guidanceLevel?: number; + /** A list of audit ids that this audit replaces. Used to ensure the report does not render the audits in this list at the same time as the audit which contains the list. */ + replacesAudits?: string[]; } interface ByteEfficiencyItem extends AuditDetails.OpportunityItem { diff --git a/types/lhr/audit-result.d.ts b/types/lhr/audit-result.d.ts index be6450a77328..f0d8d8076e45 100644 --- a/types/lhr/audit-result.d.ts +++ b/types/lhr/audit-result.d.ts @@ -76,4 +76,6 @@ export interface Result { }; /** A number indicating how much guidance Lighthouse provides to solve the problem in this audit on a 1-3 scale. Higher means more guidance. */ guidanceLevel?: number; + /** A list of audit ids that this audit replaces. Used to ensure the report does not render the audits in this list at the same time as the audit which contains the list. */ + replacesAudits?: string[]; } diff --git a/viewer/test/viewer-test-pptr.js b/viewer/test/viewer-test-pptr.js index f474813ae3a1..cd7d53ae989e 100644 --- a/viewer/test/viewer-test-pptr.js +++ b/viewer/test/viewer-test-pptr.js @@ -173,7 +173,8 @@ describe('Lighthouse Viewer', () => { ]; for (const category of lighthouseCategories) { const expectedAuditIds = getAuditsOfCategory(category) - .filter(a => a.group !== 'hidden' && !nonNavigationAudits.includes(a.id)) + .filter(a => a.group !== 'hidden' && a.group !== 'insights' && + !nonNavigationAudits.includes(a.id)) .map(a => a.id); const elementIds = await getAuditElementsIds({category, selector: selectors.audits});