Open Bug 2011480 Opened 5 months ago Updated 2 months ago

www.manucurist.com - Product images get displayed initially but appear stuck loading when scrolling back up

Categories

(Web Compatibility :: Site Reports, defect, P1)

ARM
Android

Tracking

(Webcompat Priority:P1, Webcompat Score:8)

Webcompat Priority P1
Webcompat Score 8

People

(Reporter: ctanase, Unassigned)

References

(Depends on 1 open bug, )

Details

(Keywords: webcompat:platform-bug, webcompat:site-report, Whiteboard: [webcompat-source:web-bugs])

User Story

user-impact-score:800
platform:windows,mac,linux,android
impact:workflow-broken
configuration:general
affects:all
branch:release
diagnosis-team:dom

Attachments

(4 files)

Environment:
Operating system: Android 12
Firefox version: Firefox Mobile 147.0/149

Steps to reproduce:

  1. Go to https://www.manucurist.com/it/collections/couleurs-vernis-green-flash
  2. Scroll past a few products and then back up.

Expected Behavior:
Product images are still displayed.

Actual Behavior:
Product images appear to be stuck loading.

Notes:

  • It reproduces on desktop as well for some products when loading the page
  • Reproduces regardless of the status of ETP
  • Reproduces in firefox-nightly, and firefox-release
  • Does not reproduce in chrome

Created from https://github.com/webcompat/web-bugs/issues/202200

Attached image desktop behavior.png

On desktop it reproduces only for some products when the page is loaded, no scrolling needed to reproduce.

User Story: (updated)
Webcompat Score: --- → 1
Severity: -- → S2
User Story: (updated)
Webcompat Priority: --- → P1
Webcompat Score: 1 → 8
Priority: -- → P1

Can't repro on mobile nor on desktop today, can you still repro this?

Flags: needinfo?(ctanase)

Yes, on both mobile and desktop on the latest Nightly.

Flags: needinfo?(ctanase)

Hmm, on Linux desktop I see this...

It looks like image loading is triggered by an intersection observer.

But it seems like the bigger problem is that the containing <a> has loading class.

handleLazy in https://www.manucurist.com/cdn/shop/t/72/assets/lazyimage.js is where this stuff gets done.

Each image has:

