Problem/Motivation

In FN.loadImage() (js/src/base/io/bio.media.js), the following early-exit is taken whenever the currently displayed placeholder is already considered decoded by the browser:

// Bail out early if already loaded.
if (!isBg && $.isDecoded(el)) {
  load(el, true);
  return;
}

$.isDecoded() checks el.naturalWidth !== 0 on el itself — i.e. on the placeholder image that is still assigned to [src], not on the real target image referenced by [data-src]. An inline data:image/svg+xml placeholder is synchronously available, so depending on the placeholder's viewBox/dimensions and the browser's default-object-size algorithm, naturalWidth can already be non-zero the instant the element enters the viewport.

When that happens, load(el, true) is called immediately. This marks the element as successfully loaded (b-loaded class via $.status()), but the actual swap logic — $.mapAttr(el, ['srcset', 'src'], false) inside preload() — is never reached, because that code only runs in the else branch (the new Image() / $.decode() path). The result: [data-src] is silently dropped, [src] keeps showing the placeholder forever, and the element is nevertheless flagged as loaded.

This is most reliably reproducible with plain <img class="b-lazy" data-src="…"> elements whose placeholder is a generic viewBox="0 0 1 1" SVG (e.g. icons rendered without going through the responsive-image/thumbnail pipeline, so Placeholder::generate() falls back to 1×1). We noticed it visibly on manually-lazyloaded SVG icons (social/payment logos), but the underlying condition is not SVG-specific — any placeholder that the browser happens to report as already "decoded" for its current [src] before the real image was ever assigned will trigger it.

Steps to reproduce

  1. Render an <img class="b-lazy" data-src="/path/to/real-image.svg" src="data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20viewBox%3D'0%200%201%201'%2F%3E"> (no loading attribute, so it goes through the IO path, not the native-loading shortcut).
  2. Scroll it into view.
  3. Inspect the element after Blazy has processed it: class list contains b-loaded, but [src] is unchanged (still the placeholder), [data-src] has been removed.

Proposed resolution

Only take the "already decoded" shortcut when there is no pending [data-src]/[data-srcset] swap:

-    if (!isBg && $.isDecoded(el)) {
+    if (!isBg && !isDataset && $.isDecoded(el)) {
       load(el, true);
       return;
     }

(isDataset is already computed a few lines above via $.hasAttr(el, VARS.dataSrc).)

Patch attached against 3.0.19 for both js/src/base/io/bio.media.js and the shipped js/base/io/bio.media.min.js.

Remaining tasks

  • Maintainer review of the proposed condition, in case there is a reason the "already decoded" shortcut was meant to also cover elements with a pending [data-src].
  • No automated test currently covers this path (placeholder reported as decoded before the real image is assigned); adding one would help prevent a regression.

User interface changes

None.

API changes

None.

Data model changes

None.

CommentFileSizeAuthor
svg-icon-isdecoded-shortcut.patch5.89 KBhammerslammer

Comments

hammerslammer created an issue. See original summary.

hammerslammer’s picture

Issue summary: View changes