Which @angular/* package(s) are the source of the bug?
common (NgOptimizedImage)
Is this a regression?
No — the check has behaved this way since it was introduced. Note that main recently added callOnLoadIfImageIsLoaded(img, callback), which widens the window for this false positive (see analysis below).
Description
assertNonZeroRenderedHeight emits NG02952 — "the height of the fill-mode image is zero" whenever a fill-mode image fires load while img.clientHeight === 0. However, clientHeight is also 0 when the element is simply not connected to the document yet — which legitimately happens when a memory-cached image finishes loading before Angular attaches the view.
Real-world scenario where we hit this reliably (e-commerce home page):
- The same image URL appears in multiple
@defer sections (e.g., the same product rendered in two different carousels, or the same CMS cover in two branches).
- The first instance fetches the image over the network.
- When a later deferred view renders, Angular creates the elements,
NgOptimizedImage sets src, and the browser serves the image from memory cache — the load event fires before the embedded view is attached to the document.
- Detached element → no layout →
clientHeight === 0 → NG02952 warns, telling the developer to fix position/height on a container that is actually correctly styled.
- One tick later the view attaches and the image renders perfectly (in our case, resting height 350–389px).
The warning is therefore a false positive: it fires in bursts (10+ at once, one per duplicated cached image), is machine/timing dependent (fast cache + busy main thread), and points developers at CSS that is not broken. We spent significant time chasing phantom "zero-height containers" before isolating the real cause.
Evidence that the warned images are detached at load time
We instrumented the page with a document-level capture-phase load listener (resource load events do not bubble, but they do traverse the capture path — this is the same technique Angular-adjacent image-fallback utilities use):
document.addEventListener('load', (e) => {
if ((e.target as HTMLElement)?.tagName === 'IMG') record(e.target);
}, true);
Result, repeated across many reloads:
- Every NG02952 burst fired while the listener was active, yet the warned images'
load events never traversed document — impossible unless the element had no ancestor chain at dispatch time (i.e., it was detached; a connected <img>'s load reliably reaches a document capture listener, which we verified separately).
- A periodic sweep then found the same images
complete === true, isConnected === true, with correct non-zero heights — they had attached and laid out normally right after the warning.
- No image ever loaded zero-height while connected (a paired "capture listener + clientHeight === 0" probe never fired).
Reproduction (minimal)
@Component({
selector: 'repro',
imports: [NgOptimizedImage],
template: `
<!-- Instance A: fetches the image over the network -->
<div class="box"><img [ngSrc]="url" fill /></div>
<!-- Instance B: same URL, deferred — served from memory cache;
'load' can fire before the deferred view is attached -->
@defer (on timer(50ms)) {
<div class="box"><img [ngSrc]="url" fill /></div>
}
`,
styles: `.box { position: relative; width: 240px; height: 240px; }`,
})
export class ReproComponent {
url = 'https://example.com/some-image.jpg';
}
The container is correctly styled (position: relative, fixed non-zero height), yet instance B intermittently logs NG02952 when the cached load wins the race against view attachment. Reproduction is timing-sensitive (more likely with CPU throttling / busy main thread / many duplicated deferred instances). We can provide a StackBlitz on request.
Expected behavior
The zero-height heuristic should not warn for elements that are not connected to the document at the moment the check runs — a detached element always measures clientHeight === 0 regardless of how correct the CSS is.
Actual behavior
NG02952: The NgOptimizedImage directive (activated on an <img> element with the
`ngSrc="https://…/img/250/XXXX-2.jpg"`) has detected that the height of the
fill-mode image is zero. This is likely because the containing element does not
have the CSS 'position' property set to one of the following: "relative",
"fixed", or "absolute". …
…in bursts, for images whose containers are correctly positioned and sized.
Proposed fix
Guard the warning with isConnected (one line), or re-schedule the check until the element is connected:
const renderedHeight = img.clientHeight;
if (dir.fill && renderedHeight === 0 && img.isConnected) {
console.warn(/* NG02952 … */);
}
Note: main currently runs the callback immediately for already-loaded images via callOnLoadIfImageIsLoaded(img, callback). That path executes during directive init, which for embedded/deferred views can be before attachment, making the isConnected guard more important there.
Related
Please provide the environment you discovered this bug in
Angular CLI: 21.x
@angular/common: 21.2.16 (also verified unchanged on current `main`)
Node: 20.x
Browser: Chromium (dev mode; the check is ngDevMode-only)
App: SSR + hydration (incremental), @defer sections, zoneless
Anything else?
Happy to provide a StackBlitz reproduction or a PR with the isConnected guard + test if the team agrees with the direction.
Which @angular/* package(s) are the source of the bug?
common (NgOptimizedImage)
Is this a regression?
No — the check has behaved this way since it was introduced. Note that
mainrecently addedcallOnLoadIfImageIsLoaded(img, callback), which widens the window for this false positive (see analysis below).Description
assertNonZeroRenderedHeightemits NG02952 — "the height of the fill-mode image is zero" whenever a fill-mode image firesloadwhileimg.clientHeight === 0. However,clientHeightis also0when the element is simply not connected to the document yet — which legitimately happens when a memory-cached image finishes loading before Angular attaches the view.Real-world scenario where we hit this reliably (e-commerce home page):
@defersections (e.g., the same product rendered in two different carousels, or the same CMS cover in two branches).NgOptimizedImagesetssrc, and the browser serves the image from memory cache — theloadevent fires before the embedded view is attached to the document.clientHeight === 0→ NG02952 warns, telling the developer to fixposition/height on a container that is actually correctly styled.The warning is therefore a false positive: it fires in bursts (10+ at once, one per duplicated cached image), is machine/timing dependent (fast cache + busy main thread), and points developers at CSS that is not broken. We spent significant time chasing phantom "zero-height containers" before isolating the real cause.
Evidence that the warned images are detached at load time
We instrumented the page with a document-level capture-phase
loadlistener (resourceloadevents do not bubble, but they do traverse the capture path — this is the same technique Angular-adjacent image-fallback utilities use):Result, repeated across many reloads:
loadevents never traverseddocument— impossible unless the element had no ancestor chain at dispatch time (i.e., it was detached; a connected<img>'sloadreliably reaches a document capture listener, which we verified separately).complete === true,isConnected === true, with correct non-zero heights — they had attached and laid out normally right after the warning.Reproduction (minimal)
The container is correctly styled (
position: relative, fixed non-zero height), yet instance B intermittently logs NG02952 when the cachedloadwins the race against view attachment. Reproduction is timing-sensitive (more likely with CPU throttling / busy main thread / many duplicated deferred instances). We can provide a StackBlitz on request.Expected behavior
The zero-height heuristic should not warn for elements that are not connected to the document at the moment the check runs — a detached element always measures
clientHeight === 0regardless of how correct the CSS is.Actual behavior
…in bursts, for images whose containers are correctly positioned and sized.
Proposed fix
Guard the warning with
isConnected(one line), or re-schedule the check until the element is connected:Note:
maincurrently runs the callback immediately for already-loaded images viacallOnLoadIfImageIsLoaded(img, callback). That path executes during directive init, which for embedded/deferred views can be before attachment, making theisConnectedguard more important there.Related
Please provide the environment you discovered this bug in
Anything else?
Happy to provide a StackBlitz reproduction or a PR with the
isConnectedguard + test if the team agrees with the direction.