Skip to content

Preload

Preload warms every asset your game registered through ContentProvider:PreloadAsync in batches, narrating progress on the bus so a loading screen can render it. It’s one half of the kernel’s pay-instance-costs-once pair; the other half, Pool, recycles live Instances instead of warming assets ahead of time.

The framework already knows most of the assets your game will touch: AudioKit banks and AnimKit banks each expose assetIds(). Preload.run gathers those, plus any extra content-id strings or whole Instances you pass, dedupes the ids, and feeds them to ContentProvider:PreloadAsync in batches on a spawned thread:

  1. Gather + dedupe. String ids dedupe by value (an id in both a bank and your Assets list preloads once). Instances pass through untouched — PreloadAsync resolves their asset references itself.
  2. Batch. Slices of BatchSize (default 16) go to the preloader sequentially. Batching bounds how much one PreloadAsync call bites off, and a per-batch pcall isolates failures: a batch that throws is warned ([Preload] batch failed: ...) and skipped, and the pass continues with the next batch.
  3. Narrate. Every per-asset callback publishes Preload.Progress with running counts. Assets that fetch with a non-Success status are collected into Failed — a missing asset id never aborts the warm-up.
  4. Finish. Preload.Done publishes, the handle flips Done = true, and every thread blocked in await() resumes.

run() returns immediately with a live handle; await() is for flows that must block until warm (hold the loading screen, then start the round).

-- src/Client/Bootstrap.luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Preload = require(ReplicatedStorage.ChloeKernel.Preload)
return function(kernel)
local Gui = Instance.new("ScreenGui")
Gui.IgnoreGuiInset = true
local Bar = Instance.new("Frame")
Bar.Size = UDim2.fromScale(0, 0.02)
Bar.Position = UDim2.fromScale(0, 0.98)
Bar.BackgroundColor3 = Color3.fromRGB(145, 139, 255)
Bar.Parent = Gui
Gui.Parent = game:GetService("Players").LocalPlayer:WaitForChild("PlayerGui")
kernel.Bus:subscribe("Preload.Progress", function(_, loaded, total)
Bar.Size = UDim2.fromScale(loaded / total, 0.02)
end)
kernel.Bus:subscribe("Preload.Done", function(_, loaded, total, failed)
if #failed > 0 then
warn(`{#failed} assets failed to preload`)
end
Gui:Destroy()
end)
local Warmup = Preload.run(kernel, {
-- Banks = { Audio, Anims }: pass your AudioKit/AnimKit instances (anything with :assetIds())
Assets = { "rbxassetid://9046898403", "rbxassetid://129423030", workspace }, -- ids and/or whole Instances
})
Warmup:await() -- block this flow until warm; everything above already renders
end

The loading screen is also the right moment to run DeviceBench: Preload occupies the network while the bench uses the idle CPU.

Member Description
Preload.run(kernel, options?) → handle Publishes Preload.Started synchronously, then batches on a spawned thread. Needs kernel.Bus.

Options (all optional):

Field Default Description
Assets {} Content-id strings and/or Instances. Strings dedupe; Instances pass through.
Banks {} Anything with an :assetIds() → { string } method — AudioKit, AnimKit, or your own registry.
BatchSize 16 Assets per PreloadAsync call.
Preloader ContentProvider:PreloadAsync Injectable (assets, callback(assetId, ok)) for specs.

Handle:

Field / method Description
handle.Done true once every batch has run.
handle.Loaded Assets that have completed a fetch callback (success or failure).
handle.Total Asset count fixed at gather time.
handle.Failed Array of asset ids whose fetch status was not Success.
handle:await() Yields the calling coroutine until done; returns immediately if already done.
Topic Payload Fired
Preload.Started (total) Once, synchronously inside run() — subscribe before calling run or you miss it.
Preload.Progress (loaded, total, assetId, ok) Per asset, as each fetch resolves.
Preload.Done (loaded, total, failed) Once, after the last batch. failed is the array of failed ids.
  • Total is fixed when run() gathers — assets registered into a bank afterwards are not picked up. Register banks first, preload last.
  • Dedupe applies to string ids only. The same Instance passed twice is preloaded twice (harmless — the second fetch is a cache hit).
  • Multiple run() calls are independent passes with independent handles and bus narration; there is no global queue.