Skip to content

Commit dae7769

Browse files
Adam RaineDevtools-frontend LUCI CQ
authored andcommitted
[RPP] Image delivery insight component
https://screenshot.googleplex.com/DnfY6A2YJY4uYDD Bug: 372897377 Change-Id: If90698babae22459fd9eba33cdbf8fd238926702 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6035443 Commit-Queue: Adam Raine <[email protected]> Reviewed-by: Connor Clark <[email protected]>
1 parent d2b5da9 commit dae7769

File tree

11 files changed

+229
-36
lines changed

11 files changed

+229
-36
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1838,6 +1838,7 @@ grd_files_debug_sources = [
18381838
"front_end/panels/timeline/components/insights/EventRef.js",
18391839
"front_end/panels/timeline/components/insights/FontDisplay.js",
18401840
"front_end/panels/timeline/components/insights/Helpers.js",
1841+
"front_end/panels/timeline/components/insights/ImageDelivery.js",
18411842
"front_end/panels/timeline/components/insights/InteractionToNextPaint.js",
18421843
"front_end/panels/timeline/components/insights/LCPDiscovery.js",
18431844
"front_end/panels/timeline/components/insights/LCPPhases.js",

front_end/models/trace/insights/ImageDelivery.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@ export function deps(): ['NetworkRequests', 'Meta', 'ImagePainting'] {
5252
return ['NetworkRequests', 'Meta', 'ImagePainting'];
5353
}
5454

55+
export type ImageOptimizationType = 'modern-format-or-compression'|'compression'|'video-format'|'responsive-size';
56+
5557
export interface ImageOptimization {
56-
type: 'modern-format-or-compression'|'compression'|'video-format'|'responsive-size';
58+
type: ImageOptimizationType;
5759
byteSavings: number;
5860
}
5961

front_end/panels/timeline/components/SidebarSingleInsightSet.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ describeWithEnvironment('SidebarSingleInsightSet', () => {
5252
'LCP by phase',
5353
'LCP request discovery',
5454
'Render blocking requests',
55+
'Improve image delivery',
5556
'Document request latency',
5657
'Third parties',
5758
]);
@@ -79,6 +80,7 @@ describeWithEnvironment('SidebarSingleInsightSet', () => {
7980
'LCP by phase',
8081
'LCP request discovery',
8182
'Layout shift culprits',
83+
'Improve image delivery',
8284
'Third parties',
8385
]);
8486
});
@@ -108,6 +110,7 @@ describeWithEnvironment('SidebarSingleInsightSet', () => {
108110
'LCP by phase',
109111
'LCP request discovery',
110112
'Layout shift culprits',
113+
'Improve image delivery',
111114
'Font display',
112115
'Third parties',
113116
]);

