[images] Lazy loading and out of band loads.
What is the issue with the HTML Standard?
Consider this test-case (live):
<!doctype html>
<img style="display: none" onload="this.style.display = ''" loading="lazy">
<script>
const SRC = "https://wpt.live/images/green.png?" + Math.random();
document.querySelector("img").src = SRC;
setTimeout(() => {
new Image().src = SRC;
}, 3000);
</script>
WebKit and Blink somehow switch the lazy load to eager after the image loads (showing the green rectangle).
We found one website depending on this behavior in Mozilla bug 1922416.
AFAICT this just goes against the spec (and it's super bizarre)... Two questions:
- Would other implementations be open to change to match the spec? I think it's a rather obscure case, and behavior is bizarre enough, that it might be worth doing/trying.
- If not, we should probably get this spec'd, but I'm curious about how does this actually work? Is it
srcspecific or does it also apply topicture/srcset?
cc @zcorpan @annevk @mfreed7
So to be clear, this does not happen without the new Image().src assignment? That does seem strangely side-effecting.
cc @rwlbuis @nt1m
I have not debugged, but from a quick code inspection I suspect what happens is that the lazy load gets registered in the WebKit cached resources subsystem, and once the cached resource load finishes it causes notifications to all load observers, including the lazy load image.
So I think this is a side-effect of the WebKit cached resources subsystem. I do not know though if it is crucial to register lazy loads in the webkit cached resources subsystem, but I would not be surprised if things break without that.
I'd be curious to hear from Blink folks if they made a conscious decision about this and if they have same behaviour as WebKit because of a similar cached resources subsystem.
So to be clear, this does not happen without the new Image().src assignment? That does seem strangely side-effecting.
Correct, and agreed.
Chrome and Safari actually do agree with Firefox on the testcase under one condition: if I open the browser's network-DevTools panel and check the "Disable Caches" checkbox. (Under that configuration, Chrome and Safari render the testcase as blank.)
The fact that this checkbox gives different behavior vs. the "cache-skipping reload" (Shift+Reload) in those browsers seems unexpected/unwanted.
Chrome and Safari have even worse bugs here, see: https://issues.chromium.org/issues/375209498
@domfarolino, back in the day you used to work on lazy-loading images---would you be able to help with a Chromium and/or spec perspective here?
For Chromium, this is an artifact of two things:
- The fact that the decision to lazy load an image in Chrome is made too late in the loading pipeline. Specifically, it is made after the underlying loading infrastructure creates and registers a not-yet-populated
Resourceobject representing the deferred image - Resource coalescion: that is, when two "matching" resources are requested, they are sort of coalesced, and their sinks are both registered as clients to the same underlying
Resourceobject. Since thenew Image()"matches" the lazyloaded image, but is not lazyloaded, its load is not deferred. When it's finished it counts both images as loaded, since they were coalesced.
If the images didn't "match" (for example, the crossorigin credentials attribute didn't match), then we would not see the quirk @emilio points out. For example:
<!doctype html>
<img style="display: none" onload="this.style.display = ''" loading="lazy">
<script>
const SRC = "https://wpt.live/images/green.png?" + Math.random();
document.querySelector("img").src = SRC;
function run() {
const x = new Image();
x.crossOrigin = 'use-credentials'; // This is important.
x.src = SRC;
}
</script>
<button onclick="run()">Do the stuff</button>
Spec'ing the resource coalescing logic seems hard, and I'm not sure how beneficial it would be. That is, how web-observable is it? Besides lazyload, I don't know of other cases where one resource would not be expected to load, but would end up "force-loading" because of another "matching" one that got coalesced with it.
For Chromium, the shortest path to killing off this quirk might be to simply include lazyload status in the matching criteria, so that the resources in the original example would not "match", and therefore would not have side-effects on each other. I'd be curious to hear from @rwlbuis to see if this matches what could be done with WebKit.
FWIW I wrote a test for this: https://wpt.fyi/results/html/semantics/embedded-content/the-img-element/update-the-image-data/lazy-out-of-band-load.html
Extra Safari issue: https://wpt.fyi/results/html/semantics/embedded-content/the-img-element/update-the-image-data/src-then-lazy-load.html
FYI, I'm investigating this, and I think there's a simple fix for chromium.
Digging a bit further, I came across an interesting test.
This seems to muddy the issue: if an image resource has already been loaded via other means by the time 'loading="lazy"' is applied to an <img> it will load (and fire its load event) immediately; but if the image resource completes loading at a later time, then the lazy-loading behavior persists and it doesn't fire its load event right away.
This is pretty quirky, and I also wonder if it could be the basis of a timing attack.
@emilio WDYT?
That is what the spec says to do (the list of available images stuff is checked before entering the lazy load state).
@emilio Should we consider an update to the spec, or does this behavior seem OK to you?
Seems ok to me at a glance, it's consistent with how src loads synchronously if already in the available image list, and it prevents flickering when mutating the DOM? I added tests for that behavior in Mozilla bug 1709577.
I think this also looks right to me. This is very related to my comment above https://github.com/whatwg/html/issues/10671#issuecomment-2450198052, where I mention Chromium's quirks wrt the list of available image "matching", and where we make the decision to lazy load.
I filed a WebKit bug: https://bugs.webkit.org/show_bug.cgi?id=303993
Closing since it seems the spec should stay as-is.