Skip to content

Commit cf5c715

Browse files
Connor ClarkDevtools-frontend LUCI CQ
authored andcommitted
[RPP] Add Legacy JavaScript insight component
- Shown by default (not in experimental) - Adds `scriptRef` helper for rendering a script - Sorts results by estimated savings in the model - Filter out results less than 5kb in the model Fixed: 394373852 Change-Id: I7558f9dba01238b1af80a6d8d500b900bd03b964 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6410451 Auto-Submit: Connor Clark <[email protected]> Reviewed-by: Paul Irish <[email protected]> Commit-Queue: Connor Clark <[email protected]>
1 parent 305ec9b commit cf5c715

File tree

15 files changed

+247
-14
lines changed

15 files changed

+247
-14
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1932,10 +1932,12 @@ grd_files_debug_sources = [
19321932
"front_end/panels/timeline/components/insights/InteractionToNextPaint.js",
19331933
"front_end/panels/timeline/components/insights/LCPDiscovery.js",
19341934
"front_end/panels/timeline/components/insights/LCPPhases.js",
1935+
"front_end/panels/timeline/components/insights/LegacyJavaScript.js",
19351936
"front_end/panels/timeline/components/insights/ModernHTTP.js",
19361937
"front_end/panels/timeline/components/insights/NetworkDependencyTree.js",
19371938
"front_end/panels/timeline/components/insights/NodeLink.js",
19381939
"front_end/panels/timeline/components/insights/RenderBlocking.js",
1940+
"front_end/panels/timeline/components/insights/ScriptRef.js",
19391941
"front_end/panels/timeline/components/insights/SidebarInsight.js",
19401942
"front_end/panels/timeline/components/insights/SlowCSSSelector.js",
19411943
"front_end/panels/timeline/components/insights/Table.js",

front_end/models/trace/insights/LegacyJavaScript.test.ts

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {getFirstOrError, getInsightOrError, processTrace} from '../../../testing
77
import type * as Trace from '../trace.js';
88

99
describeWithEnvironment('LegacyJavaScript', function() {
10-
it('works', async () => {
10+
it('has no results when savings are too small', async () => {
1111
const {data, insights} = await processTrace(this, 'dupe-js.json.gz');
1212
assert.strictEqual(insights.size, 1);
1313
const insight =
@@ -18,15 +18,66 @@ describeWithEnvironment('LegacyJavaScript', function() {
1818
return [script.url, data];
1919
}));
2020

21+
assert.deepEqual(results, {});
22+
assert.deepEqual(insight.metricSavings, {FCP: 0, LCP: 0} as Trace.Insights.Types.MetricSavings);
23+
});
24+
25+
it('has results when savings are big enough', async function() {
26+
if (this.timeout() > 0) {
27+
this.timeout(20000);
28+
}
29+
30+
const {data, insights} = await processTrace(this, 'yahoo-news.json.gz');
31+
assert.strictEqual(insights.size, 1);
32+
const insight =
33+
getInsightOrError('LegacyJavaScript', insights, getFirstOrError(data.Meta.navigationsByNavigationId.values()));
34+
35+
const results = Object.fromEntries([...insight.legacyJavaScriptResults.entries()].map(([script, data]) => {
36+
return [script.url, data];
37+
}));
38+
2139
assert.deepEqual(results, {
22-
'https://dupe-modules-lh-2.surge.sh/bundle.js?v1': {
23-
estimatedByteSavings: 2952,
40+
'https://s.yimg.com/du/ay/wnsrvbjmeprtfrnfx.js': {
2441
matches: [
25-
{name: '@babel/plugin-transform-classes', line: 1, column: 165614},
26-
{name: '@babel/plugin-transform-regenerator', line: 1, column: 159765},
27-
{name: '@babel/plugin-transform-spread', line: 1, column: 350646},
42+
{name: '@babel/plugin-transform-spread', line: 111, column: 7829},
43+
{name: 'Array.prototype.find', line: 111, column: 1794},
44+
{name: 'Array.prototype.includes', line: 111, column: 2127}, {name: 'Object.values', line: 111, column: 2748},
45+
{name: 'String.prototype.includes', line: 111, column: 2473},
46+
{name: 'String.prototype.startsWith', line: 111, column: 2627}
2847
],
48+
estimatedByteSavings: 42001
49+
},
50+
'https://s.yimg.com/aaq/benji/benji-2.2.99.js':
51+
{matches: [{name: 'Promise.allSettled', line: 0, column: 133}], estimatedByteSavings: 37204},
52+
'https://s.yimg.com/aaq/c/25fa214.caas-news_web.min.js': {
53+
matches: [{name: 'Array.from', line: 0, column: 13310}, {name: 'Object.assign', line: 0, column: 14623}],
54+
estimatedByteSavings: 36084
55+
},
56+
'https://news.yahoo.com/': {
57+
matches: [
58+
{name: '@babel/plugin-transform-classes', line: 0, column: 8382},
59+
{name: 'Array.prototype.filter', line: 0, column: 107712},
60+
{name: 'Array.prototype.forEach', line: 0, column: 107393},
61+
{name: 'Array.prototype.map', line: 0, column: 108005},
62+
{name: 'String.prototype.includes', line: 0, column: 108358}
63+
],
64+
estimatedByteSavings: 36070
65+
},
66+
'https://static.criteo.net/js/ld/publishertag.prebid.144.js': {
67+
matches: [
68+
{name: 'Array.isArray', line: 1, column: 74871}, {name: 'Array.prototype.filter', line: 1, column: 75344},
69+
{name: 'Array.prototype.indexOf', line: 1, column: 75013}
70+
],
71+
estimatedByteSavings: 33233
72+
},
73+
'https://s.yimg.com/oa/consent.js':
74+
{matches: [{name: 'Array.prototype.includes', line: 1, column: 132267}], estimatedByteSavings: 29997},
75+
'https://cdn.taboola.com/libtrc/yahooweb-network/loader.js': {
76+
matches: [{name: 'Object.entries', line: 0, column: 390544}, {name: 'Object.values', line: 0, column: 390688}],
77+
estimatedByteSavings: 28643
2978
},
79+
'https://pm-widget.taboola.com/yahooweb-network/pmk-20220605.1.js':
80+
{matches: [{name: 'Object.keys', line: 181, column: 26}], estimatedByteSavings: 26653}
3081
});
3182

3283
assert.deepEqual(insight.metricSavings, {FCP: 0, LCP: 0} as Trace.Insights.Types.MetricSavings);

front_end/models/trace/insights/LegacyJavaScript.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,12 @@ export const UIStrings = {
2626
/**
2727
* @description Description of an insight that identifies polyfills for modern JavaScript features, and recommends their removal.
2828
*/
29-
description: 'Legacy JavaScript',
29+
description:
30+
'Polyfills and transforms enable legacy browsers to use new JavaScript features. However, many aren\'t necessary for modern browsers. Consider modifying your JavaScript build process to not transpile [Baseline](https://web.dev/articles/baseline-and-polyfills) features, unless you know you must support legacy browsers. [Learn why most sites can deploy ES6+ code without transpiling](https://philipwalton.com/articles/the-state-of-es5-on-the-web/)',
31+
/** Label for a column in a data table; entries will be the individual JavaScript scripts. */
32+
columnScript: 'Script',
33+
/** Label for a column in a data table; entries will be the number of wasted bytes (aka the estimated savings in terms of bytes). */
34+
columnWastedBytes: 'Wasted bytes',
3035
} as const;
3136

3237
const str_ = i18n.i18n.registerUIStrings('models/trace/insights/LegacyJavaScript.ts', UIStrings);
@@ -49,6 +54,8 @@ export type LegacyJavaScriptInsightModel = InsightModel<typeof UIStrings, {
4954
legacyJavaScriptResults: LegacyJavaScriptResults,
5055
}>;
5156

57+
const BYTE_THRESHOLD = 5000;
58+
5259
function finalize(partialModel: PartialInsightModel<LegacyJavaScriptInsightModel>): LegacyJavaScriptInsightModel {
5360
const requests = [...partialModel.legacyJavaScriptResults.keys()].map(script => script.request).filter(e => !!e);
5461

@@ -86,11 +93,15 @@ export function generateInsight(
8693
const wastedBytesByRequestId = new Map<string, number>();
8794

8895
for (const script of scripts) {
89-
if (!script.content) {
96+
if (!script.content || script.content.length < BYTE_THRESHOLD) {
9097
continue;
9198
}
9299

93100
const result = detectLegacyJavaScript(script.content, script.sourceMap);
101+
if (result.estimatedByteSavings < BYTE_THRESHOLD) {
102+
continue;
103+
}
104+
94105
legacyJavaScriptResults.set(script, result);
95106

96107
if (script.request) {
@@ -101,8 +112,11 @@ export function generateInsight(
101112
}
102113
}
103114

115+
const sorted =
116+
new Map([...legacyJavaScriptResults].sort((a, b) => b[1].estimatedByteSavings - a[1].estimatedByteSavings));
117+
104118
return finalize({
105-
legacyJavaScriptResults,
119+
legacyJavaScriptResults: sorted,
106120
metricSavings: metricSavingsForWastedBytes(wastedBytesByRequestId, context),
107121
});
108122
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ describeWithEnvironment('SidebarSingleInsightSet', () => {
7272
'Forced reflow',
7373
'Use efficient cache lifetimes',
7474
'Modern HTTP',
75+
'Legacy JavaScript',
7576
]);
7677

7778
const passedInsightTitles = getPassedInsights(component).flatMap(component => {
@@ -90,6 +91,7 @@ describeWithEnvironment('SidebarSingleInsightSet', () => {
9091
'Forced reflow',
9192
'Use efficient cache lifetimes',
9293
'Modern HTTP',
94+
'Legacy JavaScript',
9395
]);
9496
});
9597

@@ -130,6 +132,7 @@ describeWithEnvironment('SidebarSingleInsightSet', () => {
130132
'CSS Selector costs',
131133
'Forced reflow',
132134
'Modern HTTP',
135+
'Legacy JavaScript',
133136
]);
134137

135138
const passedInsightTitles = getPassedInsights(component).flatMap(component => {
@@ -147,6 +150,7 @@ describeWithEnvironment('SidebarSingleInsightSet', () => {
147150
'CSS Selector costs',
148151
'Forced reflow',
149152
'Modern HTTP',
153+
'Legacy JavaScript',
150154
]);
151155
});
152156

@@ -189,6 +193,7 @@ describeWithEnvironment('SidebarSingleInsightSet', () => {
189193
'CSS Selector costs',
190194
'Forced reflow',
191195
'Modern HTTP',
196+
'Legacy JavaScript',
192197
]);
193198

194199
const passedInsightTitles = getPassedInsights(component).flatMap(component => {
@@ -205,6 +210,7 @@ describeWithEnvironment('SidebarSingleInsightSet', () => {
205210
'CSS Selector costs',
206211
'Forced reflow',
207212
'Modern HTTP',
213+
'Legacy JavaScript',
208214
]);
209215
});
210216

front_end/panels/timeline/components/SidebarSingleInsightSet.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ type InsightNameToComponentMapping =
101101
* Order does not matter (but keep alphabetized).
102102
*/
103103
const INSIGHT_NAME_TO_COMPONENT: InsightNameToComponentMapping = {
104+
Cache: Insights.Cache.Cache,
104105
CLSCulprits: Insights.CLSCulprits.CLSCulprits,
105106
DocumentLatency: Insights.DocumentLatency.DocumentLatency,
106107
DOMSize: Insights.DOMSize.DOMSize,
@@ -111,13 +112,13 @@ const INSIGHT_NAME_TO_COMPONENT: InsightNameToComponentMapping = {
111112
InteractionToNextPaint: Insights.InteractionToNextPaint.InteractionToNextPaint,
112113
LCPDiscovery: Insights.LCPDiscovery.LCPDiscovery,
113114
LCPPhases: Insights.LCPPhases.LCPPhases,
115+
LegacyJavaScript: Insights.LegacyJavaScript.LegacyJavaScript,
116+
ModernHTTP: Insights.ModernHTTP.ModernHTTP,
114117
NetworkDependencyTree: Insights.NetworkDependencyTree.NetworkDependencyTree,
115118
RenderBlocking: Insights.RenderBlocking.RenderBlocking,
116119
SlowCSSSelector: Insights.SlowCSSSelector.SlowCSSSelector,
117120
ThirdParties: Insights.ThirdParties.ThirdParties,
118121
Viewport: Insights.Viewport.Viewport,
119-
Cache: Insights.Cache.Cache,
120-
ModernHTTP: Insights.ModernHTTP.ModernHTTP,
121122
};
122123

123124
export class SidebarSingleInsightSet extends HTMLElement {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ devtools_module("insights") {
3333
"InteractionToNextPaint.ts",
3434
"LCPDiscovery.ts",
3535
"LCPPhases.ts",
36+
"LegacyJavaScript.ts",
3637
"ModernHTTP.ts",
3738
"NetworkDependencyTree.ts",
3839
"NodeLink.ts",
3940
"RenderBlocking.ts",
41+
"ScriptRef.ts",
4042
"SidebarInsight.ts",
4143
"SlowCSSSelector.ts",
4244
"Table.ts",

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type * as Overlays from '../../overlays/overlays.js';
1414
import * as Utils from '../../utils/utils.js';
1515

1616
import {BaseInsightComponent} from './BaseInsightComponent.js';
17-
import {eventRef} from './EventRef.js';
17+
import {scriptRef} from './ScriptRef.js';
1818
import type {TableData, TableDataRow} from './Table.js';
1919

2020
const {UIStrings, i18nString} = Trace.Insights.Models.DuplicatedJavaScript;
@@ -108,7 +108,7 @@ export class DuplicatedJavaScript extends BaseInsightComponent<DuplicatedJavaScr
108108

109109
return {
110110
values: [
111-
script.request ? eventRef(script.request) : script.url ?? 'unknown',
111+
scriptRef(script),
112112
index === 0 ? '--' : i18n.ByteUtilities.bytesToString(resourceSize),
113113
],
114114
overlays,
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2025 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 './Table.js';
6+
7+
import * as i18n from '../../../../core/i18n/i18n.js';
8+
import type {LegacyJavaScriptInsightModel} from '../../../../models/trace/insights/LegacyJavaScript.js';
9+
import * as Trace from '../../../../models/trace/trace.js';
10+
import * as Lit from '../../../../ui/lit/lit.js';
11+
import type * as Overlays from '../../overlays/overlays.js';
12+
13+
import {BaseInsightComponent} from './BaseInsightComponent.js';
14+
import {scriptRef} from './ScriptRef.js';
15+
import type {TableData, TableDataRow} from './Table.js';
16+
17+
const {UIStrings, i18nString} = Trace.Insights.Models.LegacyJavaScript;
18+
19+
const {html} = Lit;
20+
21+
export class LegacyJavaScript extends BaseInsightComponent<LegacyJavaScriptInsightModel> {
22+
static override readonly litTagName = Lit.StaticHtml.literal`devtools-performance-legacy-javascript`;
23+
override internalName = 'legacy-javascript';
24+
25+
override getEstimatedSavingsTime(): Trace.Types.Timing.Milli|null {
26+
return this.model?.metricSavings?.FCP ?? null;
27+
}
28+
29+
override getEstimatedSavingsBytes(): number|null {
30+
if (!this.model) {
31+
return null;
32+
}
33+
34+
let estimatedByteSavings = 0;
35+
for (const result of this.model.legacyJavaScriptResults.values()) {
36+
estimatedByteSavings += result.estimatedByteSavings;
37+
}
38+
39+
return estimatedByteSavings;
40+
}
41+
42+
override createOverlays(): Overlays.Overlays.TimelineOverlay[] {
43+
if (!this.model) {
44+
return [];
45+
}
46+
47+
const requests = [...this.model.legacyJavaScriptResults.keys()].map(script => script.request).filter(e => !!e);
48+
return requests.map(request => {
49+
return {
50+
type: 'ENTRY_OUTLINE',
51+
entry: request,
52+
outlineReason: 'ERROR',
53+
};
54+
});
55+
}
56+
57+
override renderContent(): Lit.LitTemplate {
58+
if (!this.model) {
59+
return Lit.nothing;
60+
}
61+
62+
const rows: TableDataRow[] =
63+
[...this.model.legacyJavaScriptResults.entries()].slice(0, 10).map(([script, result]) => {
64+
const overlays: Overlays.Overlays.TimelineOverlay[] = [];
65+
if (script.request) {
66+
overlays.push({
67+
type: 'ENTRY_OUTLINE',
68+
entry: script.request,
69+
outlineReason: 'ERROR',
70+
});
71+
}
72+
73+
return {
74+
values: [scriptRef(script), i18n.ByteUtilities.bytesToString(result.estimatedByteSavings)],
75+
overlays,
76+
subRows: result.matches.map(match => {
77+
return {
78+
values: [html`<span title=${`${script.url}:${match.line}:${match.column}`}>${match.name}</span>`],
79+
};
80+
})
81+
};
82+
});
83+
84+
// clang-format off
85+
return html`
86+
<div class="insight-section">
87+
<devtools-performance-table
88+
.data=${{
89+
insight: this,
90+
headers: [i18nString(UIStrings.columnScript), i18nString(UIStrings.columnWastedBytes)],
91+
rows,
92+
} as TableData}>
93+
</devtools-performance-table>
94+
</div>
95+
`;
96+
// clang-format on
97+
}
98+
}
99+
100+
declare global {
101+
interface HTMLElementTagNameMap {
102+
'devtools-performance-legacy-javascript': LegacyJavaScript;
103+
}
104+
}
105+
106+
customElements.define('devtools-performance-legacy-javascript', LegacyJavaScript);
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright 2025 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 * as Platform from '../../../../core/platform/platform.js';
6+
import type * as Trace from '../../../../models/trace/trace.js';
7+
import type * as Lit from '../../../../ui/lit/lit.js';
8+
import * as TimelineUtils from '../../utils/utils.js';
9+
10+
import {eventRef} from './EventRef.js';
11+
12+
export function scriptRef(script: Trace.Handlers.ModelHandlers.Scripts.Script): Lit.TemplateResult|string {
13+
// The happy path is that we have a network request associated with this script.
14+
if (script.request) {
15+
if (script.inline) {
16+
return eventRef(script.request, {
17+
text: `(inline) ${Platform.StringUtilities.trimEndWithMaxLength(script.content ?? '', 15)}`,
18+
});
19+
}
20+
21+
return eventRef(script.request);
22+
}
23+
24+
if (script.url) {
25+
try {
26+
const parsedUrl = new URL(script.url);
27+
return TimelineUtils.Helpers.shortenUrl(parsedUrl);
28+
} catch {
29+
}
30+
}
31+
32+
if (script.inline) {
33+
return `(inline) ${Platform.StringUtilities.trimEndWithMaxLength(script.content ?? '', 15)}`;
34+
}
35+
36+
return `script id: ${script.scriptId}`;
37+
}

0 commit comments

Comments
 (0)