⚠️ Disclaimer — AI-generated. This issue was researched and written by AI (Claude Code, Opus 4.8). It is not hand-waved speculation: the bug was verified by actually cloning, instrumenting, and running Lighthouse end-to-end (real Google Chrome 149, Lighthouse 13.2.0 and 13.4.0), with the decisive values logged from real runs. A minimal reproduction and the measured evidence are included below so maintainers can verify independently.
Summary
The "Improve image delivery" insight (image-delivery-insight, backed by @paulirish/trace_engine's ImageDelivery) false-flags correctly-sized responsive images as "larger than needed" on high‑DPR mobile audits run in the DevTools panel and PageSpeed Insights. The standalone Lighthouse CLI is not affected. The recommended fix is upstream in the trace engine / Chromium, because the data the insight needs is missing from the trace.
Related: #16579, #16766. The DevTools CL that closed crbug.com/416580500 ([RPP] Correct image paint size using host DPR) and the Lighthouse plumbing (#16559, #16771) do not fix the DevTools/PSI case — see below.
Root cause (verified)
To undo the fact that Chrome paints images at the host DPR (not the emulated one), the insight corrects the displayed size in ImagePaintingHandler.finalize:
correctedDisplaySize = (paintWidth / HostDPR) * emulatedDPR
HostDPR comes from Lighthouse's HostDPR artifact, which is window.devicePixelRatio read in core/gather/driver/environment.js → getDevicePixelRatio.
- In the standalone CLI, that read happens before emulation is applied, so
HostDPR = the true host value (e.g. 1). The correction is right and nothing is flagged. ✅
- In the DevTools panel / PageSpeed Insights, the inspected target is already under device emulation when
HostDPR is sampled, so window.devicePixelRatio returns the emulated value (e.g. 1.75). With HostDPR == emulatedDPR, the correction collapses to the raw CSS slot size:
(412 / 1.75) * 1.75 = 412 // DPR ignored → image appears 67% oversized
This bites whenever host DPR < emulated DPR (e.g. a DPR‑1 monitor running the mobile profile). On a Retina host (DPR 2 ≥ 1.75) the error rounds the other way and hides the warning — which is why it doesn't reproduce on many dev machines.
Reproduction
A responsive image correctly sized for mobile DPR 1.75 — on a 412px viewport, sizes="100vw" needs 412 × 1.75 = 721px, and the browser correctly loads the 721px candidate:
<meta name="viewport" content="width=device-width, initial-scale=1">
<img src="img-721.jpg"
srcset="img-412.jpg 412w, img-721.jpg 721w, img-1100.jpg 1100w"
sizes="100vw" width="721" height="721" style="width:100%;height:auto" alt="hero">
Run with the inspected page emulated before Lighthouse starts (what the DevTools panel does), via the Node API:
const page = await browser.newPage();
const client = await page.target().createCDPSession();
await client.send('Emulation.setDeviceMetricsOverride',
{width:412, height:823, deviceScaleFactor:1.75, mobile:true});
const {lhr} = await lighthouse('http://localhost:8099/',
{onlyAudits:['image-delivery-insight'], screenEmulation:{disabled:true}, formFactor:'mobile'},
undefined, page);
// → image-delivery-insight score 0:
// "This image file is larger than it needs to be (721x721) for its displayed
// dimensions (412x412). Use responsive images to reduce the image download size."
The standalone CLI on the identical page/version reports score 1 (pass).
Measured evidence (instrumented real runs, Chrome 149)
console.error probes added in trace-engine-result.js, ImagePaintingHandler.finalize, and ImageDelivery.generateInsight:
| Scenario |
HostDPR |
emulatedDPR |
displayed size used |
wasted |
result |
| CLI headless (13.4.0) |
1 |
1.75 |
721×721 (corrected) |
0.0% |
PASS |
| CLI headless (13.2.0) |
1 |
1.75 |
721×721 (corrected) |
0.0% |
PASS |
| Pre-emulated target (DevTools-style) |
1.75 |
1.75 |
412×412 |
67.3% |
FAIL |
window.devicePixelRatio probe: 1 before setDeviceMetricsOverride(1.75), 1.75 after. This is not a version regression (12.x → 13.x behave identically), and it is not the page markup — the image is correctly sized for the device.
The host DPR is missing from the trace
I captured a mobile-emulated trace with the full cc/compositor categories enabled (cc, disabled-by-default-cc, disabled-by-default-cc.debug, disabled-by-default-layer-tree, viz, … — 4505 events). The trace contains only the emulated dpr: 1.75 (in PaintTimingVisualizer::Viewport). There is no device_scale_factor anywhere, and PaintImage sizes are recorded in host‑physical px (never CSS/DIP). So the trace engine cannot recover the host DPR from trace data — it must be supplied externally, which is exactly the step that breaks under emulation.
Recommended fix (Chromium / trace engine)
Because ImageDelivery is shared by DevTools, PageSpeed Insights, and Lighthouse, fixing it at the source fixes all three. Put the missing data in the trace:
-
Minimal — Blink records the host/raster device scale factor in the trace, e.g. alongside the existing dpr in PaintTimingVisualizer::Viewport:
// third_party/blink/renderer/core/paint/paint_timing_visualizer.cc
value->SetDouble("dpr", frame->DevicePixelRatio()); // emulated (already present)
value->SetDouble("hostDpr", screen_info.device_scale_factor); // NEW: true raster DPR
Then MetaHandler exposes hostDpr, and ImagePaintingHandler.finalize divides by the trace-sourced host DPR instead of the externally-sampled metadata.hostDPR:
const meta = metaHandlerData();
const hostDpr = meta.hostDevicePixelRatio ?? options.metadata?.hostDPR; // trace value is immune to emulation
const emulatedDpr = meta.devicePixelRatio;
// correctedDisplaySize = (paintWidth / hostDpr) * emulatedDpr
-
Cleaner — Blink records the PaintImage displayed size in CSS/DIP px. Then no host-DPR correction is needed at all (expected = fileSize vs displayedCssPx * emulatedDpr, and emulatedDpr is already reliably in the trace), and the paintEventToCorrectedDisplaySize / HostDPR machinery can be removed.
Interim, no Chrome change required:
- Embedder-supplied host DPR. Only embedders are affected. The DevTools-frontend UI window runs on the host display, so DevTools' own
window.devicePixelRatio is the true host DPR (PSI is headless → 1). Thread that into metadata.hostDPR instead of sampling the emulated target.
- Lighthouse-only.
image-delivery-insight can suppress a RESPONSIVE_SIZE finding using ImageElements.displayedWidth/Height (CSS px, already gathered) and the trace's emulated DPR — naturalWidth ≤ displayedCSS × emulatedDPR — the same DPR-aware check the legacy uses-responsive-images/image-size-responsive audits use. (Fixes the CLI + DevTools Lighthouse panel, not PSI's Performance‑panel Insights.)
Note: crbug.com/416580500 is marked Fixed, but the DevTools/PSI path is still broken as shown above, so this likely needs a fresh devtools-frontend/Chromium bug.
Environment
- Lighthouse 12.6.0 → 13.4.0 (verified on 13.2.0 and 13.4.0)
- Google Chrome / Chromium 149
- DevTools Lighthouse panel and PageSpeed Insights, mobile profile (412×823, DPR 1.75)
- Host monitor DPR 1 (any host DPR < emulated DPR)
Summary
The "Improve image delivery" insight (
image-delivery-insight, backed by@paulirish/trace_engine'sImageDelivery) false-flags correctly-sized responsive images as "larger than needed" on high‑DPR mobile audits run in the DevTools panel and PageSpeed Insights. The standalone Lighthouse CLI is not affected. The recommended fix is upstream in the trace engine / Chromium, because the data the insight needs is missing from the trace.Related: #16579, #16766. The DevTools CL that closed crbug.com/416580500 (
[RPP] Correct image paint size using host DPR) and the Lighthouse plumbing (#16559, #16771) do not fix the DevTools/PSI case — see below.Root cause (verified)
To undo the fact that Chrome paints images at the host DPR (not the emulated one), the insight corrects the displayed size in
ImagePaintingHandler.finalize:HostDPRcomes from Lighthouse'sHostDPRartifact, which iswindow.devicePixelRatioread incore/gather/driver/environment.js→getDevicePixelRatio.HostDPR= the true host value (e.g.1). The correction is right and nothing is flagged. ✅HostDPRis sampled, sowindow.devicePixelRatioreturns the emulated value (e.g.1.75). WithHostDPR == emulatedDPR, the correction collapses to the raw CSS slot size:This bites whenever host DPR < emulated DPR (e.g. a DPR‑1 monitor running the mobile profile). On a Retina host (DPR 2 ≥ 1.75) the error rounds the other way and hides the warning — which is why it doesn't reproduce on many dev machines.
Reproduction
A responsive image correctly sized for mobile DPR 1.75 — on a 412px viewport,
sizes="100vw"needs 412 × 1.75 = 721px, and the browser correctly loads the 721px candidate:Run with the inspected page emulated before Lighthouse starts (what the DevTools panel does), via the Node API:
The standalone CLI on the identical page/version reports score 1 (pass).
Measured evidence (instrumented real runs, Chrome 149)
console.errorprobes added intrace-engine-result.js,ImagePaintingHandler.finalize, andImageDelivery.generateInsight:window.devicePixelRatioprobe: 1 beforesetDeviceMetricsOverride(1.75), 1.75 after. This is not a version regression (12.x → 13.x behave identically), and it is not the page markup — the image is correctly sized for the device.The host DPR is missing from the trace
I captured a mobile-emulated trace with the full cc/compositor categories enabled (
cc,disabled-by-default-cc,disabled-by-default-cc.debug,disabled-by-default-layer-tree,viz, … — 4505 events). The trace contains only the emulateddpr: 1.75(inPaintTimingVisualizer::Viewport). There is nodevice_scale_factoranywhere, andPaintImagesizes are recorded in host‑physical px (never CSS/DIP). So the trace engine cannot recover the host DPR from trace data — it must be supplied externally, which is exactly the step that breaks under emulation.Recommended fix (Chromium / trace engine)
Because
ImageDeliveryis shared by DevTools, PageSpeed Insights, and Lighthouse, fixing it at the source fixes all three. Put the missing data in the trace:Minimal — Blink records the host/raster device scale factor in the trace, e.g. alongside the existing
dprinPaintTimingVisualizer::Viewport:Then
MetaHandlerexposeshostDpr, andImagePaintingHandler.finalizedivides by the trace-sourced host DPR instead of the externally-sampledmetadata.hostDPR:Cleaner — Blink records the
PaintImagedisplayed size in CSS/DIP px. Then no host-DPR correction is needed at all (expected = fileSizevsdisplayedCssPx * emulatedDpr, andemulatedDpris already reliably in the trace), and thepaintEventToCorrectedDisplaySize/HostDPRmachinery can be removed.Interim, no Chrome change required:
window.devicePixelRatiois the true host DPR (PSI is headless → 1). Thread that intometadata.hostDPRinstead of sampling the emulated target.image-delivery-insightcan suppress aRESPONSIVE_SIZEfinding usingImageElements.displayedWidth/Height(CSS px, already gathered) and the trace's emulated DPR —naturalWidth ≤ displayedCSS × emulatedDPR— the same DPR-aware check the legacyuses-responsive-images/image-size-responsiveaudits use. (Fixes the CLI + DevTools Lighthouse panel, not PSI's Performance‑panel Insights.)Note: crbug.com/416580500 is marked Fixed, but the DevTools/PSI path is still broken as shown above, so this likely needs a fresh devtools-frontend/Chromium bug.
Environment