phantomas 2.9.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/program.js +1 -0
- package/extensions/devices/devices.js +3 -3
- package/extensions/postLoadDelay/postLoadDelay.js +4 -2
- package/lib/browser.js +7 -3
- package/lib/metadata/metadata.json +58 -19
- package/modules/analyzeImages/analyzeImages.js +154 -0
- package/modules/analyzeImages/scope.js +60 -0
- package/modules/domComplexity/domComplexity.js +0 -29
- package/modules/domComplexity/scope.js +0 -46
- package/modules/lazyLoadableImages/scope.js +7 -1
- package/package.json +4 -3
package/bin/program.js
CHANGED
|
@@ -131,6 +131,7 @@ function getProgram() {
|
|
|
131
131
|
|
|
132
132
|
// Output and reporting
|
|
133
133
|
.option("--analyze-css", "emit in-depth CSS metrics")
|
|
134
|
+
.option("--analyze-images", "emit in-depth image metrics")
|
|
134
135
|
.option("--colors", "forces ANSI colors even when output is piped")
|
|
135
136
|
.option(
|
|
136
137
|
"--film-strip",
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
|
|
6
6
|
module.exports = function (phantomas) {
|
|
7
7
|
const puppeteer = require("puppeteer"),
|
|
8
|
-
devices = puppeteer.
|
|
9
|
-
// @see https://github.com/
|
|
8
|
+
devices = puppeteer.KnownDevices,
|
|
9
|
+
// @see https://github.com/puppeteer/puppeteer/blob/main/packages/puppeteer-core/src/common/Device.ts
|
|
10
10
|
availableDevices = {
|
|
11
11
|
phone: "Galaxy S5", // 360x640
|
|
12
12
|
"phone-landscape": "Galaxy S5 landscape", // 640x360
|
|
@@ -14,7 +14,7 @@ module.exports = function (phantomas) {
|
|
|
14
14
|
"tablet-landscape": "Kindle Fire HDX landscape", // 1280x800
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
let device;
|
|
18
18
|
|
|
19
19
|
// check if --phone or --tablet option was passed
|
|
20
20
|
Object.keys(availableDevices).forEach(function (item) {
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
*/
|
|
4
4
|
"use strict";
|
|
5
5
|
|
|
6
|
+
const { setTimeout } = require("timers/promises");
|
|
7
|
+
|
|
6
8
|
module.exports = function (phantomas) {
|
|
7
9
|
// e.g. --post-load-delay 5
|
|
8
10
|
var delay = parseInt(phantomas.getParam("post-load-delay"), 10);
|
|
@@ -14,9 +16,9 @@ module.exports = function (phantomas) {
|
|
|
14
16
|
// https://github.com/GoogleChrome/puppeteer/blob/v1.11.0/docs/api.md#framewaitforselectororfunctionortimeout-options-args
|
|
15
17
|
phantomas.log("Will wait %d second(s) after load", delay);
|
|
16
18
|
|
|
17
|
-
phantomas.on("beforeClose", (
|
|
19
|
+
phantomas.on("beforeClose", async () => {
|
|
18
20
|
phantomas.log("Sleeping for %d seconds", delay);
|
|
19
21
|
|
|
20
|
-
return
|
|
22
|
+
return setTimeout(delay * 1000);
|
|
21
23
|
});
|
|
22
24
|
};
|
package/lib/browser.js
CHANGED
|
@@ -221,13 +221,17 @@ Browser.prototype.init = async (phantomasOptions) => {
|
|
|
221
221
|
const resp = await this.cdp.send("Network.getResponseBody", {
|
|
222
222
|
requestId: data.requestId,
|
|
223
223
|
});
|
|
224
|
+
|
|
225
|
+
// If the data is binary, decode from base64 and respond with a buffer
|
|
226
|
+
body = resp.base64Encoded
|
|
227
|
+
? Buffer.from(resp.body, "base64")
|
|
228
|
+
: resp.body;
|
|
229
|
+
|
|
224
230
|
networkDebug(
|
|
225
231
|
"Content for #%s received (%d bytes)",
|
|
226
232
|
data.requestId,
|
|
227
|
-
|
|
233
|
+
body.length
|
|
228
234
|
);
|
|
229
|
-
|
|
230
|
-
body = resp.body;
|
|
231
235
|
} catch (err) {
|
|
232
236
|
// In case the resource was dumped after a redirect
|
|
233
237
|
// https://github.com/puppeteer/puppeteer/issues/2258
|
|
@@ -243,6 +243,19 @@
|
|
|
243
243
|
"cssInlineStyles"
|
|
244
244
|
]
|
|
245
245
|
},
|
|
246
|
+
"analyzeImages": {
|
|
247
|
+
"file": "/modules/analyzeImages/analyzeImages.js",
|
|
248
|
+
"desc": "Adds Responsive Images metrics using analyze-images npm module.\nRun phantomas with --analyze-images option to use this module",
|
|
249
|
+
"events": [],
|
|
250
|
+
"metrics": [
|
|
251
|
+
"imagesWithoutDimensions",
|
|
252
|
+
"imagesNotOptimized",
|
|
253
|
+
"imagesScaledDown",
|
|
254
|
+
"imagesOldFormat",
|
|
255
|
+
"imagesExcessiveDensity",
|
|
256
|
+
"imagesWithIncorrectSizesParam"
|
|
257
|
+
]
|
|
258
|
+
},
|
|
246
259
|
"assetsTypes": {
|
|
247
260
|
"file": "/modules/assetsTypes/assetsTypes.js",
|
|
248
261
|
"desc": "Analyzes number of requests and sizes of different types of assets",
|
|
@@ -361,8 +374,6 @@
|
|
|
361
374
|
"DOMelementMaxDepth",
|
|
362
375
|
"nodesWithInlineCSS",
|
|
363
376
|
"iframesCount",
|
|
364
|
-
"imagesScaledDown",
|
|
365
|
-
"imagesWithoutDimensions",
|
|
366
377
|
"DOMidDuplicated"
|
|
367
378
|
]
|
|
368
379
|
},
|
|
@@ -977,6 +988,48 @@
|
|
|
977
988
|
"module": "analyzeCss",
|
|
978
989
|
"testsCovered": true
|
|
979
990
|
},
|
|
991
|
+
"imagesWithoutDimensions": {
|
|
992
|
+
"desc": "number of <img> nodes without both width and height attribute",
|
|
993
|
+
"offenders": true,
|
|
994
|
+
"unit": "number",
|
|
995
|
+
"module": "analyzeImages",
|
|
996
|
+
"testsCovered": true
|
|
997
|
+
},
|
|
998
|
+
"imagesNotOptimized": {
|
|
999
|
+
"desc": "number of loaded images that could be lighter it optimized",
|
|
1000
|
+
"offenders": true,
|
|
1001
|
+
"unit": "number",
|
|
1002
|
+
"module": "analyzeImages",
|
|
1003
|
+
"testsCovered": true
|
|
1004
|
+
},
|
|
1005
|
+
"imagesScaledDown": {
|
|
1006
|
+
"desc": "number of loaded images scaled down when displayed",
|
|
1007
|
+
"offenders": true,
|
|
1008
|
+
"unit": "number",
|
|
1009
|
+
"module": "analyzeImages",
|
|
1010
|
+
"testsCovered": true
|
|
1011
|
+
},
|
|
1012
|
+
"imagesOldFormat": {
|
|
1013
|
+
"desc": "number of loaded images that could benefit from new generation formats (WebP or AVIF)",
|
|
1014
|
+
"offenders": true,
|
|
1015
|
+
"unit": "number",
|
|
1016
|
+
"module": "analyzeImages",
|
|
1017
|
+
"testsCovered": true
|
|
1018
|
+
},
|
|
1019
|
+
"imagesExcessiveDensity": {
|
|
1020
|
+
"desc": "number of images that could be served smaller as the human eye can hardly see the difference",
|
|
1021
|
+
"offenders": true,
|
|
1022
|
+
"unit": "number",
|
|
1023
|
+
"module": "analyzeImages",
|
|
1024
|
+
"testsCovered": false
|
|
1025
|
+
},
|
|
1026
|
+
"imagesWithIncorrectSizesParam": {
|
|
1027
|
+
"desc": "number of responsive images with an improperly set sizes parameter",
|
|
1028
|
+
"offenders": true,
|
|
1029
|
+
"unit": "number",
|
|
1030
|
+
"module": "analyzeImages",
|
|
1031
|
+
"testsCovered": true
|
|
1032
|
+
},
|
|
980
1033
|
"htmlCount": {
|
|
981
1034
|
"desc": "number of HTML responses",
|
|
982
1035
|
"offenders": true,
|
|
@@ -1304,20 +1357,6 @@
|
|
|
1304
1357
|
"module": "domComplexity",
|
|
1305
1358
|
"testsCovered": true
|
|
1306
1359
|
},
|
|
1307
|
-
"imagesScaledDown": {
|
|
1308
|
-
"desc": "number of <img> nodes that have images scaled down in HTML",
|
|
1309
|
-
"offenders": true,
|
|
1310
|
-
"unit": "number",
|
|
1311
|
-
"module": "domComplexity",
|
|
1312
|
-
"testsCovered": true
|
|
1313
|
-
},
|
|
1314
|
-
"imagesWithoutDimensions": {
|
|
1315
|
-
"desc": "number of <img> nodes without both width and height attribute",
|
|
1316
|
-
"offenders": true,
|
|
1317
|
-
"unit": "number",
|
|
1318
|
-
"module": "domComplexity",
|
|
1319
|
-
"testsCovered": true
|
|
1320
|
-
},
|
|
1321
1360
|
"DOMidDuplicated": {
|
|
1322
1361
|
"desc": "number of duplicated IDs found in DOM",
|
|
1323
1362
|
"offenders": true,
|
|
@@ -1876,8 +1915,8 @@
|
|
|
1876
1915
|
"testsCovered": false
|
|
1877
1916
|
}
|
|
1878
1917
|
},
|
|
1879
|
-
"metricsCount":
|
|
1880
|
-
"modulesCount":
|
|
1918
|
+
"metricsCount": 191,
|
|
1919
|
+
"modulesCount": 37,
|
|
1881
1920
|
"extensionsCount": 14,
|
|
1882
|
-
"version": "2.
|
|
1921
|
+
"version": "2.9.0"
|
|
1883
1922
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adds Responsive Images metrics using analyze-images npm module.
|
|
3
|
+
*
|
|
4
|
+
* Run phantomas with --analyze-images option to use this module
|
|
5
|
+
*/
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
module.exports = function (phantomas) {
|
|
9
|
+
phantomas.setMetric("imagesWithoutDimensions"); // @desc number of <img> nodes without both width and height attribute @offenders
|
|
10
|
+
phantomas.setMetric("imagesNotOptimized"); // @desc number of loaded images that could be lighter it optimized @offenders
|
|
11
|
+
phantomas.setMetric("imagesScaledDown"); // @desc number of loaded images scaled down when displayed @offenders
|
|
12
|
+
phantomas.setMetric("imagesOldFormat"); // @desc number of loaded images that could benefit from new generation formats (WebP or AVIF) @offenders
|
|
13
|
+
phantomas.setMetric("imagesExcessiveDensity"); // @desc number of images that could be served smaller as the human eye can hardly see the difference @offenders
|
|
14
|
+
phantomas.setMetric("imagesWithIncorrectSizesParam"); // @desc number of responsive images with an improperly set sizes parameter @offenders
|
|
15
|
+
|
|
16
|
+
if (phantomas.getParam("analyze-images") !== true) {
|
|
17
|
+
phantomas.log(
|
|
18
|
+
"To enable images in-depth metrics please run phantomas with --analyze-images option"
|
|
19
|
+
);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const analyzer = require("analyze-image");
|
|
24
|
+
phantomas.log("Using version %s", analyzer.version);
|
|
25
|
+
|
|
26
|
+
async function analyzeImage(body, context) {
|
|
27
|
+
phantomas.log("Starting analyze-image on %j", context);
|
|
28
|
+
const results = await analyzer(body, context, {});
|
|
29
|
+
phantomas.log("Response from analyze-image: %j", results);
|
|
30
|
+
|
|
31
|
+
for (const offenderName in results.offenders) {
|
|
32
|
+
phantomas.log(
|
|
33
|
+
"Offender %s found: %j",
|
|
34
|
+
offenderName,
|
|
35
|
+
results.offenders[offenderName]
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const newOffenderName = offenderName.replace("image", "images");
|
|
39
|
+
phantomas.incrMetric(newOffenderName);
|
|
40
|
+
phantomas.addOffender(newOffenderName, {
|
|
41
|
+
url: context.url || shortenDataUri(context.inline),
|
|
42
|
+
...results.offenders[offenderName],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// prepare a list of images (both external and inline)
|
|
48
|
+
const images = [];
|
|
49
|
+
|
|
50
|
+
phantomas.on("recv", async (entry, res) => {
|
|
51
|
+
if (entry.isImage) {
|
|
52
|
+
images.push({
|
|
53
|
+
contentPromise: res.getContent, // defer getting the response content
|
|
54
|
+
url: entry.url,
|
|
55
|
+
htmlTags: [],
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
phantomas.on("base64recv", async (entry) => {
|
|
61
|
+
if (entry.isImage) {
|
|
62
|
+
images.push({
|
|
63
|
+
inline: entry.url,
|
|
64
|
+
htmlTags: [],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
phantomas.on("imgtag", (context) => {
|
|
70
|
+
phantomas.log("Image tag found: %j", context);
|
|
71
|
+
|
|
72
|
+
// If we previously found a network/inline request that matches the currentSrc, attach tag to it.
|
|
73
|
+
const correspondingResp = images.find(
|
|
74
|
+
(resp) =>
|
|
75
|
+
resp.url === context.currentSrc || resp.inline === context.currentSrc
|
|
76
|
+
);
|
|
77
|
+
if (correspondingResp) {
|
|
78
|
+
phantomas.log(
|
|
79
|
+
"Attached to previously found network image %s",
|
|
80
|
+
correspondingResp.url || "[inline]"
|
|
81
|
+
);
|
|
82
|
+
correspondingResp.htmlTags.push(context);
|
|
83
|
+
} else {
|
|
84
|
+
phantomas.log("Can't attach to previously found network image");
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// ok, now let's analyze the collected images
|
|
89
|
+
phantomas.on("beforeClose", () => {
|
|
90
|
+
const promises = [];
|
|
91
|
+
|
|
92
|
+
images.forEach((entry) => {
|
|
93
|
+
promises.push(
|
|
94
|
+
new Promise(async (resolve) => {
|
|
95
|
+
phantomas.log("Analyzing %s", entry.url || "inline image");
|
|
96
|
+
let imageBody;
|
|
97
|
+
|
|
98
|
+
if (entry.inline) {
|
|
99
|
+
imageBody = extractImageFromDataUri(entry.inline);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (entry.contentPromise) {
|
|
103
|
+
imageBody = await entry.contentPromise();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (imageBody && imageBody.length > 0) {
|
|
107
|
+
// If several img tags use the same source, then we only treat the largest one
|
|
108
|
+
// (because it's not a perf issue when an image is re-used on the page on a smaller size)
|
|
109
|
+
let largestTag;
|
|
110
|
+
|
|
111
|
+
entry.htmlTags.forEach((tag) => {
|
|
112
|
+
if (
|
|
113
|
+
!largestTag ||
|
|
114
|
+
tag.displayWidth * tag.displayHeight >
|
|
115
|
+
largestTag.displayWidth * largestTag.displayHeight
|
|
116
|
+
) {
|
|
117
|
+
largestTag = tag;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await analyzeImage(imageBody, {
|
|
122
|
+
url: entry.url,
|
|
123
|
+
inline: entry.inline,
|
|
124
|
+
...largestTag,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
resolve();
|
|
129
|
+
})
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return Promise.all(promises);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
function extractImageFromDataUri(str) {
|
|
137
|
+
const result = str.match(/^data:image\/[a-z+]*(?:;[a-z0-9]*)?,(.*)$/);
|
|
138
|
+
if (result) {
|
|
139
|
+
// Inline SVGs might be urlencoded
|
|
140
|
+
if (str.startsWith("data:image/svg+xml") && str.includes("%3Csvg")) {
|
|
141
|
+
return decodeURIComponent(result[1]);
|
|
142
|
+
}
|
|
143
|
+
return result[1];
|
|
144
|
+
}
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function shortenDataUri(str) {
|
|
149
|
+
if (str.length > 100) {
|
|
150
|
+
return str.substring(0, 50) + " [...] " + str.substring(str.length - 50);
|
|
151
|
+
}
|
|
152
|
+
return str;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
(function analyzeImageScope(phantomas) {
|
|
2
|
+
window.addEventListener("load", function () {
|
|
3
|
+
phantomas.spyEnabled(false, "Checking images");
|
|
4
|
+
const images = document.querySelectorAll("img");
|
|
5
|
+
phantomas.spyEnabled(true);
|
|
6
|
+
|
|
7
|
+
images.forEach((node) => {
|
|
8
|
+
var imgWidth = node.hasAttribute("width")
|
|
9
|
+
? parseInt(node.getAttribute("width"), 10)
|
|
10
|
+
: false,
|
|
11
|
+
imgHeight = node.hasAttribute("height")
|
|
12
|
+
? parseInt(node.getAttribute("height"), 10)
|
|
13
|
+
: false;
|
|
14
|
+
|
|
15
|
+
// get dimensions from inline CSS (issue #399)
|
|
16
|
+
if (imgWidth === false || imgHeight === false) {
|
|
17
|
+
imgWidth = parseInt(node.style.width, 10) || false;
|
|
18
|
+
imgHeight = parseInt(node.style.height, 10) || false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (imgWidth === false || imgHeight === false) {
|
|
22
|
+
phantomas.incrMetric("imagesWithoutDimensions");
|
|
23
|
+
phantomas.addOffender("imagesWithoutDimensions", {
|
|
24
|
+
path: phantomas.getDOMPath(node, true /* dontGoUpTheDom */),
|
|
25
|
+
src: node.currentSrc,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const html =
|
|
30
|
+
node.parentNode.tagName === "PICTURE"
|
|
31
|
+
? node.parentNode.outerHTML
|
|
32
|
+
: node.outerHTML;
|
|
33
|
+
|
|
34
|
+
// Check if the image or one of its parents is in display:none
|
|
35
|
+
// If it is the case, node.width and node.height values are not reliable.
|
|
36
|
+
// https://stackoverflow.com/a/53068496/4716391
|
|
37
|
+
const isVisible = !!node.offsetParent;
|
|
38
|
+
phantomas.log(
|
|
39
|
+
"analyzeImg: ignoring displayWidth and displayHeight because image is not visible"
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (node.currentSrc) {
|
|
43
|
+
phantomas.emit("imgtag", {
|
|
44
|
+
html: html,
|
|
45
|
+
displayWidth: isVisible ? node.width : undefined,
|
|
46
|
+
displayHeight: isVisible ? node.height : undefined,
|
|
47
|
+
viewportWidth: window.innerWidth,
|
|
48
|
+
viewportHeight: window.innerHeight,
|
|
49
|
+
currentSrc: node.currentSrc,
|
|
50
|
+
dpr: window.devicePixelRatio,
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
phantomas.log(
|
|
54
|
+
"analyzeImg: image tag found without currentSrc: %s",
|
|
55
|
+
html
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
})(window.__phantomas);
|
|
@@ -21,35 +21,6 @@ module.exports = function (phantomas) {
|
|
|
21
21
|
|
|
22
22
|
phantomas.setMetric("iframesCount"); // @desc number of iframe nodes @offenders
|
|
23
23
|
|
|
24
|
-
// images
|
|
25
|
-
// TODO: move to a separate module
|
|
26
|
-
phantomas.setMetric("imagesScaledDown"); // @desc number of <img> nodes that have images scaled down in HTML @offenders
|
|
27
|
-
phantomas.setMetric("imagesWithoutDimensions"); // @desc number of <img> nodes without both width and height attribute @offenders
|
|
28
|
-
|
|
29
|
-
// keep the track of SVG graphics (#479)
|
|
30
|
-
var svgResources = [];
|
|
31
|
-
phantomas.on("recv", (entry) => {
|
|
32
|
-
if (entry.isSVG) {
|
|
33
|
-
svgResources.push(entry.url);
|
|
34
|
-
phantomas.log(
|
|
35
|
-
"imagesScaledDown: will ignore <%s> [%s]",
|
|
36
|
-
entry.url,
|
|
37
|
-
entry.contentType
|
|
38
|
-
);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
phantomas.on("imagesScaledDown", (image) => {
|
|
43
|
-
if (svgResources.indexOf(image.url) === -1) {
|
|
44
|
-
phantomas.log("Scaled down image: %j", image);
|
|
45
|
-
|
|
46
|
-
phantomas.incrMetric("imagesScaledDown");
|
|
47
|
-
phantomas.addOffender("imagesScaledDown", image);
|
|
48
|
-
} else {
|
|
49
|
-
phantomas.log("imagesScaledDown: ignored <%s> (is SVG)", image.url);
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
24
|
// duplicated ID (issue #392)
|
|
54
25
|
phantomas.setMetric("DOMidDuplicated"); // @desc number of duplicated IDs found in DOM
|
|
55
26
|
|
|
@@ -75,52 +75,6 @@
|
|
|
75
75
|
return false;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
// images
|
|
79
|
-
if (node.nodeName === "IMG") {
|
|
80
|
-
var imgWidth = node.hasAttribute("width")
|
|
81
|
-
? parseInt(node.getAttribute("width"), 10)
|
|
82
|
-
: false,
|
|
83
|
-
imgHeight = node.hasAttribute("height")
|
|
84
|
-
? parseInt(node.getAttribute("height"), 10)
|
|
85
|
-
: false;
|
|
86
|
-
|
|
87
|
-
// get dimensions from inline CSS (issue #399)
|
|
88
|
-
if (imgWidth === false || imgHeight === false) {
|
|
89
|
-
imgWidth = parseInt(node.style.width, 10) || false;
|
|
90
|
-
imgHeight = parseInt(node.style.height, 10) || false;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (imgWidth === false || imgHeight === false) {
|
|
94
|
-
phantomas.incrMetric("imagesWithoutDimensions");
|
|
95
|
-
phantomas.addOffender(
|
|
96
|
-
"imagesWithoutDimensions",
|
|
97
|
-
"%s <%s>",
|
|
98
|
-
path,
|
|
99
|
-
node.src
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
node.naturalHeight &&
|
|
105
|
-
node.naturalWidth &&
|
|
106
|
-
imgHeight &&
|
|
107
|
-
imgWidth
|
|
108
|
-
) {
|
|
109
|
-
if (
|
|
110
|
-
node.naturalHeight > imgHeight ||
|
|
111
|
-
node.naturalWidth > imgWidth
|
|
112
|
-
) {
|
|
113
|
-
phantomas.emit("imagesScaledDown", {
|
|
114
|
-
url: node.src,
|
|
115
|
-
naturalWidth: node.naturalWidth,
|
|
116
|
-
naturalHeight: node.naturalHeight,
|
|
117
|
-
imgWidth,
|
|
118
|
-
imgHeight,
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
78
|
break;
|
|
125
79
|
|
|
126
80
|
case Node.TEXT_NODE:
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
len = images.length,
|
|
13
13
|
offset,
|
|
14
14
|
path,
|
|
15
|
+
native,
|
|
15
16
|
processedImages = {},
|
|
16
17
|
src,
|
|
17
18
|
viewportHeight = window.innerHeight;
|
|
@@ -30,6 +31,9 @@
|
|
|
30
31
|
// @see https://stackoverflow.com/questions/35586728/detect-used-srcset-or-picture-tag-source-with-javascript
|
|
31
32
|
src = images[i].currentSrc;
|
|
32
33
|
|
|
34
|
+
// Chrome headless loads images with native lazyloading, therefore we need to filter by ourself.
|
|
35
|
+
native = images[i].loading === "lazy";
|
|
36
|
+
|
|
33
37
|
// ignore base64-encoded images
|
|
34
38
|
if (src === null || src === "" || /^data:/.test(src)) {
|
|
35
39
|
continue;
|
|
@@ -42,6 +46,7 @@
|
|
|
42
46
|
processedImages[src] = {
|
|
43
47
|
offset: offset,
|
|
44
48
|
path: path,
|
|
49
|
+
native: native,
|
|
45
50
|
};
|
|
46
51
|
}
|
|
47
52
|
|
|
@@ -50,6 +55,7 @@
|
|
|
50
55
|
processedImages[src] = {
|
|
51
56
|
offset: offset,
|
|
52
57
|
path: path,
|
|
58
|
+
native: native,
|
|
53
59
|
};
|
|
54
60
|
}
|
|
55
61
|
}
|
|
@@ -62,7 +68,7 @@
|
|
|
62
68
|
Object.keys(processedImages).forEach((src) => {
|
|
63
69
|
var img = processedImages[src];
|
|
64
70
|
|
|
65
|
-
if (img.offset > viewportHeight) {
|
|
71
|
+
if (img.offset > viewportHeight && !img.native) {
|
|
66
72
|
phantomas.log(
|
|
67
73
|
"lazyLoadableImages: <%s> image (%s) is below the fold (at %dpx)",
|
|
68
74
|
src,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "phantomas",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"author": "macbre <maciej.brencz@gmail.com> (http://macbre.net)",
|
|
5
5
|
"description": "Headless Chromium-based web performance metrics collector and monitoring tool",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -27,13 +27,14 @@
|
|
|
27
27
|
"node": ">=16.0"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"analyze-css": "^2.
|
|
30
|
+
"analyze-css": "^2.1.89",
|
|
31
|
+
"analyze-image": "^1.0.0",
|
|
31
32
|
"commander": "^9.0.0",
|
|
32
33
|
"debug": "^4.1.1",
|
|
33
34
|
"decamelize": "^5.0.0",
|
|
34
35
|
"fast-stats": "0.0.6",
|
|
35
36
|
"js-yaml": "^4.0.0",
|
|
36
|
-
"puppeteer": "^
|
|
37
|
+
"puppeteer": "^22.4.1"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@jest/globals": "^28.0.0",
|