front_end/panels/timeline/components/SidebarSingleInsightSet.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const INSIGHT_NAME_TO_COMPONENT = {
5656
LCPDiscovery: Insights.LCPDiscovery.LCPDiscovery,
5757
CLSCulprits: Insights.CLSCulprits.CLSCulprits,
5858
RenderBlocking: Insights.RenderBlocking.RenderBlocking,
59+
ImageDelivery: Insights.ImageDelivery.ImageDelivery,
5960
DocumentLatency: Insights.DocumentLatency.DocumentLatency,
6061
FontDisplay: Insights.FontDisplay.FontDisplay,
6162
Viewport: Insights.Viewport.Viewport,

front_end/panels/timeline/components/insights/BUILD.gn

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ devtools_module("insights") {
2222
"EventRef.ts",
2323
"FontDisplay.ts",
2424
"Helpers.ts",
25+
"ImageDelivery.ts",
2526
"InteractionToNextPaint.ts",
2627
"LCPDiscovery.ts",
2728
"LCPPhases.ts",

front_end/panels/timeline/components/insights/EventRef.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import * as i18n from '../../../../core/i18n/i18n.js';
56
import * as Platform from '../../../../core/platform/platform.js';
67
import * as Trace from '../../../../models/trace/trace.js';
78
import * as ComponentHelpers from '../../../../ui/components/helpers/helpers.js';
@@ -78,14 +79,81 @@ export function eventRef(event: EventRefSupportedEvents): LitHtml.TemplateResult
7879
></devtools-performance-event-ref>`;
7980
}
8081

82+
class ImageRef extends HTMLElement {
83+
readonly #shadow = this.attachShadow({mode: 'open'});
84+
readonly #boundRender = this.#render.bind(this);
85+
86+
#request?: Trace.Types.Events.SyntheticNetworkRequest;
87+
#imagePaint?: Trace.Types.Events.PaintImage;
88+
89+
connectedCallback(): void {
90+
this.#shadow.adoptedStyleSheets = [baseInsightComponentStyles];
91+
}
92+
93+
set request(request: Trace.Types.Events.SyntheticNetworkRequest) {
94+
this.#request = request;
95+
void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#boundRender);
96+
}
97+
98+
set imagePaint(imagePaint: Trace.Types.Events.PaintImage|undefined) {
99+
this.#imagePaint = imagePaint;
100+
void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#boundRender);
101+
}
102+
103+
#render(): void {
104+
if (!this.#request) {
105+
return;
106+
}
107+
108+
// clang-format off
109+
LitHtml.render(html`
110+
<div class="image-ref">
111+
${this.#request.args.data.mimeType.includes('image') ? html`
112+
<img
113+
class="element-img"
114+
src=${this.#request.args.data.url}
115+
@error=${handleBadImage}/>
116+
`: LitHtml.nothing}
117+
<span class="element-img-details">
118+
${eventRef(this.#request)}
119+
<span class="element-img-details-size">${
120+
this.#imagePaint ?
121+
`${this.#imagePaint.args.data.srcWidth}x${this.#imagePaint.args.data.srcHeight}` :
122+
i18n.ByteUtilities.bytesToString(this.#request.args.data.decodedBodyLength ?? 0)
123+
}</span>
124+
</span>
125+
</div>
126+
`, this.#shadow, {host: this});
127+
// clang-format on
128+
}
129+
}
130+
131+
function handleBadImage(event: Event): void {
132+
const img = event.target as HTMLImageElement;
133+
img.style.display = 'none';
134+
}
135+
136+
export function imageRef(
137+
request: Trace.Types.Events.SyntheticNetworkRequest,
138+
imagePaint?: Trace.Types.Events.PaintImage): LitHtml.TemplateResult {
139+
return html`
140+
<devtools-performance-image-ref
141+
.request=${request}
142+
.imagePaint=${imagePaint}
143+
></devtools-performance-image-ref>
144+
`;
145+
}
146+
81147
declare global {
82148
interface GlobalEventHandlersEventMap {
83149
[EventReferenceClick.eventName]: EventReferenceClick;
84150
}
85151

86152
interface HTMLElementTagNameMap {
87153
'devtools-performance-event-ref': EventRef;
154+
'devtools-performance-image-ref': ImageRef;
88155
}
89156
}
90157

91158
customElements.define('devtools-performance-event-ref', EventRef);
159+
customElements.define('devtools-performance-image-ref', ImageRef);
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright 2024 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import '../../../../ui/components/icon_button/icon_button.js';
6+
import './Table.js';
7+
8+
import * as i18n from '../../../../core/i18n/i18n.js';
9+
import type {
10+
ImageDeliveryInsightModel, ImageOptimizationType, OptimizableImage} from
11+
'../../../../models/trace/insights/ImageDelivery.js';
12+
import type * as Trace from '../../../../models/trace/trace.js';
13+
import * as LitHtml from '../../../../ui/lit-html/lit-html.js';
14+
import type * as Overlays from '../../overlays/overlays.js';
15+
16+
import {BaseInsightComponent} from './BaseInsightComponent.js';
17+
import {imageRef} from './EventRef.js';
18+
import type {TableDataRow} from './Table.js';
19+
20+
const {html} = LitHtml;
21+
22+
const UIStrings = {
23+
/**
24+
* @description Column header for a table column containing network requests for images that are not sized correctly for how they are displayed on the page.
25+
*/
26+
sizeAppropriately: 'Size appropriately',
27+
/**
28+
* @description Column header for a table column containing network requests for images which can improve their file size (e.g. use a different format, increase compression, etc).
29+
*/
30+
optimizeFile: 'Optimize file size',
31+
/**
32+
* @description Table row value representing the remaining items not shown in the table due to size constraints. This row will always represent at least 2 items.
33+
* @example {5} PH1
34+
*/
35+
others: '{PH1} others',
36+
};
37+
38+
const str_ = i18n.i18n.registerUIStrings('panels/timeline/components/insights/ImageDelivery.ts', UIStrings);
39+
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
40+
41+
export class ImageDelivery extends BaseInsightComponent<ImageDeliveryInsightModel> {
42+
static override readonly litTagName = LitHtml.literal`devtools-performance-image-delivery`;
43+
override internalName: string = 'image-delivery';
44+
45+
override createOverlays(): Overlays.Overlays.TimelineOverlay[] {
46+
if (!this.model) {
47+
return [];
48+
}
49+
50+
const {optimizableImages} = this.model;
51+
return optimizableImages.map(image => this.#createOverlayForRequest(image.request));
52+
}
53+
54+
#createOverlayForRequest(request: Trace.Types.Events.SyntheticNetworkRequest): Overlays.Overlays.EntryOutline {
55+
return {
56+
type: 'ENTRY_OUTLINE',
57+
entry: request,
58+
outlineReason: 'ERROR',
59+
};
60+
}
61+
62+
#getTopImagesAsRows(
63+
optimizableImages: OptimizableImage[], typeFilter: (type: ImageOptimizationType) => boolean,
64+
showDimensions?: boolean): TableDataRow[] {
65+
const MAX_REQUESTS = 3;
66+
const topImages =
67+
optimizableImages.filter(image => image.optimizations.some(o => typeFilter(o.type)))
68+
.sort((a, b) => b.request.args.data.decodedBodyLength - a.request.args.data.decodedBodyLength);
69+
70+
const remaining = topImages.splice(MAX_REQUESTS);
71+
72+
const rows: TableDataRow[] = topImages.map(image => ({
73+
values: [
74+
imageRef(
75+
image.request,
76+
showDimensions ? image.largestImagePaint : undefined,
77+
),
78+
],
79+
overlays: [this.#createOverlayForRequest(image.request)],
80+
}));
81+
82+
if (remaining.length > 0) {
83+
const value = remaining.length > 1 ? i18nString(UIStrings.others, {PH1: remaining.length}) :
84+
imageRef(
85+
remaining[0].request,
86+
showDimensions ? remaining[0].largestImagePaint : undefined,
87+
);
88+
rows.push({
89+
values: [value],
90+
overlays: remaining.map(r => this.#createOverlayForRequest(r.request)),
91+
});
92+
}
93+
94+
return rows;
95+
}
96+
97+
#renderContent(): LitHtml.LitTemplate {
98+
if (!this.model) {
99+
return LitHtml.nothing;
100+
}
101+
102+
const optimizableImages = this.model.optimizableImages;
103+
104+
// clang-format off
105+
return html`
106+
<div class="insight-section">
107+
<devtools-performance-table
108+
.data=${{
109+
insight: this,
110+
headers: [i18nString(UIStrings.sizeAppropriately)],
111+
rows: this.#getTopImagesAsRows(optimizableImages, type => type === 'responsive-size', true),
112+
}}>
113+
</devtools-performance-table>
114+
</div>
115+
<div class="insight-section">
116+
<devtools-performance-table
117+
.data=${{
118+
insight: this,
119+
headers: [i18nString(UIStrings.optimizeFile)],
120+
rows: this.#getTopImagesAsRows(optimizableImages, type => type !== 'responsive-size'),
121+
}}>
122+
</devtools-performance-table>
123+
</div>`;
124+
// clang-format on
125+
}
126+
127+
override render(): void {
128+
const output = this.#renderContent();
129+
this.renderWithContent(output);
130+
}
131+
}
132+
133+
declare global {
134+
interface HTMLElementTagNameMap {
135+
'devtools-performance-image-delivery': ImageDelivery;
136+
}
137+
}
138+
139+
customElements.define('devtools-performance-image-delivery', ImageDelivery);

front_end/panels/timeline/components/insights/LCPDiscovery.ts

Lines changed: 2 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as LitHtml from '../../../../ui/lit-html/lit-html.js';
1111
import type * as Overlays from '../../overlays/overlays.js';
1212

1313
import {BaseInsightComponent} from './BaseInsightComponent.js';
14-
import {eventRef} from './EventRef.js';
14+
import {imageRef} from './EventRef.js';
1515

1616
const {html} = LitHtml;
1717

@@ -157,30 +157,6 @@ export class LCPDiscovery extends BaseInsightComponent<LCPDiscoveryInsightModel>
157157
];
158158
}
159159

160-
#handleBadImage(event: Event): void {
161-
const img = event.target as HTMLImageElement;
162-
img.style.display = 'none';
163-
}
164-
165-
#renderImage(imageData: LCPImageDiscoveryData): LitHtml.TemplateResult {
166-
// clang-format off
167-
return html`
168-
<div class="lcp-element">
169-
${imageData.request.args.data.mimeType.includes('image') ?
170-
html`
171-
<img
172-
class="element-img"
173-
src=${imageData.request.args.data.url}
174-
@error=${this.#handleBadImage}
175-
/>`: LitHtml.nothing}
176-
<span class="element-img-details">
177-
${eventRef(imageData.request)}
178-
<span class="element-img-details-size">${i18n.ByteUtilities.bytesToString(imageData.request.args.data.decodedBodyLength ?? 0)}</span>
179-
</span>
180-
</div>`;
181-
// clang-format on
182-
}
183-
184160
override getEstimatedSavingsTime(): Trace.Types.Timing.MilliSeconds|null {
185161
return getImageData(this.model)?.estimatedSavings ?? null;
186162
}
@@ -209,7 +185,7 @@ export class LCPDiscovery extends BaseInsightComponent<LCPDiscoveryInsightModel>
209185
</li>
210186
</ul>
211187
</div>
212-
${this.#renderImage(imageData)}
188+
${imageRef(imageData.request)}
213189
</div>`;
214190
// clang-format on
215191
}

front_end/panels/timeline/components/insights/baseInsightComponent.css

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,15 @@ dd.dl-title {
131131
align-items: center;
132132
}
133133

134+
.image-ref {
135+
display: inline-flex;
136+
align-items: center;
137+
138+
&:not(:empty) {
139+
padding-top: var(--sys-size-5);
140+
}
141+
}
142+
134143
.element-img {
135144
width: var(--sys-size-13);
136145
height: var(--sys-size-13);
@@ -194,16 +203,7 @@ ul.insight-icon-results {
194203
color: var(--sys-color-state-disabled);
195204
}
196205

197-
.lcp-element {
198-
display: inline-flex;
199-
align-items: center;
200-
}
201-
202206
.insight-results:not(:last-child) {
203207
border-bottom: var(--sys-size-1) solid var(--sys-color-divider);
204208
padding-bottom: var(--sys-size-5);
205209
}
206-
207-
.lcp-element:not(:empty) {
208-
padding: inherit;
209-
}

front_end/panels/timeline/components/insights/insights.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as DocumentLatency from './DocumentLatency.js';
88
import * as EventRef from './EventRef.js';
99
import * as FontDisplay from './FontDisplay.js';
1010
import * as Helpers from './Helpers.js';
11+
import * as ImageDelivery from './ImageDelivery.js';
1112
import * as InteractionToNextPaint from './InteractionToNextPaint.js';
1213
import * as LCPDiscovery from './LCPDiscovery.js';
1314
import * as LCPPhases from './LCPPhases.js';
@@ -27,6 +28,7 @@ export {
2728
EventRef,
2829
FontDisplay,
2930
Helpers,
31+
ImageDelivery,
3032
InteractionToNextPaint,
3133
LCPDiscovery,
3234
LCPPhases,

0 commit comments

Comments
 (0)