-
Notifications
You must be signed in to change notification settings - Fork 9.5k
/
Copy pathmodule-duplication.js
138 lines (114 loc) · 4.28 KB
/
module-duplication.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {makeComputedArtifact} from './computed-artifact.js';
import {JSBundles} from './js-bundles.js';
const RELATIVE_SIZE_THRESHOLD = 0.1;
const ABSOLUTE_SIZE_THRESHOLD_BYTES = 1024 * 0.5;
class ModuleDuplication {
/**
* @param {string} source
*/
static normalizeSource(source) {
// Trim trailing question mark - b/c webpack.
source = source.replace(/\?$/, '');
// Normalize paths for dependencies by only keeping everything after the last `node_modules`.
const lastNodeModulesIndex = source.lastIndexOf('node_modules');
if (lastNodeModulesIndex !== -1) {
source = source.substring(lastNodeModulesIndex);
}
return source;
}
/**
* @param {string} source
*/
static _shouldIgnoreSource(source) {
// Ignore bundle overhead.
if (source.includes('webpack/bootstrap')) return true;
if (source.includes('(webpack)/buildin')) return true;
// Ignore webpack module shims, i.e. aliases of the form `module.exports = window.jQuery`
if (source.includes('external ')) return true;
return false;
}
/**
* @param {Map<string, Array<{scriptId: string, resourceSize: number}>>} moduleNameToSourceData
*/
static _normalizeAggregatedData(moduleNameToSourceData) {
for (const [key, originalSourceData] of moduleNameToSourceData.entries()) {
let sourceData = originalSourceData;
// Sort by resource size.
sourceData.sort((a, b) => b.resourceSize - a.resourceSize);
// Remove modules smaller than a % size of largest.
if (sourceData.length > 1) {
const largestResourceSize = sourceData[0].resourceSize;
sourceData = sourceData.filter(data => {
const percentSize = data.resourceSize / largestResourceSize;
return percentSize >= RELATIVE_SIZE_THRESHOLD;
});
}
// Remove modules smaller than an absolute theshold.
sourceData = sourceData.filter(data => data.resourceSize >= ABSOLUTE_SIZE_THRESHOLD_BYTES);
// Delete source datas with only one value (no duplicates).
if (sourceData.length > 1) {
moduleNameToSourceData.set(key, sourceData);
} else {
moduleNameToSourceData.delete(key);
}
}
}
/**
* @param {Pick<LH.Artifacts, 'Scripts'|'SourceMaps'>} artifacts
* @param {LH.Artifacts.ComputedContext} context
*/
static async compute_(artifacts, context) {
const bundles = await JSBundles.request(artifacts, context);
/**
* @typedef SourceData
* @property {string} source
* @property {number} resourceSize
*/
/** @type {Map<LH.Artifacts.RawSourceMap, SourceData[]>} */
const sourceDatasMap = new Map();
// Determine size of each `sources` entry.
for (const {rawMap, sizes} of bundles) {
if ('errorMessage' in sizes) continue;
/** @type {SourceData[]} */
const sourceDataArray = [];
sourceDatasMap.set(rawMap, sourceDataArray);
for (let i = 0; i < rawMap.sources.length; i++) {
if (this._shouldIgnoreSource(rawMap.sources[i])) continue;
const sourceKey = (rawMap.sourceRoot || '') + rawMap.sources[i];
const sourceSize = sizes.files[sourceKey];
sourceDataArray.push({
source: ModuleDuplication.normalizeSource(rawMap.sources[i]),
resourceSize: sourceSize,
});
}
}
/** @type {Map<string, Array<{scriptId: string, scriptUrl: string, resourceSize: number}>>} */
const moduleNameToSourceData = new Map();
for (const {rawMap, script} of bundles) {
const sourceDataArray = sourceDatasMap.get(rawMap);
if (!sourceDataArray) continue;
for (const sourceData of sourceDataArray) {
let data = moduleNameToSourceData.get(sourceData.source);
if (!data) {
data = [];
moduleNameToSourceData.set(sourceData.source, data);
}
data.push({
scriptId: script.scriptId,
scriptUrl: script.url,
resourceSize: sourceData.resourceSize,
});
}
}
this._normalizeAggregatedData(moduleNameToSourceData);
return moduleNameToSourceData;
}
}
const ModuleDuplicationComputed =
makeComputedArtifact(ModuleDuplication, ['Scripts', 'SourceMaps']);
export {ModuleDuplicationComputed as ModuleDuplication};