imgtune 0.2.0 → 0.3.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/CHANGELOG.md +98 -0
- package/README.md +69 -6
- package/dist/audit.js +1 -0
- package/dist/backgrounds.js +31 -7
- package/dist/broken.js +5 -1
- package/dist/browser.js +4 -4
- package/dist/candidates.js +25 -0
- package/dist/check-report.js +37 -4
- package/dist/check.js +18 -1
- package/dist/cli.js +20 -0
- package/dist/egress-proxy.js +2 -1
- package/dist/ffmpeg.js +35 -0
- package/dist/hints.js +294 -18
- package/dist/image-open.js +9 -0
- package/dist/images.js +116 -7
- package/dist/layout.js +7 -7
- package/dist/lcp.js +2 -1
- package/dist/measure.js +39 -1
- package/dist/media-encode.js +5 -1
- package/dist/media-model.js +8 -4
- package/dist/media-phase.js +25 -2
- package/dist/px.js +11 -0
- package/dist/recheck.js +9 -1
- package/dist/remote.js +616 -63
- package/dist/replay.js +20 -0
- package/dist/resources/audio.js +248 -0
- package/dist/resources/brotli.js +22 -0
- package/dist/resources/cache.js +59 -0
- package/dist/resources/capture.js +225 -0
- package/dist/resources/compression.js +122 -0
- package/dist/resources/favicon.js +130 -0
- package/dist/resources/font.js +440 -0
- package/dist/resources/index.js +8 -0
- package/dist/resources/lottie.js +491 -0
- package/dist/resources/model.js +478 -0
- package/dist/resources/phase.js +171 -0
- package/dist/resources/pool.js +47 -0
- package/dist/resources/report.js +57 -0
- package/dist/resources/sfnt.js +305 -0
- package/dist/resources/svg.js +178 -0
- package/dist/resources/types.js +1 -0
- package/dist/resources/zip.js +67 -0
- package/dist/run.js +24 -4
- package/dist/sizes.js +7 -5
- package/dist/summary.js +47 -1
- package/dist/vector-art.js +294 -0
- package/dist/vector-phase.js +222 -0
- package/dist/video-analysis.js +119 -21
- package/package.json +25 -4
- package/skills/imgtune/SKILL.md +8 -4
- package/skills/imgtune/reference.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,103 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 (date TBD)
|
|
4
|
+
|
|
5
|
+
- Page weight, not only images. `scan` now reports what else a page downloads, each with its bytes per view and a fix an
|
|
6
|
+
agent can apply: images and video that are loaded but never shown, fonts, compression, favicons, vector art, SVG, Lottie
|
|
7
|
+
files, 3D models and audio. Images and video stay the headline. The positioning is in the README, the site and the skill.
|
|
8
|
+
- Loaded but not shown: images and videos. `scan` reports the images and `<video>` elements the browser downloads but no
|
|
9
|
+
viewport shows: `display:none`, `visibility:hidden`, `opacity: 0`, a zero-size box, an offscreen slide of a track, a closed
|
|
10
|
+
`<details>`, `content-visibility: hidden`, a hover swap, a tab panel, and a CSS background layer whose file a hidden element
|
|
11
|
+
loaded. A copy counts as hidden only if it reads hidden on two reads at least 1 s apart and again after a scroll; anything
|
|
12
|
+
that toggles (a carousel, a crossfade, a reveal) counts as shown, and a copy seen visible at any step of the scroll pass is
|
|
13
|
+
shown. Each case gets its own fix: a `<picture>` with a media condition for a responsive twin, `loading="lazy"` or render on
|
|
14
|
+
activation for a carousel, tab or hover copy, and removal or an on-demand load for an image no viewport shows on load. A
|
|
15
|
+
hidden copy's bytes are kept apart from the savings, and it gets no sizes, quality or encode claim at the viewports that hide
|
|
16
|
+
it. An image that already has `loading="lazy"` is no longer advised to get it. The JSON gains `images[].hidden` and
|
|
17
|
+
`videos[].hidden`. See the Hidden images guide.
|
|
18
|
+
- Video: the claim is per viewport. A `<video>`'s per-view bytes are counted at each viewport (what that viewport loads) and
|
|
19
|
+
averaged over the viewports, the same as images. A desktop and a mobile copy of one video count once. A video that only some
|
|
20
|
+
viewports load therefore claims less than it did in 0.2. A video no viewport shows is never encoded (`never-shown`).
|
|
21
|
+
- `<source media>` support is stated as reported in the hints: Chrome and Edge 120 or later, Firefox 120 or later, Safari
|
|
22
|
+
(version not verified).
|
|
23
|
+
- Fonts. `scan` reports each web font file's bytes per view and its savings: a re-encode to WOFF2 (measured for small files,
|
|
24
|
+
estimated for large ones), a subset to the Unicode scripts the page's text uses (the subset keeps Basic Latin, Latin-1,
|
|
25
|
+
punctuation and currency), and faces the browser downloaded but no rendered text uses. Icon fonts are not subset, and a
|
|
26
|
+
family of several static weights is noted as a candidate for one variable font, with no saving. Each finding names the
|
|
27
|
+
command and the expected bytes. See the Fonts guide.
|
|
28
|
+
- Compression. Text served with no `Content-Encoding` (JavaScript, CSS, HTML, JSON, XML, SVG, WASM, TTF and OTF, at least 1 KB)
|
|
29
|
+
is reported with its brotli-11 saving, grouped per host and content type. The fix is the server or CDN setting. See the
|
|
30
|
+
Compression guide.
|
|
31
|
+
- Favicons. A favicon over 10 KB, or an apple-touch-icon over 30 KB, is reported with the saving of a re-encode to 32 px or
|
|
32
|
+
180 px. Each icon is fetched once per run. See the Favicons guide.
|
|
33
|
+
- Vector art. A still raster that is flat (a few colours cover most of it: logos, icons, flat illustrations) gets a vector
|
|
34
|
+
check. `scan` traces the heaviest such images (`--svg-budget`, default 50, 0 to 1000; 0 traces none) with vtracer, through
|
|
35
|
+
the optional `@neplex/vectorizer` dependency, and renders each SVG back at the served size. The fidelity is SSIMULACRA2 at
|
|
36
|
+
half size against the served image (bar 70). A passing SVG must be at least 30% smaller than the served file, and its saving
|
|
37
|
+
is the record's claim when it is the larger one; the raster fix stays as the fallback. The traced file is written to
|
|
38
|
+
`svg/<id>.svg` in the scan folder. Without the vectorizer, a flat image gets one note and no byte claim. `fix.vector` in the
|
|
39
|
+
JSON, a `vector` field on hints, and the summary and the check report count the flat and traced images. See the Vector art
|
|
40
|
+
guide.
|
|
41
|
+
- SVG files. Each SVG of 2 KB to 5 MB is optimised with SVGO (floatPrecision 2) and reported when the brotli size drops by at
|
|
42
|
+
least 20% and the render still scores 70 or more on SSIMULACRA2 at half size. SVGs with embedded `data:` rasters are listed.
|
|
43
|
+
Inline `<svg>` is not checked.
|
|
44
|
+
- Lottie animations. `scan` reads each Lottie JSON and `.lottie` file the page loads: the size, layers, frame rate and duration,
|
|
45
|
+
and the savings: embedded rasters re-encoded to WebP at the display size, numbers rounded to three decimals, and a `.lottie`
|
|
46
|
+
repack (an estimate). A huge animation shown small gets a hint to export at the display size. See the Lottie guide.
|
|
47
|
+
- 3D models. `scan` checks glTF and GLB files: the geometry saving with meshopt is measured (the file is repacked with
|
|
48
|
+
gltf-transform), base64 buffers get the GLB repack, and PNG or JPEG textures get a WebP estimate. Draco and meshopt files are
|
|
49
|
+
reported as compressed. USDZ, OBJ, FBX, KTX2, splats and PLY are informational. See the 3D models guide.
|
|
50
|
+
- Audio. `scan` probes each audio file with ffprobe. WAV and FLAC, lossy files above 128 kbps stereo (64 kbps mono), and stereo
|
|
51
|
+
files with identical channels get an Opus re-encode, measured on a 10 s sample. A file the page loads on page load gets the
|
|
52
|
+
`preload="none"` fix. Without ffmpeg only the bytes are reported. See the Audio guide.
|
|
53
|
+
- Resources, as one group. The non-image checks above run on every response the page makes, and their findings are reported in
|
|
54
|
+
a Resources section (after Video), in the JSON as `resources`, and in a Resources block of the PR comment. They are not in
|
|
55
|
+
the image totals. A file on another site is labelled "third-party (ask the vendor)" and totalled apart from first-party
|
|
56
|
+
savings; a small saving of a kind that is weak for its cost (SVG, Lottie, favicons) is a "lead" and sorts after the real
|
|
57
|
+
savings. The heavy steps (brotli-11 sizes, SVGO and render, WOFF2 and subset encodes, Lottie re-encodes, 3D meshopt, audio
|
|
58
|
+
sample encodes) are computed once per run for each distinct content, not once per viewport and page. `--resource-time <s>`
|
|
59
|
+
(default 60) caps the heavy work of a run; a file over the budget keeps its cheap finding, marked as an estimate or not
|
|
60
|
+
measured. The resources overview is in the guide.
|
|
61
|
+
- A plain `<img>` with no `srcset`, served much wider than it is shown (from a same-origin file or a CDN host no adapter knows),
|
|
62
|
+
gets resized variants instead of a `sizes` change. `sizes` does nothing without `srcset`. The hint says to serve the variants
|
|
63
|
+
at concrete widths (1 to 3, from the device pixels each viewport needs) and to add the `srcset` and `sizes`. The summary's top
|
|
64
|
+
group reads `no srcset -> serve <widths> + srcset/sizes`, and the saving is booked as `resize`, not `quality`. Known loaders and
|
|
65
|
+
pages with Next.js detected keep their advice. The same resize booking applies to CSS background layers. The JSON gains
|
|
66
|
+
`bytes.resize` and `srcset`.
|
|
67
|
+
- Measuring browser: `--disable-software-rasterizer` is no longer passed to Chromium (`--disable-gpu` stays). With it WebGL was
|
|
68
|
+
off, so a page with a 3D viewer never requested its model. Only WebGL pages pay the extra CPU.
|
|
69
|
+
- Flags. `--no-resources` skips the resource checks and their capture (`scan`, `check`, `run` and `audit`). A page scanned that
|
|
70
|
+
way is recorded with `resources: false`. `--resource-time <s>` (`scan`, `check`, `run`, `audit`; default 60, 0 to 3600) caps
|
|
71
|
+
the heavy resource work. `--svg-budget <N>` (`scan`, `check`, `run`, `audit`; default 50, 0 to 1000) sets how many flat images
|
|
72
|
+
are traced.
|
|
73
|
+
- Optional dependencies. `@neplex/vectorizer` (0.1.0) is used for the vector trace. `@gltf-transform/core`,
|
|
74
|
+
`@gltf-transform/extensions`, `@gltf-transform/functions` and `meshoptimizer` are used for the 3D measurements. Each is loaded
|
|
75
|
+
when a check needs it. If one is missing, its check gives one note and no measured saving; the rest of the scan runs.
|
|
76
|
+
- Smaller fixes: a hidden copy's hints carry its URL and its bytes per viewport (`revealCapped` marks candidates past the
|
|
77
|
+
per-viewport limit that were not re-read after a scroll); a `<details>` closed on load is not counted as shown. A failed
|
|
78
|
+
linked stylesheet makes a page `broken` only when no linked stylesheet applied (otherwise it is a warning); a challenge page
|
|
79
|
+
(WAF or Cloudflare) is `blocked` whatever its images; a search the time limit cut short is labelled "est., search stopped at
|
|
80
|
+
the time limit"; a page that reloads itself is read between its reloads.
|
|
81
|
+
|
|
82
|
+
### Changed for 0.2 users
|
|
83
|
+
|
|
84
|
+
- The JSON gains `resources` (a page file's findings, present unless the scan ran with `--no-resources`), `images[].hidden` and
|
|
85
|
+
`videos[].hidden`, `fix.vector` on a still raster that looks like vector art, and `bytes.resize` (with `srcset`) on a plain
|
|
86
|
+
image with no `srcset`. Existing fields keep their meaning, except as noted below.
|
|
87
|
+
- Video numbers change: the per-view claim is averaged over the viewports (see above), so a video loaded at some viewports only
|
|
88
|
+
claims less than in 0.2. Compare the `videos[].bytes.perView` of a 0.2 run with a 0.3 run only with that in mind.
|
|
89
|
+
- Plain images with no `srcset`: the saving moves from `quality` (and `sizes`) to `resize`, so the quality total of a page can
|
|
90
|
+
fall by the same amount. The hint asks for resized variants, not a `sizes` change.
|
|
91
|
+
- Hidden images and videos used to be dropped silently. They are now reported, in their own lines and in the JSON, and kept out
|
|
92
|
+
of the savings, so a page's totals can differ from 0.2 even when no image changed.
|
|
93
|
+
- The Chromium flags changed: WebGL is on for the scan (see above). A page with a WebGL viewer may now load more bytes than it
|
|
94
|
+
did in 0.2, and those bytes are reported.
|
|
95
|
+
- The result cache: `CACHE_VERSION` is still 3, but the cache keys include the imgtune version, so 0.2 entries are not reused and
|
|
96
|
+
the first 0.3 scan is cold.
|
|
97
|
+
- New flags: `--no-resources`, `--resource-time` and `--svg-budget`. See the CLI reference.
|
|
98
|
+
- New optional dependencies: `@neplex/vectorizer`, `@gltf-transform/core`, `@gltf-transform/extensions`, `@gltf-transform/functions`
|
|
99
|
+
and `meshoptimizer`. They install with the package as optional dependencies; none is required.
|
|
100
|
+
|
|
3
101
|
## 0.2.0 (2026-11-14)
|
|
4
102
|
|
|
5
103
|
- Animated media and video. `scan` measures animated GIFs (in an `<img>` or a CSS background), animated WebP and APNG, and
|
package/README.md
CHANGED
|
@@ -2,20 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/imgtune)
|
|
4
4
|
|
|
5
|
-
**
|
|
5
|
+
**imgtune measures page weight on any live site: images first, and everything else the page downloads.**
|
|
6
6
|
|
|
7
7
|
Website: https://indicozy.github.io/imgtune/ · [Try the demo](https://indicozy.github.io/imgtune/demo) · [Docs](https://indicozy.github.io/imgtune/guide/getting-started)
|
|
8
8
|
|
|
9
|
-
Lighthouse tells you your
|
|
10
|
-
points at the line of code that renders it (when your dev server is running), and checks each quality target with
|
|
11
|
-
SSIMULACRA2. Your coding agent applies the change, and a rescan checks it.
|
|
9
|
+
Lighthouse tells you your page is heavy. imgtune tells you which bytes to cut and how: the `sizes`, quality and format for each image, the codec and width for each video, images and videos that load but are never shown, fonts to subset or drop, files served without compression, and 3D models, audio, Lottie and SVG that could be smaller. Each fix comes with the expected bytes and, for images and video with your dev server running, the line of code to change. Your coding agent applies it, and a rescan checks it.
|
|
12
10
|
|
|
13
11
|
**Works with your image CDN.** Cloudinary, imgix and `next/image` resize to whatever width `sizes` asks for, so a wrong
|
|
14
12
|
`sizes` is waste that no CDN removes. ImgTune works on top of a CDN, not instead of one. Already on Cloudinary auto
|
|
15
13
|
quality (`q_auto`)? ImgTune focuses on `sizes`, which no CDN can fix for you. A format the CDN picks per request
|
|
16
14
|
(Cloudinary `f_auto`, imgix `auto=format`) is not proposed as a change.
|
|
17
15
|
|
|
18
|
-
**Built for
|
|
16
|
+
**Built for heavy sites:** e-commerce catalogs, media, marketplaces, and marketing pages with video and 3D.
|
|
17
|
+
|
|
18
|
+
**What it checks**, one guide each:
|
|
19
|
+
|
|
20
|
+
- [Images](https://indicozy.github.io/imgtune/guide/how-it-measures)
|
|
21
|
+
- [Video and animated images](https://indicozy.github.io/imgtune/guide/video-and-gifs)
|
|
22
|
+
- [Loaded but not shown](https://indicozy.github.io/imgtune/guide/hidden-images)
|
|
23
|
+
- [Fonts](https://indicozy.github.io/imgtune/guide/fonts)
|
|
24
|
+
- [Compression](https://indicozy.github.io/imgtune/guide/compression)
|
|
25
|
+
- [Vector art (raster to SVG)](https://indicozy.github.io/imgtune/guide/vector-art)
|
|
26
|
+
- [SVG](https://indicozy.github.io/imgtune/guide/svg)
|
|
27
|
+
- [Lottie](https://indicozy.github.io/imgtune/guide/lottie)
|
|
28
|
+
- [3D models](https://indicozy.github.io/imgtune/guide/3d-models)
|
|
29
|
+
- [Audio](https://indicozy.github.io/imgtune/guide/audio)
|
|
30
|
+
- [Favicons](https://indicozy.github.io/imgtune/guide/favicons)
|
|
19
31
|
|
|
20
32
|
## Two ways to measure
|
|
21
33
|
|
|
@@ -129,6 +141,14 @@ desktop at 1x), scrolls it to the end, and records every image.
|
|
|
129
141
|
- Production check: `imgtune check` (see [Two ways to measure](#two-ways-to-measure)) and `imgtune audit` do the same
|
|
130
142
|
survey and deep scan for a whole site, with no source needed for `check`.
|
|
131
143
|
|
|
144
|
+
## Resources
|
|
145
|
+
|
|
146
|
+
`scan` also reads the page's other responses (not images) and runs seven checks on them: compression, favicons, fonts,
|
|
147
|
+
SVG, Lottie, 3D models and audio. They are reported on their own, not in the image totals. Guides:
|
|
148
|
+
[compression](https://indicozy.github.io/imgtune/guide/compression), [favicons](https://indicozy.github.io/imgtune/guide/favicons), [fonts](https://indicozy.github.io/imgtune/guide/fonts), [SVG](https://indicozy.github.io/imgtune/guide/svg), [Lottie](https://indicozy.github.io/imgtune/guide/lottie),
|
|
149
|
+
[3D models](https://indicozy.github.io/imgtune/guide/3d-models) and [audio](https://indicozy.github.io/imgtune/guide/audio). The [resources overview](https://indicozy.github.io/imgtune/guide/resources) covers the labels, the budget
|
|
150
|
+
and the flags: `--no-resources` turns all seven off, and `--resource-time` caps the heavy work.
|
|
151
|
+
|
|
132
152
|
## The evidence ladder
|
|
133
153
|
|
|
134
154
|
Every fix hint is labelled with the strength of its evidence, the rung it stands on:
|
|
@@ -143,6 +163,35 @@ Every fix hint is labelled with the strength of its evidence, the rung it stands
|
|
|
143
163
|
A ceiling is never derived from the existing `sizes` value. When a usage is measured, its `sizes` comes from the layout
|
|
144
164
|
the browser resolved, not from a guess.
|
|
145
165
|
|
|
166
|
+
## Lottie and SVG
|
|
167
|
+
|
|
168
|
+
`scan` also reads the Lottie animations and the SVG files a page loads. A Lottie report gives the size, layers, frame rate and
|
|
169
|
+
duration, and its savings: embedded rasters re-encoded to WebP at the display size, a `.lottie` repack (an estimate), and
|
|
170
|
+
numbers rounded to three decimals. An SVG is optimised with SVGO and reported when the brotli size drops by 20% or more and
|
|
171
|
+
the render still scores 70 or more on SSIMULACRA2 at half size. Guides: [Lottie](https://indicozy.github.io/imgtune/guide/lottie) and [SVG](https://indicozy.github.io/imgtune/guide/svg).
|
|
172
|
+
|
|
173
|
+
## 3D models and audio
|
|
174
|
+
|
|
175
|
+
`scan` also reads the 3D models and audio files a page loads. A glTF or GLB gets a measured meshopt saving (the file is
|
|
176
|
+
repacked with gltf-transform), base64 buffers get the GLB repack, and PNG or JPEG textures get a WebP estimate. An audio
|
|
177
|
+
file that is lossless, above 128 kbps stereo, or stereo with identical channels gets an Opus re-encode, measured on a 10 s
|
|
178
|
+
sample with ffmpeg. Guides: [3D models](https://indicozy.github.io/imgtune/guide/3d-models) and [audio](https://indicozy.github.io/imgtune/guide/audio).
|
|
179
|
+
|
|
180
|
+
## Vector art
|
|
181
|
+
|
|
182
|
+
A flat logo, icon or illustration served as PNG, WebP or JPEG can often be an SVG: smaller, and sharp at every density.
|
|
183
|
+
`scan` traces the heaviest flat images (`--svg-budget`, default 50) with an optional dependency, `@neplex/vectorizer`,
|
|
184
|
+
renders each SVG back, and keeps it only when it scores at least 70 at half size and is at least 30% smaller. A passing
|
|
185
|
+
trace is written to `svg/` in the scan folder. Without the dependency, a flat image gets one note and no byte claim. See [the guide](https://indicozy.github.io/imgtune/guide/vector-art).
|
|
186
|
+
|
|
187
|
+
## Loaded but not shown
|
|
188
|
+
|
|
189
|
+
`scan` also finds the images the browser downloads without showing: a desktop image hidden on phones (or the reverse), an
|
|
190
|
+
offscreen slide of a carousel, a tab panel, an element with `opacity: 0` or `visibility: hidden`, and a zero-size box. Their
|
|
191
|
+
bytes per view are reported on their own line ("loaded but not shown") and kept out of the savings. Each one gets the fix
|
|
192
|
+
for its case: a `<picture>` with a media condition for a responsive twin, `loading="lazy"` or render-on-activation for a
|
|
193
|
+
carousel, tab or hover copy, and an on-demand load or removal for an image no viewport shows on load. See [the guide](https://indicozy.github.io/imgtune/guide/hidden-images).
|
|
194
|
+
|
|
146
195
|
## GIFs and video
|
|
147
196
|
|
|
148
197
|
`scan` also measures animated media: animated GIFs in an `<img>` or a CSS background, animated WebP and APNG, and
|
|
@@ -165,7 +214,8 @@ self-hosted `<video>` elements with a plain file source. Static GIFs get the nor
|
|
|
165
214
|
the animated-image alternative with its size. A small `<img>` GIF or a CSS background gets an animated AVIF or WebP. A
|
|
166
215
|
`<video>` hint names each lever: the width per breakpoint (`<source media>`), the CRF, the codec, the audio track of a
|
|
167
216
|
muted video, and `preload`. A poster is audited as an image. Duration and frame rate are informational only.
|
|
168
|
-
- **Bytes: per view and per play.** Per view is
|
|
217
|
+
- **Bytes: per view and per play.** Per view is counted at each viewport and averaged over the viewports, as for images. An
|
|
218
|
+
autoplay video counts its full file,
|
|
169
219
|
`preload="auto"` counts the bytes the scan saw (browser-dependent), and `preload="metadata"` or `"none"` counts 0. Per
|
|
170
220
|
play is the full file. Both are shown for every video, so a large click-to-play file is not missed. Animated images stay
|
|
171
221
|
in the image totals; `<video>` has its own Video section, and posters count as images.
|
|
@@ -183,6 +233,19 @@ self-hosted `<video>` elements with a plain file source. Static GIFs get the nor
|
|
|
183
233
|
- **Check an agent's output** with `imgtune image <candidate> --reference <original>`: the same VMAF and still-frame bar,
|
|
184
234
|
exit 0 on pass and 1 on fail (see the table below).
|
|
185
235
|
|
|
236
|
+
## Fonts
|
|
237
|
+
|
|
238
|
+
`scan` reports each web font file the page downloads: its bytes per view, and what it could save.
|
|
239
|
+
|
|
240
|
+
- **WOFF2:** a TTF, OTF or WOFF that is not WOFF2 is re-encoded (measured for small files, estimated for large ones).
|
|
241
|
+
- **Subsetting:** a font with far more glyphs than the page's scripts need (the Unicode blocks its text uses, plus Basic
|
|
242
|
+
Latin, Latin-1, punctuation and currency) is subset to them. The fix gives the `pyftsubset` command.
|
|
243
|
+
- **Unused faces:** a face the browser downloaded but no rendered text uses is wasted bytes. Icon fonts, and faces the
|
|
244
|
+
browser rendered with (even as a fallback), are never called unused.
|
|
245
|
+
- **Static weights:** three or more static faces of one family are noted as a candidate for one variable font (no saving).
|
|
246
|
+
|
|
247
|
+
See [the Fonts guide](https://indicozy.github.io/imgtune/guide/fonts).
|
|
248
|
+
|
|
186
249
|
## Commands
|
|
187
250
|
|
|
188
251
|
| command | what it does |
|
package/dist/audit.js
CHANGED
|
@@ -232,6 +232,7 @@ async function preflight(prod, settings, outRoot, cwd, log, storageState) {
|
|
|
232
232
|
ignoreRobots: settings.ignoreRobots,
|
|
233
233
|
aggressive: false,
|
|
234
234
|
quality: false,
|
|
235
|
+
resources: false,
|
|
235
236
|
thirdParty: settings.thirdParty,
|
|
236
237
|
outDir: join(outRoot, "preflight"),
|
|
237
238
|
viewports: viewportsOf(plan.viewports),
|
package/dist/backgrounds.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Pure parsing and sizing here; the measurement (fetches, decodes, resize estimates) is in remote.ts, and the records are
|
|
7
7
|
// built in buildBackgroundRecords below.
|
|
8
8
|
import { sha1 } from "./atomic.js";
|
|
9
|
-
import { losslessServed, rungOfPage, searchHintsOf } from "./images.js";
|
|
9
|
+
import { hiddenFixCore, losslessServed, rungOfPage, searchHintsOf } from "./images.js";
|
|
10
10
|
import { animatedTarget } from "./media-model.js";
|
|
11
11
|
import { loaderName, sanitizeText, urlPattern } from "./locate.js";
|
|
12
12
|
/** The most background elements read per viewport: a page with more keeps the first of them in document order. */
|
|
@@ -264,7 +264,8 @@ export function buildBackgroundRecords(input) {
|
|
|
264
264
|
}
|
|
265
265
|
const records = [];
|
|
266
266
|
for (const [key, members] of byKey) {
|
|
267
|
-
|
|
267
|
+
// The reference member is shown when some viewport shows the layer: a hidden copy names no size.
|
|
268
|
+
const member = members.find((m) => m.viewport.name === refName && m.hidden === undefined) ?? members.find((m) => m.hidden === undefined) ?? members[0];
|
|
268
269
|
const source = sources.get(member.bg.index);
|
|
269
270
|
const url = member.url;
|
|
270
271
|
const perViewport = [];
|
|
@@ -287,8 +288,22 @@ export function buildBackgroundRecords(input) {
|
|
|
287
288
|
entry.overFetchRatio = Math.round((inst.pixelWidth / inst.needed) * 100) / 100;
|
|
288
289
|
if (inst.skipped !== undefined)
|
|
289
290
|
entry.skipped = inst.skipped;
|
|
291
|
+
if (inst.hidden !== undefined)
|
|
292
|
+
entry.hidden = inst.hidden;
|
|
290
293
|
perViewport.push(entry);
|
|
291
294
|
}
|
|
295
|
+
// A layer's entries: the viewports it is shown at, and those it is hidden at with its file downloaded (a hidden layer that
|
|
296
|
+
// was not downloaded is not an entry: there are no bytes to report).
|
|
297
|
+
const entries = members
|
|
298
|
+
.filter((m) => m.hidden !== undefined || m.skipped !== "hidden")
|
|
299
|
+
.map((m) => ({
|
|
300
|
+
viewport: { name: m.viewport.name, width: m.viewport.width },
|
|
301
|
+
bytes: m.bytes,
|
|
302
|
+
hidden: m.hidden,
|
|
303
|
+
inPanel: m.inPanel === true,
|
|
304
|
+
hover: m.hover === true,
|
|
305
|
+
}));
|
|
306
|
+
const hiddenFix = hiddenFixCore(entries, input.viewports.length);
|
|
292
307
|
const saved = members.reduce((sum, m) => sum + m.est, 0) / viewportCount;
|
|
293
308
|
const served = member.format;
|
|
294
309
|
const animatedMember = members.find((m) => m.animated !== undefined);
|
|
@@ -324,14 +339,15 @@ export function buildBackgroundRecords(input) {
|
|
|
324
339
|
},
|
|
325
340
|
perViewport,
|
|
326
341
|
fix: {
|
|
327
|
-
width: member.needed > 0 ? member.needed : undefined,
|
|
328
|
-
|
|
342
|
+
width: member.hidden === undefined && member.needed > 0 ? member.needed : undefined,
|
|
343
|
+
// A layer's saving is a resize to its needed width (est.), not a re-encode: booked as resize.
|
|
344
|
+
bytes: { saved, addedForSharpness: 0, sizes: 0, resize: saved, quality: 0 },
|
|
329
345
|
},
|
|
330
346
|
evidence: { rung: rungOfPage(pageUrl, input.harness), notes },
|
|
331
347
|
};
|
|
332
348
|
if (best !== undefined)
|
|
333
349
|
record.fix.format = best;
|
|
334
|
-
if (animatedMember?.animated !== undefined) {
|
|
350
|
+
if (animatedMember?.animated !== undefined && hiddenFix?.shownIn.length !== 0) {
|
|
335
351
|
record.fix.animated = {
|
|
336
352
|
info: animatedMember.animated,
|
|
337
353
|
context: "css-background",
|
|
@@ -339,12 +355,20 @@ export function buildBackgroundRecords(input) {
|
|
|
339
355
|
perViewBytes: members.reduce((sum, m) => sum + m.bytes, 0) / viewportCount,
|
|
340
356
|
};
|
|
341
357
|
}
|
|
358
|
+
// A hidden layer is not a vector claim: the vector check and the hidden fix never stack on one record.
|
|
359
|
+
if (hiddenFix !== undefined)
|
|
360
|
+
record.hidden = hiddenFix;
|
|
361
|
+
else {
|
|
362
|
+
const flatMember = members.find((m) => m.flatness?.flat === true);
|
|
363
|
+
if (flatMember?.flatness !== undefined)
|
|
364
|
+
record.fix.vector = { flatness: { ...flatMember.flatness } };
|
|
365
|
+
}
|
|
342
366
|
records.push(record);
|
|
343
367
|
}
|
|
344
368
|
return records.filter((r) => r.perViewport.some((p) => p.loaded || p.skipped !== undefined));
|
|
345
369
|
}
|
|
346
370
|
export function backgroundTotals(instances) {
|
|
347
|
-
const measured = instances.filter((i) => i.skipped === undefined);
|
|
371
|
+
const measured = instances.filter((i) => i.skipped === undefined && i.hidden === undefined);
|
|
348
372
|
return {
|
|
349
373
|
measured: measured.length,
|
|
350
374
|
servedBytes: measured.reduce((sum, i) => sum + i.bytes, 0),
|
|
@@ -353,7 +377,7 @@ export function backgroundTotals(instances) {
|
|
|
353
377
|
};
|
|
354
378
|
}
|
|
355
379
|
export function backgroundPage(instances, viewportCount, records) {
|
|
356
|
-
const measured = instances.filter((i) => i.skipped === undefined);
|
|
380
|
+
const measured = instances.filter((i) => i.skipped === undefined && i.hidden === undefined);
|
|
357
381
|
return {
|
|
358
382
|
layers: records.filter((r) => r.perViewport.some((p) => p.loaded)).length,
|
|
359
383
|
servedBytes: measured.reduce((sum, i) => sum + i.bytes, 0) / viewportCount,
|
package/dist/broken.js
CHANGED
|
@@ -25,7 +25,11 @@ export function brokenOf(result, expectedImages) {
|
|
|
25
25
|
fix: "the URL does not serve this page: check the path, and that the server being scanned serves this route",
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
// A failed stylesheet breaks the page only when the page is unstyled (no linked stylesheet applied) or empty (no images).
|
|
29
|
+
// A partly styled page with a failed sheet, usually a third-party CDN's, is measured and warned about (the scan prints it).
|
|
30
|
+
const unstyled = (result.stylesheetsLoaded ?? 0) === 0;
|
|
31
|
+
const empty = result.counts.images === 0;
|
|
32
|
+
if (result.stylesheetsFailed !== undefined && Object.keys(result.stylesheetsFailed).length > 0 && (unstyled || empty)) {
|
|
29
33
|
const hosts = result.stylesheetHosts ?? [];
|
|
30
34
|
// The most failed in any one view: the same sheet at two viewports is one sheet, not two.
|
|
31
35
|
const count = Math.max(...Object.values(result.stylesheetsFailed));
|
package/dist/browser.js
CHANGED
|
@@ -14,11 +14,11 @@ export function mapLaunchError(err) {
|
|
|
14
14
|
return /Executable doesn['’]t exist/.test(text) ? new ChromiumMissingError() : err;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* Chromium flags for measuring. `--disable-
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Chromium flags for measuring. `--disable-gpu` keeps the GPU process out. The software rasterizer (SwiftShader) is left
|
|
18
|
+
* on: with `--disable-software-rasterizer` WebGL is off, and a page with a 3D viewer never requests its model (a census
|
|
19
|
+
* site made 5 model requests with the default flags and none without). Only WebGL pages pay the extra CPU for it.
|
|
20
20
|
*/
|
|
21
|
-
export const CHROMIUM_ARGS = ["--disable-gpu"
|
|
21
|
+
export const CHROMIUM_ARGS = ["--disable-gpu"];
|
|
22
22
|
/** The headless Chromium every measuring command uses. */
|
|
23
23
|
export async function launchChromium() {
|
|
24
24
|
try {
|
package/dist/candidates.js
CHANGED
|
@@ -13,6 +13,31 @@ export function rightCandidate(candidates, needed) {
|
|
|
13
13
|
const sorted = [...candidates].sort((a, b) => a.width - b.width);
|
|
14
14
|
return sorted.find((c) => c.width >= needed) ?? sorted[sorted.length - 1];
|
|
15
15
|
}
|
|
16
|
+
/** The device-pixel step a plain image's variant widths are rounded up to. */
|
|
17
|
+
export const SRCSET_WIDTH_STEP = 10;
|
|
18
|
+
/** A plain image gets at most this many variant widths, one file each. */
|
|
19
|
+
export const SRCSET_MAX_WIDTHS = 3;
|
|
20
|
+
/** The widths of a srcset as printed: "190w, 270w, 380w". */
|
|
21
|
+
export function srcsetWidthLabel(widths) {
|
|
22
|
+
return widths.map((w) => `${w}w`).join(", ");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The widths the variants of a plain image (no srcset today) need: each device-pixel need rounded up to SRCSET_WIDTH_STEP
|
|
26
|
+
* and capped at the file's own width. More than SRCSET_MAX_WIDTHS are merged by joining the closest pair and keeping the
|
|
27
|
+
* larger, so every need is still covered by a file at least as wide. Sorted ascending; empty when no need is known.
|
|
28
|
+
*/
|
|
29
|
+
export function srcsetWidthsOf(needs, naturalWidth) {
|
|
30
|
+
const cap = naturalWidth !== undefined && naturalWidth > 0 ? naturalWidth : Infinity;
|
|
31
|
+
const widths = [...new Set(needs.filter((n) => n > 0).map((n) => Math.min(Math.ceil(n / SRCSET_WIDTH_STEP) * SRCSET_WIDTH_STEP, cap)))].sort((a, b) => a - b);
|
|
32
|
+
while (widths.length > SRCSET_MAX_WIDTHS) {
|
|
33
|
+
let at = 0;
|
|
34
|
+
for (let i = 1; i < widths.length - 1; i++)
|
|
35
|
+
if (widths[i + 1] - widths[i] < widths[at + 1] - widths[at])
|
|
36
|
+
at = i;
|
|
37
|
+
widths.splice(at, 1);
|
|
38
|
+
}
|
|
39
|
+
return widths;
|
|
40
|
+
}
|
|
16
41
|
/** The served file is undersized: its pixels are below 0.9 x need, and a larger candidate exists. */
|
|
17
42
|
export function isUndersized(servedWidth, needed, largest) {
|
|
18
43
|
return servedWidth < UNDERSIZED_RATIO * needed && servedWidth < largest;
|
package/dist/check-report.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// The Markdown report of `imgtune check`: a pure builder over the survey results, the deep results, the route grouping and
|
|
2
2
|
// the deep selection. No browser, no files. The audit mode extends it with source-aware sections; the sections below keep
|
|
3
3
|
// their order so the two stay comparable. Page-derived text goes through mdText and mdCode (see text.ts).
|
|
4
|
-
import { hintsOf, videoRowOf, videoSavingOf, videoTotalsLine } from "./hints.js";
|
|
4
|
+
import { groupSrcsetOf, hiddenPagesLine, hiddenVideoSummaryLine, hintsOf, videoRowOf, videoSavingOf, videoTotalsLine } from "./hints.js";
|
|
5
|
+
import { srcsetWidthLabel } from "./candidates.js";
|
|
5
6
|
import { lcpLinesOf } from "./lcp.js";
|
|
6
7
|
import { planQualityCap, qualitiesLine } from "./qualities-cap.js";
|
|
7
8
|
import { hasFix } from "./images.js";
|
|
8
9
|
import { APPROX_MARK, siteNumbers } from "./remote.js";
|
|
9
10
|
import { errorLine, mdCode, mdText, safeText } from "./text.js";
|
|
11
|
+
import { resourceTotalsLine } from "./resources/report.js";
|
|
10
12
|
/** How many fixes, heavy pages, sharpness images and survey route types the report lists in full. */
|
|
11
13
|
export const REPORT_FIXES = 15;
|
|
12
14
|
export const REPORT_SHARPNESS = 15;
|
|
@@ -66,6 +68,7 @@ export function fixGroupsOf(deep, qualities) {
|
|
|
66
68
|
const pages = [...new Set(group.hints.map((h) => h.page))];
|
|
67
69
|
const saved = group.hints.reduce((sum, h) => sum + h.bytes.saved, 0) / 1024;
|
|
68
70
|
const sizes = group.hints.reduce((sum, h) => sum + (h.bytes.sizes ?? 0), 0) / 1024;
|
|
71
|
+
const resize = group.hints.reduce((sum, h) => sum + (h.bytes.resize ?? 0), 0) / 1024;
|
|
69
72
|
const sharp = group.hints.reduce((sum, h) => sum + h.bytes.addedForSharpness, 0) / 1024;
|
|
70
73
|
const targets = group.hints.map((h) => h.quality?.target).filter((t) => t !== undefined);
|
|
71
74
|
const quality = group.hints.find((h) => h.quality !== undefined)?.quality;
|
|
@@ -82,6 +85,7 @@ export function fixGroupsOf(deep, qualities) {
|
|
|
82
85
|
current,
|
|
83
86
|
proposed,
|
|
84
87
|
basis: first.sizes.basis,
|
|
88
|
+
srcset: groupSrcsetOf(group),
|
|
85
89
|
format: best !== undefined && best !== first.format?.current ? best : undefined,
|
|
86
90
|
...(quality !== undefined
|
|
87
91
|
? {
|
|
@@ -99,7 +103,8 @@ export function fixGroupsOf(deep, qualities) {
|
|
|
99
103
|
savedKBSum: saved,
|
|
100
104
|
savedKBPerPage: perPage(saved),
|
|
101
105
|
sizesKBPerPage: perPage(sizes),
|
|
102
|
-
|
|
106
|
+
resizeKBPerPage: perPage(resize),
|
|
107
|
+
qualityKBPerPage: perPage(saved - sizes - resize),
|
|
103
108
|
sharpnessKBPerPage: perPage(sharp),
|
|
104
109
|
});
|
|
105
110
|
}
|
|
@@ -108,7 +113,11 @@ export function fixGroupsOf(deep, qualities) {
|
|
|
108
113
|
/** The line of one fix: where, the sizes and quality change, the saving per view and its pages. */
|
|
109
114
|
function fixLines(fix, rank) {
|
|
110
115
|
const where = fix.located ? mdCode(fix.where) : `${mdCode(fix.where)} (not located)`;
|
|
111
|
-
const sizes = fix.
|
|
116
|
+
const sizes = fix.srcset !== undefined
|
|
117
|
+
? `no srcset -> serve ${srcsetWidthLabel(fix.srcset.widths)} + srcset/sizes`
|
|
118
|
+
: fix.current === fix.proposed
|
|
119
|
+
? "sizes unchanged"
|
|
120
|
+
: `sizes ${mdCode(fix.current)} -> ${mdCode(fix.proposed)}${fix.basis ? ` (${mdText(fix.basis)})` : ""}`;
|
|
112
121
|
const quality = fix.quality === undefined
|
|
113
122
|
? "quality -"
|
|
114
123
|
: fix.quality.target === undefined
|
|
@@ -116,7 +125,8 @@ function fixLines(fix, rank) {
|
|
|
116
125
|
: fix.quality.void
|
|
117
126
|
? `quality ${fix.quality.current} -> ${fix.quality.target} (snapped: void, counted as 0)`
|
|
118
127
|
: `quality ${fix.quality.current} -> ${fix.quality.target}${fix.quality.snapFrom !== undefined ? ` (snapped from ${fix.quality.snapFrom}${fix.quality.allowed ? `, allowed ${fix.quality.allowed.join(",")}` : ""})` : ""}`;
|
|
119
|
-
const
|
|
128
|
+
const split = fix.resizeKBPerPage > 0 ? `sizes ${kb(fix.sizesKBPerPage)} + resize ${kb(fix.resizeKBPerPage)} + quality ${kb(fix.qualityKBPerPage)}` : `sizes ${kb(fix.sizesKBPerPage)} + quality ${kb(fix.qualityKBPerPage)}`;
|
|
129
|
+
const saving = `saves ${kb(fix.savedKBPerPage)} KB/view per page (${split})`;
|
|
120
130
|
const source = fix.pattern !== undefined ? `, src ${mdCode(fix.pattern)}` : "";
|
|
121
131
|
const where2 = `${fix.pages.length} page(s), ${fix.images} image(s), loader ${mdText(fix.loader)}${source}`;
|
|
122
132
|
return [
|
|
@@ -157,6 +167,9 @@ export function buildCheckReport(input) {
|
|
|
157
167
|
const media = [...input.survey, ...input.deep].find((p) => p.mediaNote !== undefined)?.mediaNote;
|
|
158
168
|
if (media !== undefined)
|
|
159
169
|
out(mdText(media), "");
|
|
170
|
+
const vectorNote = input.deep.find((p) => p.vectorNote !== undefined)?.vectorNote;
|
|
171
|
+
if (vectorNote !== undefined)
|
|
172
|
+
out(mdText(vectorNote), "");
|
|
160
173
|
if (input.stop !== undefined) {
|
|
161
174
|
out("## Stopped before the deep phase", "", `${mdText(input.stop.reason)}`, `Fix: ${mdText(input.stop.fix)}`, "");
|
|
162
175
|
}
|
|
@@ -174,6 +187,9 @@ export function buildCheckReport(input) {
|
|
|
174
187
|
const exactEst = mean(deepTotals.map((t) => t.exactEstKB));
|
|
175
188
|
const numbers = siteNumbers(deepOk.map((p) => p.page));
|
|
176
189
|
out(`- Deep (${deepOk.length} page(s), viewports ${input.options.deepViewports.map(mdText).join(", ")}): mean image ${kb(numbers.meanImageBytes / 1024)} KB/view.`, ` - exact: ${kb(exact)} KB/view (${pct(numbers.exactPct)})`, ` - exact + est.: ${APPROX_MARK.trim()} ${kb(exactEst)} KB/view (${pct(numbers.exactEstPct)})${approx ? " (some estimates are approximate)" : ""}`, ` - sharpness: +${kb(mean(deepTotals.map((t) => t.sharpnessKB)))} KB/view, a separate number: it is never netted against the saving`);
|
|
190
|
+
const hiddenLine = hiddenPagesLine(deepOk);
|
|
191
|
+
if (hiddenLine !== undefined)
|
|
192
|
+
out(` - ${mdText(hiddenLine)}`);
|
|
177
193
|
// The LCP image is the one the first paint waits for: its line, per page, before the ranked fixes.
|
|
178
194
|
for (const page of deepOk) {
|
|
179
195
|
for (const line of lcpLinesOf(page, mdCode))
|
|
@@ -201,12 +217,29 @@ export function buildCheckReport(input) {
|
|
|
201
217
|
if (videos.length > 0) {
|
|
202
218
|
out("## Video", "");
|
|
203
219
|
out(`${videoTotalsLine(videos.map((v) => v.video))} (${videos.length} video(s) on ${deepOk.filter((p) => (p.videos ?? []).length > 0).length} deep page(s))`, "");
|
|
220
|
+
const hiddenVideos = hiddenVideoSummaryLine(videos.map((v) => v.video));
|
|
221
|
+
if (hiddenVideos !== undefined)
|
|
222
|
+
out(`- ${mdText(hiddenVideos)}`);
|
|
204
223
|
const ranked = [...videos].sort((a, b) => videoSavingOf(b.video).view - videoSavingOf(a.video).view || a.page.localeCompare(b.page));
|
|
205
224
|
for (const row of ranked.slice(0, REPORT_FIXES)) {
|
|
206
225
|
out(`- ${mdCode(videoRowOf(row.video, (text) => JSON.stringify(safeText(text))))} on ${mdCode(pathOf(row.page))}`);
|
|
207
226
|
}
|
|
208
227
|
out("");
|
|
209
228
|
}
|
|
229
|
+
// 3c. Resources: the deep pages' non-image findings (compression, favicons), largest saving first (see resources/).
|
|
230
|
+
const resourceRows = deepOk.flatMap((p) => (p.resources ?? []).map((finding) => ({ page: p.url, finding })));
|
|
231
|
+
if (resourceRows.length > 0) {
|
|
232
|
+
out("## Resources", "");
|
|
233
|
+
out(mdText(resourceTotalsLine(resourceRows.map((r) => r.finding)) ?? ""), "");
|
|
234
|
+
const ranked = [...resourceRows].sort((a, b) => b.finding.savedBytes - a.finding.savedBytes || a.page.localeCompare(b.page));
|
|
235
|
+
for (const row of ranked.slice(0, REPORT_FIXES)) {
|
|
236
|
+
const saved = row.finding.savedBytes > 0 ? `, saves ${kb(row.finding.savedBytes / 1024)} KB/view${row.finding.estimate ? " (est.)" : ""}` : "";
|
|
237
|
+
out(`- ${mdText(row.finding.kind)}: ${mdText(row.finding.title)}${saved} on ${mdCode(pathOf(row.page))}`);
|
|
238
|
+
if (row.finding.fix[0] !== undefined)
|
|
239
|
+
out(` - fix: ${mdText(row.finding.fix[0])}`);
|
|
240
|
+
}
|
|
241
|
+
out("");
|
|
242
|
+
}
|
|
210
243
|
// 4. Heavy but not wasteful.
|
|
211
244
|
out("## Heavy but not wasteful", "");
|
|
212
245
|
const heavyCut = percentile(deepTotals.map((t) => t.kb), 0.75);
|
package/dist/check.js
CHANGED
|
@@ -11,7 +11,7 @@ import { gpuSettingOf, gpuSettingOne } from "./gpu.js";
|
|
|
11
11
|
import { parseQualities } from "./hints.js";
|
|
12
12
|
import { BROKEN_EXIT, surveyStop } from "./broken.js";
|
|
13
13
|
import { DEFAULT_THIRD_PARTY, parseAllowHosts, thirdPartyFromFlags } from "./third-party.js";
|
|
14
|
-
import { MAX_IDLE_TIMEOUT_MS, MAX_PAGE_CONCURRENCY, parseLimit, parseRange, readStorageState, runScan, scanPlan, } from "./remote.js";
|
|
14
|
+
import { MAX_IDLE_TIMEOUT_MS, MAX_PAGE_CONCURRENCY, MAX_RESOURCE_TIME, MAX_SVG_BUDGET, parseLimit, parseRange, readStorageState, runScan, scanPlan, } from "./remote.js";
|
|
15
15
|
import { restrictRoutes, groupRoutes, parseRouteMap } from "./routes.js";
|
|
16
16
|
import { sampleByPattern, urlsFromSource } from "./urls.js";
|
|
17
17
|
import { deepRetries, selectDeepPages } from "./deep.js";
|
|
@@ -38,6 +38,8 @@ export function parseCheckFlags(argv) {
|
|
|
38
38
|
args.noCache = true;
|
|
39
39
|
else if (arg === "--gpu-encode")
|
|
40
40
|
args.gpuEncode = true;
|
|
41
|
+
else if (arg === "--no-resources")
|
|
42
|
+
args.noResources = true;
|
|
41
43
|
else if (arg === "--ignore-robots")
|
|
42
44
|
args.ignoreRobots = true;
|
|
43
45
|
else if (arg === "--block-third-party")
|
|
@@ -59,6 +61,8 @@ export function parseCheckFlags(argv) {
|
|
|
59
61
|
arg === "--page-concurrency" ||
|
|
60
62
|
arg === "--qualities" ||
|
|
61
63
|
arg === "--idle-timeout" ||
|
|
64
|
+
arg === "--svg-budget" ||
|
|
65
|
+
arg === "--resource-time" ||
|
|
62
66
|
arg === "--storage-state" ||
|
|
63
67
|
arg === "--allow-host") {
|
|
64
68
|
const value = valueOf(argv, i, arg);
|
|
@@ -79,6 +83,10 @@ export function parseCheckFlags(argv) {
|
|
|
79
83
|
args.qualities = parseQualities(value);
|
|
80
84
|
else if (arg === "--idle-timeout")
|
|
81
85
|
args.idleTimeoutMs = parseRange(arg, value, 0, MAX_IDLE_TIMEOUT_MS);
|
|
86
|
+
else if (arg === "--svg-budget")
|
|
87
|
+
args.svgBudget = parseRange(arg, value, 0, MAX_SVG_BUDGET);
|
|
88
|
+
else if (arg === "--resource-time")
|
|
89
|
+
args.resourceTime = parseRange(arg, value, 0, MAX_RESOURCE_TIME);
|
|
82
90
|
else if (arg === "--storage-state")
|
|
83
91
|
args.storageState = value;
|
|
84
92
|
else
|
|
@@ -184,6 +192,7 @@ export async function scanCheckPages(input) {
|
|
|
184
192
|
ignoreRobots: settings.ignoreRobots,
|
|
185
193
|
aggressive: false,
|
|
186
194
|
thirdParty: settings.thirdParty,
|
|
195
|
+
resources: settings.noResources !== true,
|
|
187
196
|
};
|
|
188
197
|
const base = { urls: [], out: outRoot, aggressive: false, quality: true, thirdParty: settings.thirdParty };
|
|
189
198
|
const surveyPlan = scanPlan({ ...base, survey: true });
|
|
@@ -206,6 +215,8 @@ export async function scanCheckPages(input) {
|
|
|
206
215
|
...(settings.thirdParty.block ? ["--block-third-party"] : []),
|
|
207
216
|
...(settings.noCache ? ["--no-cache"] : []),
|
|
208
217
|
...(settings.gpuEncode ? ["--gpu-encode"] : []),
|
|
218
|
+
...(settings.noResources ? ["--no-resources"] : []),
|
|
219
|
+
...(settings.resourceTime !== undefined ? ["--resource-time", String(settings.resourceTime)] : []),
|
|
209
220
|
].join(" "),
|
|
210
221
|
});
|
|
211
222
|
log(`survey: ${urls.length} URL(s), ${routes.types.length} route type(s)`);
|
|
@@ -244,9 +255,12 @@ export async function scanCheckPages(input) {
|
|
|
244
255
|
...(settings.thirdParty.allowHosts.length > 0 ? ["--allow-host", shellWord(settings.thirdParty.allowHosts.join(","))] : []),
|
|
245
256
|
...(settings.noCache ? ["--no-cache"] : []),
|
|
246
257
|
...(settings.gpuEncode ? ["--gpu-encode"] : []),
|
|
258
|
+
...(settings.noResources ? ["--no-resources"] : []),
|
|
259
|
+
...(settings.resourceTime !== undefined ? ["--resource-time", String(settings.resourceTime)] : []),
|
|
247
260
|
...(settings.ignoreRobots ? ["--ignore-robots"] : []),
|
|
248
261
|
...(settings.pageConcurrency !== undefined ? ["--page-concurrency", String(settings.pageConcurrency)] : []),
|
|
249
262
|
...(settings.idleTimeoutMs !== undefined ? ["--idle-timeout", String(settings.idleTimeoutMs)] : []),
|
|
263
|
+
...(settings.svgBudget !== undefined ? ["--svg-budget", String(settings.svgBudget)] : []),
|
|
250
264
|
];
|
|
251
265
|
commands.push({ phase: "deep", command: ["imgtune scan", ...deepUrls.map(shellWord), "--out", shellWord(deepDir), ...flags].join(" ") });
|
|
252
266
|
log(`deep: ${deepUrls.length} page(s), full scan`);
|
|
@@ -258,6 +272,7 @@ export async function scanCheckPages(input) {
|
|
|
258
272
|
pageConcurrency: fullPlan.pageConcurrency,
|
|
259
273
|
idleTimeoutMs: fullPlan.idleTimeoutMs,
|
|
260
274
|
gpu: settings.gpu,
|
|
275
|
+
svgBudget: settings.svgBudget,
|
|
261
276
|
expectImages: (url) => expected.get(url),
|
|
262
277
|
};
|
|
263
278
|
const deepStart = performance.now();
|
|
@@ -346,6 +361,8 @@ export async function finishCheck(input) {
|
|
|
346
361
|
pageConcurrency: settings.pageConcurrency,
|
|
347
362
|
idleTimeoutMs: settings.idleTimeoutMs,
|
|
348
363
|
noCache: settings.noCache,
|
|
364
|
+
...(settings.noResources ? { noResources: true } : {}),
|
|
365
|
+
...(settings.resourceTime !== undefined ? { resourceTime: settings.resourceTime } : {}),
|
|
349
366
|
ignoreRobots: settings.ignoreRobots,
|
|
350
367
|
storageState: input.storageState,
|
|
351
368
|
thirdParty: settings.thirdParty.block ? "blocked" : "allowed",
|