ContentProvider doesn't preload ImageLabels

As described in the post below, ImageLabels can’t be preloaded expected using ContentProvider’s ‘LoadAsync’:

While a workaround in said post is provided, this way of preloading is hacky and will likely be missed by most users.

Reproduction Steps:

  • Create a disabled ‘ScreenGui’ with a frame inside containing one or multiple ImageLabels (in my case grouped under an images folder).
  • Preload the ImageLabel/s using ‘ContentProvider:PreloadAsync()’.
  • Enable the ‘StarterGui’ once the player has loaded and ‘PreloadAsync’ is done yielding, this will result in the images not being visible, their ‘.IsLoaded’ property being false, and will take a couple seconds to render.

For my use case this makes it incredibly tedious to handle spritesheet playback, as it requires me to create an ImageLabel for each frame just to parent it to a temporary ‘ScreenGui’ with an ‘ImageTransparency’ of ‘0.99’, as not doing so makes it so 100% of the times the spritesheet plays for the first time, it doesn’t display, as none of it’s images have time to load.

Expected behavior

I expect ImageLabels to display immediately when they have been preloaded already with ‘ContentProvider:PreloadAsync’.

A private message is associated with this bug report

2 Likes

Thank you for your report. We will create a feature request for this.

4 Likes

If supporting Decals but not ImageLabels is not a bug, can the Limitations section of the :PreloadAsync documentation be updated to say so?

3 Likes

Hey @JustBorgar , just to double check - by disabled StarterGuis, do you mean disabled ScreenGuis? There doesn’t seem to be a “disable” option on StarterGuis.

Oh sorry that’s a typo on my part, I meant ScreenGuis (I’ve gone ahead and edited the post).

1 Like

I have been unable to reproduce this issue. I followed the reproduction steps, and added the following code:

local contentProvider:ContentProvider = game:GetService("ContentProvider")
local screenGui:ScreenGui = script.Parent
local frame:Frame = screenGui["Frame"]

local imageLabels:{ImageLabel} = {}
local assetIds:{string} = {}
for _, instance:Instance in frame:GetChildren() do
	if instance:IsA("ImageLabel") then
		table.insert(imageLabels, instance)
		table.insert(assetIds, instance.Image)
	end
end

print("Calling PreloadAsync")
contentProvider:PreloadAsync(imageLabels, function(assetId:string, fetchStatus:Enum.AssetFetchStatus)
	print("PreloadAsync callback; assetId=" .. assetId .. "; fetch status=" .. fetchStatus.Name)
end)
--contentProvider:PreloadAsync(assetIds, function(assetId:string, fetchStatus:Enum.AssetFetchStatus)
--	print("PreloadAsync callback; assetId=" .. assetId .. "; fetch status=" .. fetchStatus.Name)
--end)
print("After PreloadAsync")

print("Enabling ScreenGui")
screenGui.Enabled = true

I observe that PreloadAsync takes a while to load all images, but once it yields and ScreenGui gets enabled, all ImageLabels display their images immediately.

If you can prepare a minimal place that reliably reproduces the issue, that would be most helpful.

Here’s a repro with 2 examples:
Repro.rbxl (63.2 KB)

  • Example 1 preloads the images by Id, and technically returns the callback, but it doesn’t actually preload them.
  • Example 2 generates an ImageId per image and once again, doesn’t preload, in this case resulting in the callbacks triggering and run never being called as the ImageIds aren’t made visible until preloaded.

Edit: Original repro had an oversight on the 2nd example, fixed it in the first edit, added an IsLoaded print on the 2nd showcasing it’s not preloaded.

Again ImageLabels do load if visible, it’s just that PreloadAsync doesn’t seem to actually preload them, so the loading process doesn’t start until they are rendered (thus the workaround in the post linked above).

Below are 2 other posts reporting the same issue:

2 Likes

Thank you! I have added some code to print ImageLabel.IsLoaded and observe the following.

In Example 1, PreloadAsync indeed doesn’t preload images as expected, the images don’t display until the 2nd loop.

In Example 2, according to IsLoaded the images only load after being rendered once, however the image do display correctly on the 1st loop.

It’s possible IsLoaded is inaccurate in example 2’s scenario. If we disregard IsLoaded and only look at the render, it looks like PreloadAsync works as expected in example 2. Could you double check whether you observe the same? This is important for us to determine where the exact problem(s) is.

Also, if on Windows, you can delete all subdirectories in %TEMP%/Roblox and restart Studio to clear its cache, forcing it to redownload all images.

I’ve attached the Id list I used to the private message (simply replace the table in the 2nd example for that one), from what I can tell it has a failure rate of 100% to display the images the first time fine (by the looks of it ImageLabels are able to display a handful of images fine even if not preloaded (such as the case in the repo), but if you need to preload 200+ images it takes a while to load them, even if preloaded prior).

I’m on linux, so there’s a slight chance this is a vinegar issue, but I can give it a go if you are unable to reproduce it on your end.

Thank you, with more images I can reproduce the issue on both examples now. We will continue looking into it.

1 Like

I have identified the root cause of this issue and am working on a fix. However, it affects many parts of the Roblox engine. Out of concern of the platform’s stability over the holiday season, I plan to roll out the fix in early 2026, in case it causes more bugs. Thank you for your patience.

In the meanwhile, please make sure to call PreloadAsync with an array of Instances whenever possible. Calling with an array of asset ID strings is generally not supported; see ContentProvider:PreloadAsync and Preload update.

7 Likes

Is there any other alternative? My games are entirely 2D, so the amount of images I’d have to clone would definitely cause lag on the client.

I’m not 100% sure this will be an improvement, but consider combining your individual textures into sprite sheets, like this one. Then, use the ImageRectOffset and ImageRectSize properties on ImageLabel to display only part of that sprite sheet.

This is a common practice in games, which reduces the number of assets to load, and may perform better when PreloadAsync doesn’t work as expected.

2 Likes

Thank you! I’ve applied this logic to some of the smaller assets, and it works just fine. However, the larger assets exceed the 8192-pixel size limit when formed into a spritesheet, so I still have to clone them. Hopefully, ContentProvider is fixed soon

Unfortunately we are unable to fix this issue at this time.

To give more context, as long as you pass in instances not string IDs, PreloadAsync has always worked. However, the Roblox engine also aggressively seeks and releases unused assets, i.e. garbage collection. This saves memory and reduces out-of-memory crashes. Therefore, if you preload ImageLabels but don’t make them visible, the engine will think these textures are unused and collect them, therefore cancelling the effect of PreloadAsync. We realize this defeats the purpose of preloading, but that’s where things stand right now.

When I said I was working on a fix, it was to forcibly mark preloaded textures as used, preventing collection. However this is at odds with conserving memory, so we can’t move forward with it.

We currently suggest you use the workarounds linked earlier, or merge sprites into a single texture.

This issue remains in our backlog for now. I am a creator myself and also frustrated by it. I will continue looking for ways to properly fix it, but don’t have any estimate.

6 Likes

2d developers should js look for a different engine

1 Like

If you are not too concerned about memory you can put all the images you want to preload inside of a CanvasGroup and set the group transparency to 1 (the canvas group also has to be set to Visible = true), just make sure that all the images under the canvas group that you want preloaded are not transparent, are set to visible and are not off-screen.

1 Like

My current solution is to parent the imagelabel to the playergui then quickly unparent it to kind of “load” it without ContentProvider. Then I later put it there again when I need to and its loaded