Pre-load the back side of the card

Hello, Anki forum!
I would like to share an improvement proposal.
It takes some time to render HTML of the Back side of the card (especially if it has embedded elements).
I believe we can improve user experience if the Back side of the card is pre-uploaded as soon as the Front side of the card is shown. So that the back side is fully rendered as soon as we press Show button.
I’ve created a screencast to better explain the proposal:

1 Like

Hey Anatolii, thanks for posting this, I was about to submit a feature request related to this too.

@dae, @hengiesel Is this something that would be difficult to implement?
(Forgive me if I’m wrong, I’m no programmer myself.)

If not pre-loading the whole back of the card, could you enable pre-loading any images or media that is present in the back of the card? I ask this because I have a pretty alright computer, yet it takes a noticeable amount of time to render images that are stored locally when I flip the card. (Though this is prominent only with higher-res images)

Interestingly, I don’t notice such a delay when reviewing the same cards with AnkiDroid. Though I’m unsure if that’s because my phone uses solid-state storage versus my computer’s hard-drive; or because AnkiDroid preloads the images.

Are you on Windows? I recently switched to Linux and was quite surprised to see Anki performing almost as well on my Laptop as on my phone.

The problem with Windows

On Windows, Anki’s webview seems to suffer from memory leaks, regardless which video driver you choose, so it will most likely perform poorly compared to macOS or Linux.


Regarding the delay for the back side:

The load time depends on your template.
You should be able to reduce the delay if you defer the loading of your images (and iframes) to the end of the event loop with setTimeout(). Here is an example for images:
(Please correct me if I’m wrong! I found it difficult to test - even with dozens of hires images - because Linux is so snappy :smiley:)

{{FrontSide}}

<hr id=answer>

<div id="content">{{Back}}</div>

<script>
var images = document.getElementById("content").querySelectorAll("img")
var sources = []

for (img of images) {
  sources.push(img.src)
  img.src = ""
}

setTimeout(function() {
  for (img of images) img.src = sources.pop()
}, 0)
</script>

You should also only call heavy JavaScript functions when you really need them. I hide most of the card’s content by default and only show it after a tap/click anywhere on the screen. This way you hit two birds with one stone: You adhere to the minimum-information principle and hide the ugliness of “lazy loading” most of your content :slight_smile:

1 Like

It seems that @dae and @hengiesel are currently working on improving reviewer.ts, so such a feature might be implemented in the future.

But in fact, limited and partial preloading is already possible using javascript in a card template. By dynamically creating a link element (<link rel="preload">) and inserting it into the header, some types of content in the backside can be preloaded while the frontside is being displayed, although it is not possible to preload the frontside of the next card.

Here is an example of a script that will preload the image files in the “Back” field.

Front Template:

{{Front}}

<script>
    function insertPreloadLink(href, contentType) {
        if (!document.head.querySelector(`link[rel="preload"][href="${href}"]`)) {
            const preloadLink = document.createElement("link");
            preloadLink.href = href;
            preloadLink.rel = "preload";
            preloadLink.as = contentType;
            document.head.appendChild(preloadLink);
        }
    }

    function preloadImages(html) {
        const fragment = document.createDocumentFragment();
        const div = document.createElement("div");
        fragment.appendChild(div);
        div.innerHTML = html;
        fragment.querySelectorAll("img").forEach((img) => {
            insertPreloadLink(img.src, "image");
        });
    }

    // Run only on the question side and only on Anki desktop
    setTimeout(() => {
        if (window["ankiPlatform"] === "desktop" && !document.getElementById("answer")) {
            preloadImages(`{{Back}}`);
        }
    }, 0);
</script>

For testing purposes, I created a card with a number of very large file size images on the backside, and used the above script to display the card, which improved the performance of the card display noticeably.

1 Like

@hkr Wouldn’t that cause the children of the head element to grow indefinitely?

Pre-loading of images in some form is an option for the future. Rendering the entire back side in advance is tricky with the current design unfortunately, due to some of the issues mentioned in the GitHub link above.

1 Like

@kleinerpirat Your script does improve the loading time, thanks! Though there’s still a noticeable loading time, as opposed to @hkr’s script. And yes, I am on windows.

I do the same, using hint fields in the back of the card; but I found myself opening one particular field with extra content very often and decided to make that a non-hint field.



@hkr Your script pretty makes the loading time imperceptible, thank you!

@dae, I checked using the webview inspector, and yes, as I keep reviewing, the preload link elements with the images keep accumulating in the head.

@hkr, to prevent the elements from accumulating in the head, could a function be added to remove all link elements present in the head (from the previous card), and only then append the new link elements?

I tried doing so by modifying your script by:

  1. Adding a className of “fastImage” to the appended link elements.
  2. Selecting with get elementsby classname,
  3. using removeChild to delete elements

Here is the modified script, where am I going wrong?

<script>
    function insertPreloadLink(href, contentType) {
        if (!document.head.querySelector(`link[rel="preload"][href="${href}"]`)) {
            const preloadLink = document.createElement("link");
            preloadLink.href = href;
            preloadLink.rel = "preload";
            preloadLink.as = contentType;
			preloadLink.className = "fastImage";
            document.head.appendChild(preloadLink);
        }
    }

    function preloadImages(html) {
        const fragment = document.createDocumentFragment();
        const div = document.createElement("div");
        fragment.appendChild(div);
        div.innerHTML = html;
        fragment.querySelectorAll("img").forEach((img) => {
            insertPreloadLink(img.src, "image");
        });
    }
	
function removeElementsByClass(fastImage){
    const elements = document.getElementsByClassName(fastImage);
    while(elements.length > 0){
        elements[0].parentNode.removeChild(elements[0]);
    }
}
    // Run only on the question side and only on Anki desktop
    setTimeout(() => {
        if (window["ankiPlatform"] === "desktop" && !document.getElementById("answer")) {
			removeElementsByClass();
            preloadImages(`{{Extra}}`);
            preloadImages(`{{First Aid}}`);
            preloadImages(`{{Pathoma}}`);
            preloadImages(`{{Lecture Notes}}`);
			preloadImages(`{{Boards and Beyond}}`);
			preloadImages(`{{Sketchy}}`);
			preloadImages(`{{Physeo}}`);
			preloadImages(`{{Additional Resources}}`);
			}
    }, 0);
</script>


When that didn’t work, I changed document.head.appendChild(preloadLink); to document.getElementById("qa").appendChild(preloadLink);, because the div with the id qa seems to get refreshed each time a card is loaded or flipped. Is there anything wrong with this?

1 Like

Yes, that’ s right. Thank you for pointing out my mistake. I should have done some more testing and noticed that issue before posting.

I think it would be simpler to append <link> elements to the #qa as you are doing, and that should work. I just thought that in general it would be preferable to put <link> elements inside the <head> section.

I have modified the script as follows:

  • First remove all the preload link elements added in the previous card, and then insert new ones.
  • Now use content property of <template> element to get a field string instead of directly enclosing a field with backticks, just in case a field contains a backtick.
{{Front}}

<template class="template-preload">
    {{Field 1}}
    {{Field 2}}
    {{Field 3}}
</template>

<script>
    function insertPreloadLink(href, contentType) {
        const preloadLink = document.createElement("link");
        preloadLink.href = href;
        preloadLink.rel = "preload";
        preloadLink.as = contentType;
        preloadLink.classList.add("preload-link");
        document.head.appendChild(preloadLink);
    }

    function removePreloadLinks() {
        document.head
            .querySelectorAll("link.preload-link")
            .forEach((link) => link.remove());
    }

    function preloadImages() {
        const tmpl = document.querySelector(".template-preload");
        tmpl.content.querySelectorAll("img").forEach((img) => {
            insertPreloadLink(img.src, "image");
        });
    }

    // Run only on the question side and only on Anki desktop
    setTimeout(() => {
        if (window["ankiPlatform"] === "desktop" && !document.getElementById("answer")) {
            removePreloadLinks();
            preloadImages();
        }
    }, 0);
</script>
2 Likes

Thanks for the detailed answer, hkr! :slight_smile: