GoogleChrome / GoogleChrome/lighthouse
Image Delivery insight false-flags correctly-sized responsive images under DPR>1 emulation (host DPR sampled from an emulated page; fix belongs in the trace engine / Chromium)
- Dominant language
- JavaScript
- Stars
- 30.8k
- Forks
- 9.8k
- Avg merge
- 1d 20h
- Merged PRs (30d)
- 19
Description
> ⚠️ **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:
```html
```
Run with the inspected page emulated **before** Lighthouse starts (what the DevTools panel does), via the Node API:
```js
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:
1. **Minimal** — Blink records the host/raster device scale factor in the trace, e.g. alongside the existing `dpr` in `PaintTimingVisualizer::Viewport`:
```cpp
// 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`:
```js
const meta = metaHandlerData();
const hostDpr = meta.hostDevicePixelRatio ?? options.metadata?.hostDPR; // trace value is immune to emulation
const emulatedDpr = meta.devicePixelRatio;
// correctedDisplaySize = (paintWidth / hostDpr) * emulatedDpr
```
2. **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)
Contributor guide
Assessment
This issue has not been assessed yet.