Drawing URLMedia on canvas, delays in first frame

I have a canvas that I’m drawing frames onto using a timer. Part of what I’m drawing are images referenced by URLMedia instances (URLMedia makes sense in the context of the larger app this is going to be part of).

The issue is that the first frame draws slowly, until it has drawn every image once, then it draws the rest of the frame quickly. Frames after that first one draw very quickly.

It seemed obvious to me that the delay was caused by the URLMedia lazily downloading the media. So I put the following code in the init, thinking it would predownload the images before I started drawing the first frame:

    self.grass_left = URLMedia('_/theme/grass_left.png')
    self.grass_right = URLMedia('_/theme/grass_right.png')
    self.grass_middle = URLMedia('_/theme/grass_middle.png')
    self.path_left = URLMedia('_/theme/path_left.png')
    self.path_right = URLMedia('_/theme/path_right.png')
    
    # Prefetch so the first frame doesn't draw slow
    # Why doesn't this work?
    self.grass_left.get_bytes()
    self.grass_right.get_bytes()
    self.grass_middle.get_bytes()
    self.path_left.get_bytes()
    self.path_right.get_bytes()

That did not work. The first frame still draws slowly until each image has been drawn once.

Here’s a clone link that shows the behavior (apologies for the strobe flashing of the frames, I put a delay between frames so I could verify that later frames did draw quickly): Anvil | Login

Can anyone explain why calling get_bytes() doesn’t prevent the first frame delay? Looking at the Anvil source it seems like get_bytes () should fetch the image from the URL and then cache it.

I don’t have an answer. I too would expect that calling get_bytes() would pre-fetch all the images, but perhaps getting bytes and drawing the image tickle two different sides of the image.

This seems to be a good workaround: if the first draw is the slow one, let’s make that first draw in advance, rather than calling get_bytes, and draw it outside of the screen:

    def form_show(self, **event_args):
        c = self.canvas_1
        for obj in self.terrain:
            if 'image' in obj:
                c.draw_image(obj['image'], 10000, 0)

        self.timer_1.interval = 0.05

Also, I noticed that you are playing with the canvas width and height in the __init__ rather than in the form_show. That can be dangerous, because in the __init__ some components may not yet know their size.

1 Like

Nice workaround! That does make the first frame smooth rather than choppy, thanks.

I also verified that the same URLMedia, drawn onto a second canvas, also draws quickly when I predraw on the first canvas. So there’s definitely something in drawing it on a canvas that does more than get_bytes in terms of caching the image.

Hopefully someone can explain why the prefetch with get_bytes isn’t working. I’d like to understand that interaction.

The prefetch for bytes is slightly different to the loading required for drawing it onto a canvas.
We actually don’t need the bytes for that.
But we do need to create an HTML Image Element for the canvas to draw and that’s what’s happening on the first render.
That’s why pre-drawing it on another canvas helps with caching.

You’ve probably done something like

dummy_canvas = Canvas()
dummy_canvas.draw_image(my_image)

Or, since it’s in the init method you can just use the main canvas.
(at this stage it has no height or width so it won’t be visible on load).

1 Like

Thanks for the explanation, that makes sense.