anki icon indicating copy to clipboard operation
anki copied to clipboard

Revisit async image decoding/preloading

Open dae opened this issue 3 months ago • 2 comments

A few users have reported they feel #4320 was a regression.

That got me wondering about why we're seeing a difference. I gather that even with async decoding, the image loading promise should not resolve until the images have been fully decoded, so I would have thought our preloading code would still be working. If I had to guess at what's going, perhaps the switch to async decoding is causing the images to take longer to load (as other layout work gets preferentially run). preload.ts only waits 200ms for preloading, so perhaps that's no longer sufficient. Increasing it might reduce the flicker some users are seeing, though perhaps result in slower card transitions again?

@Luc-Mcgrady is currently looking into porting our review screen to Svelte, so it might be a good time to be thinking about this. There are perhaps alternative ways we could be preloading/caching, such as preloading the next card's resources?

dae avatar Oct 01 '25 06:10 dae

I was using this script on the back side of the card for async decoding of images that I got from somewhere on some forum, with a previous version of Anki. (25.02.5)

It completely fixed the slow transition to the back side of cards containing large images, and was significantly (10x in my estimate) faster at loading the images as well, compared to 25.09.2. It doesn't seem to be working with 25.09.2 as the performance remains slow with or without the script.

(() => {
  // 1) Reserve layout so text can paint now (no reflow later)
  document.querySelectorAll("img").forEach(img => {
    // If width/height missing, try to infer from style or add a sane default
    if (!img.hasAttribute("width") && !img.hasAttribute("height")) {
      // Prefer CSS width if present; else keep intrinsic aspect via aspect-ratio if you know it
      // Minimal safe default so we don't collapse to 0 and cause reflow
      img.style.minWidth = img.style.minWidth || "200px";
      img.style.minHeight = img.style.minHeight || "150px";
      // If you know your typical aspect, use:
      // img.style.aspectRatio = "4 / 3";
    }

    // Move src -> data-src to avoid decode on first paint
    const src = img.getAttribute("src");
    if (src) {
      img.setAttribute("data-src", src);
      img.removeAttribute("src");
    }

    // Make decode non-blocking and optional nice fade-in
    img.setAttribute("decoding", "async");
    img.style.opacity = "0";
    img.style.transition = "opacity 120ms ease-out";
    img.addEventListener("load", () => { img.style.opacity = "1"; }, { once: true });
  });

  // 2) After initial paint, kick images back in
  requestAnimationFrame(() => {
    // rAF ensures at least one frame with text only
    document.querySelectorAll("img[data-src]").forEach(img => {
      img.setAttribute("src", img.getAttribute("data-src"));
      img.removeAttribute("data-src");
    });
  });
})();

I'm unsure how different this is from #4320

Also, even AnkiWeb with firefox renders the images significantly faster than 25.09.2, despite the images not even being pulled from local storage.

talenelat-elin avatar Nov 04 '25 15:11 talenelat-elin

such as preloading the next card's resources?

I've made an attempt at this in the above linked commit. (https://github.com/Luc-Mcgrady/anki/commit/6d35ce61bed1a9b07c8c442f1eca61618873a407)

Luc-Mcgrady avatar Nov 27 '25 07:11 Luc-Mcgrady