<card-gallery>
  <div class="image-animate media media--adapt media--hover-effect" style="--image-ratio-percent: 100.0%;">
    <img src="//www.manucurist.com/cdn/shop/files/rainbow-gif-carre.gif?v=1768834912&amp;width=1100" alt="Rainbow da personalizzare Green Flash™" srcset="//www.manucurist.com/cdn/shop/files/rainbow-gif-carre.gif?v=1768834912&amp;width=165 165w, //www.manucurist.com/cdn/shop/files/rainbow-gif-carre.gif?v=1768834912&amp;width=360 360w, //www.manucurist.com/cdn/shop/files/rainbow-gif-carre.gif?v=1768834912&amp;width=535 535w, //www.manucurist.com/cdn/shop/files/rainbow-gif-carre.gif?v=1768834912&amp;width=750 750w, //www.manucurist.com/cdn/shop/files/rainbow-gif-carre.gif?v=1768834912&amp;width=940 940w, //www.manucurist.com/cdn/shop/files/rainbow-gif-carre.gif?v=1768834912&amp;width=1100 1100w" width="1100" height="1100" loading="eager" class="motion-reduce" sizes="(min-width: 1600px) 367px, (min-width: 990px) calc((100vw - 10rem) / 4), (min-width: 750px) calc((100vw - 10rem) / 3), calc((100vw - 3rem) / 2)" is="lazy-image">
    <img src="//www.manucurist.com/cdn/shop/files/DUCROS_Q230615-MANUCURIST_022_0657_Adb98_83e6f6e8-f716-4553-a403-7509ece4ab4f.jpg?v=1768834912&amp;width=1100" alt="Rainbow da personalizzare Green Flash™" srcset="//www.manucurist.com/cdn/shop/files/DUCROS_Q230615-MANUCURIST_022_0657_Adb98_83e6f6e8-f716-4553-a403-7509ece4ab4f.jpg?v=1768834912&amp;width=165 165w, //www.manucurist.com/cdn/shop/files/DUCROS_Q230615-MANUCURIST_022_0657_Adb98_83e6f6e8-f716-4553-a403-7509ece4ab4f.jpg?v=1768834912&amp;width=360 360w, //www.manucurist.com/cdn/shop/files/DUCROS_Q230615-MANUCURIST_022_0657_Adb98_83e6f6e8-f716-4553-a403-7509ece4ab4f.jpg?v=1768834912&amp;width=535 535w, //www.manucurist.com/cdn/shop/files/DUCROS_Q230615-MANUCURIST_022_0657_Adb98_83e6f6e8-f716-4553-a403-7509ece4ab4f.jpg?v=1768834912&amp;width=750 750w, //www.manucurist.com/cdn/shop/files/DUCROS_Q230615-MANUCURIST_022_0657_Adb98_83e6f6e8-f716-4553-a403-7509ece4ab4f.jpg?v=1768834912&amp;width=940 940w, //www.manucurist.com/cdn/shop/files/DUCROS_Q230615-MANUCURIST_022_0657_Adb98_83e6f6e8-f716-4553-a403-7509ece4ab4f.jpg?v=1768834912&amp;width=1100 1100w" width="1100" height="1100" loading="lazy" class="motion-reduce" sizes="(min-width: 1600px) 367px, (min-width: 990px) calc((100vw - 10rem) / 4), (min-width: 750px) calc((100vw - 10rem) / 3), calc((100vw - 3rem) / 2)" is="lazy-image">
  </div>
  <div class="image-animate media no-js-hidden media--adapt media--hover-effect medium-hide large-up-hide" style="--image-ratio-percent: 100.0%;">
    <img src="//www.manucurist.com/cdn/shop/files/DUCROS_Q230615-MANUCURIST_022_0657_Adb98_83e6f6e8-f716-4553-a403-7509ece4ab4f.jpg?v=1768834912&amp;width=1100" alt="Rainbow da personalizzare Green Flash™" srcset="//www.manucurist.com/cdn/shop/files/DUCROS_Q230615-MANUCURIST_022_0657_Adb98_83e6f6e8-f716-4553-a403-7509ece4ab4f.jpg?v=1768834912&amp;width=165 165w, //www.manucurist.com/cdn/shop/files/DUCROS_Q230615-MANUCURIST_022_0657_Adb98_83e6f6e8-f716-4553-a403-7509ece4ab4f.jpg?v=1768834912&amp;width=360 360w, //www.manucurist.com/cdn/shop/files/DUCROS_Q230615-MANUCURIST_022_0657_Adb98_83e6f6e8-f716-4553-a403-7509ece4ab4f.jpg?v=1768834912&amp;width=535 535w, //www.manucurist.com/cdn/shop/files/DUCROS_Q230615-MANUCURIST_022_0657_Adb98_83e6f6e8-f716-4553-a403-7509ece4ab4f.jpg?v=1768834912&amp;width=750 750w, //www.manucurist.com/cdn/shop/files/DUCROS_Q230615-MANUCURIST_022_0657_Adb98_83e6f6e8-f716-4553-a403-7509ece4ab4f.jpg?v=1768834912&amp;width=940 940w, //www.manucurist.com/cdn/shop/files/DUCROS_Q230615-MANUCURIST_022_0657_Adb98_83e6f6e8-f716-4553-a403-7509ece4ab4f.jpg?v=1768834912&amp;width=1100 1100w" width="1100" height="1100" loading="lazy" class="motion-reduce" sizes="(min-width: 1600px) 367px, (min-width: 990px) calc((100vw - 10rem) / 4), (min-width: 750px) calc((100vw - 10rem) / 3), calc((100vw - 3rem) / 2)" is="lazy-image">
  </div>
</card-gallery>

is="lazy-image". FWIW this seems to only happen without image cache, so after some attempt to reproduce you need to clear the cache to reproduce it again. (devtools no-cache option doesn't seem to apply for images?)

FWIW, lazyimage.js has some WebReflection's libraries:

Those polyfills always override window.customElements.whenDefined regardless of browser support (which is fair and expected for polyfills).

And then its code:

class LazyImage extends HTMLImageElement {
  constructor() {
    super();

    this.wrapper = this.closest('.media-wrapper');
    if (this.wrapper === null) return;

    this.handleLazy();
    addEventListener('resize', this.handleLazy.bind(this), true);

    const observer = new MutationObserver((changes) => {
      changes.forEach((change) => {
        if (change.attributeName.includes('src') || change.attributeName.includes('srcset')) {
          this.handleLazy();
        }
      });
    });
    observer.observe(this, {attributes : true});
  }

  handleLazy () {
    const mql = window.matchMedia('(min-width: 750px)');
    if (mql.matches && this.classList.contains('medium-hide')) return;
    if (!mql.matches && this.classList.contains('small-hide')) return;
    if (this.complete || this.classList.contains('loaded')) return;
    
    this.wrapper.classList.add('loading');
    this.addEventListener('load', () => {
      const loaded = () => {
        this.classList.add('loaded');
        this.wrapper.classList.remove('loading');
      };

      window.requestIdleCallback ? window.requestIdleCallback(loaded, {timeout: 150}) : setTimeout(loaded);
    }, false);
  }
}
window.customElements.define('lazy-image', LazyImage, { extends: 'img' });

So handleLazy() is called on construction. If it's .complete then it returns and does nothing else. That's problematic as the this.wrapper.classList.remove('loading') part is not called even though it's complete.

I see that branch also runs on Chrome, and I can confirm it also affects Chrome.

Hmm, on mobile, something else removes src and that causes reloading based on srcset.

https://www.manucurist.com/cdn/shop/t/72/assets/global.js?v=24674843534642605641764679293

class CardGallery extends HTMLElement {
  constructor() {
    super();

    theme.initWhenVisible({
      element: this,
      callback: this.init.bind(this),
      threshold: 200
    });
    this.matchMedia();
  }

  init() {
    if (!theme.config.mqlSmall) return;
    this.flickity = new Flickity(this, {
      accessibility: false,
      rightToLeft: theme.config.rtl,
      prevNextButtons: false,
      pageDots: true,
      on: {
        ready: () => {
          this.querySelectorAll('img').forEach((image) => {
            // Force lazy image reload
            image.removeAttribute('src');
          })
        }
      }
    })
  }

initWhenVisible works through intersection observer, as Jeff mentioned in comment #6. But I see this always called properly on mobile, and cannot reproduce comment #1.

Hmm wait, I see it on tablet but not on my phone.

The repro requires scroll-to-hide-toolbar customization to be on. (Which is by default. I disabled it because it's so annoying...)

Ok, I see what's happening.

From comment #10, you see there's a resize listener. Scrolling up with scroll-to-hide-toolbar triggers resize event, and the listener is handleLazy. And from comment #9 you see that each card-gallery has multiple images, with the second being hidden by display: none (it's for mouse hover).

handleLazy is called for both. As the first image is already loaded, the early return happens and nothing goes wrong. The culprit is in the second image. We do not load the second image at all, so .complete remains false. That means the early return does not happen, so handleLazy goes on and add loading class to its ancestor, causing both images to be hidden. Now given the first image is already loaded and the second image never loads, the load listener never runs and the loading class stays forever.

Interestingly Chrome loads the second image and has .complete being true.

Attached file 2014480.html

not a minimal repro but to check on mobile...

Okay. That third image I initially missed in comment #9 was the key.

The first and third are non-hidden, only second is hidden. And the second and third have the same image sources.

With loading=lazy, the third is loaded on both Firefox and Chrome, which effectively loads the second too. Then comment #13 comes in and removes src for all image elements, including the second image.

Boom! Somehow Chrome then sets .complete to true on the second image element, as the resource is technically loaded. Not on Firefox. Thus only Firefox suffers from the problem in comment #16.

I need some good ice cream and beer and whatever by debugging this hell.

The chrome behavior makes zero sense to me.

You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: