Gift Effect Asset Delivery and Preloading: CDN, Versioning, Warmup, and Weak-Network Optimization
Learn how to deliver and preload SVGA, VAP, and PAG gift assets using CDN distribution, version control, priority caching, and weak-network fallback strategies.

The previous post covered the backend architecture — how messages get pushed, deduplicated, and made idempotent. But that system solves “how does the gift message reliably arrive.” There’s a prerequisite question it doesn’t answer: the moment a user sends a gift, is the effect asset already on the client?
Gift effects aren’t hardcoded. They’re SVGA / VAP / PAG asset packages, anywhere from a few hundred KB to several MB. If the client only starts downloading after the user taps “send,” the animation should have finished playing by the time the download completes — the user sees a blank space or a spinner instead.
This post breaks down the asset delivery and preloading pipeline: how asset packages are organized and versioned, how CDN delivers them efficiently, how the client preloads by priority, and how to guarantee “tap-to-play” under weak networks.

Heart of the Universe: a full-screen planetary effect — a few-hundred-KB-to-several-MB asset package that shows a black screen without preloading (XingJi Shop)
1. The problem: assets must be in place the instant a gift is sent
Gift effect playback has a hard constraint: the window from tap to first frame is extremely short. A user who sends “Blazing Warhorse” expects to see the horse charge across the screen immediately, not a 2-second loading spinner.
If the asset isn’t ready ahead of time, three bad experiences follow:
Black screen / blank: the effect container has popped up, but the asset is still downloading, so the playback area is empty.
First-frame delay: the effect arrives only after download + decode finish, wrecking the timing.
Lost playback: the download times out and the effect never plays — the money the user spent is “invisible.”
So asset delivery has exactly one core goal: before the user is likely to send a gift, get the assets they’ll probably need onto the client in advance. That’s what preloading solves. CDN and versioning are the infrastructure that makes preloading fast and correct.
2. Asset package organization and versioning
Package structure
Each gift maps to one asset package:
gift_blazing_warhorse/
├── manifest.json # metadata: version, format, dimensions, duration, md5
├── effect.svga # main effect file (or .mp4 for VAP / .pag)
├── thumbnail.webp # gift panel thumbnail
└── sound.mp3 # sound effect (optional)manifest.json is the package’s spec sheet:
{
"giftId": "gift_blazing_warhorse",
"version": 12,
"format": "svga",
"fileUrl": "https://cdn.example.com/gifts/blazing_warhorse/v12/effect.svga",
"md5": "a1b2c3d4e5f6...",
"size": 512284,
"duration": 3200,
"tier": "large",
"minAppVersion": "8.2.0"
}Versioning: version number + MD5 as double insurance
Gift effects iterate — designers swap animations, fix bugs, compress file sizes. The client must be able to detect “this gift’s asset has been updated.”
Version number: a monotonically increasing integer. The client’s local cache records the downloaded version of each gift and compares it against the server’s manifest. If the local version lags, it redownloads.
MD5 check: after download, verify the file’s MD5 against the value in the manifest. A mismatch means the download is corrupt or tampered with — discard and redownload. This step catches the “half a file downloaded” cases that cause visual corruption or parser crashes.
File downloaded = downloadFile(manifest.fileUrl);
String actualMd5 = md5(downloaded);
if (!actualMd5.equals(manifest.md5)) {
downloaded.delete();
throw new IntegrityException("MD5 mismatch, redownload");
}Incremental update vs full download
For large packages, if only the sound or thumbnail changed, redownloading everything is wasteful. Split the package into independent files, each with its own version number, and the client only downloads what changed:
{
"giftId": "gift_blazing_warhorse",
"files": [
{ "name": "effect.svga", "version": 12, "md5": "...", "size": 480000 },
{ "name": "thumbnail.webp", "version": 3, "md5": "...", "size": 8000 },
{ "name": "sound.mp3", "version": 5, "md5": "...", "size": 24000 }
]
}The client compares versions file by file and only downloads the updated one. For the “only the sound changed” case, the download drops from 512KB to 24KB.

3. CDN delivery: getting assets there fast, close, and reliably
Where assets live and how they’re delivered directly determines download time. The core is the CDN (content delivery network).
Why a CDN is mandatory
If every client downloads from the origin (business server), the origin’s bandwidth gets saturated and cross-region users see high latency. A CDN caches assets on edge nodes around the world, and users download from the nearest one:
[Origin OSS] --backfill--> [CDN edge node (nearest)] --download--> [Client]A user in Beijing downloads from the Beijing node, a user in Singapore from the Singapore node — short physical distance, low latency.
Caching strategy
Gift assets are immutable content — a file at a given version never changes. This is a natural fit for long-lived caching:
Put the version in the URL:
.../blazing_warhorse/v12/effect.svga. When the version changes, the URL changes.Set a very long
Cache-Control: max-age=31536000, immutable(one year).When updating a gift, publish a new version URL (v13) and leave the old URL untouched — sidestepping cache invalidation entirely.
This “versioned URL + permanent cache” pattern pushes CDN hit rate to 99%+, with almost no backfill to origin.
Asset warmup: push to edge nodes before a new gift launches
The instant a new gift launches, if the edge nodes haven’t cached it yet, a flood of user requests all backfill to origin at once, overwhelming it (cache stampede).
The fix is proactive warmup: after the new asset is published but before it goes live, call the CDN vendor’s prefetch API to push the asset to all edge nodes:
# Call CDN prefetch API (Alibaba Cloud example)
POST /?Action=PushObjectCache
ObjectPath=https://cdn.example.com/gifts/blazing_warhorse/v13/effect.svgaOnce warmup completes, open the gift to users — the very first request hits the edge cache.
4. Client preloading strategy: ready before the gift is sent
CDN solves “download fast,” preloading solves “download early.” The core idea: by priority, at the right moment, pull the assets likely to be used onto the device in advance.
Tiered preloading
Not every gift is worth prefetching — a gift shop might have hundreds, and downloading them all would blow out storage and bandwidth. Tier by priority:
P0 high-frequency gifts (background prefetch after app launch): little hearts, roses — high-frequency, low-cost gifts that cover 80% of send volume. Silently prefetch in the background after launch, cache permanently.
P1 room-related gifts (prefetch on entering a room): when entering a stream, fetch that streamer’s “exclusive gifts” and “popular gifts” list and prefetch that batch. Users in this room are likely to send these.
P2 long-tail gifts (on-demand on user interaction): when the user opens the gift panel or scrolls to a specific gift, trigger a prefetch. The fact that they browsed to it means there’s some chance of sending it.
// Trigger P1 preloading on entering a room
void onEnterRoom(String roomId) {
List<Gift> roomGifts = giftApi.getRoomGifts(roomId); // popular gifts in this room
for (Gift gift : roomGifts) {
if (!cache.contains(gift.giftId, gift.version)) {
preloadQueue.enqueue(gift, Priority.P1);
}
}
}Preloading timing
App launch: P0 high-frequency gifts — silently download all on Wi-Fi, only the most essential few on cellular.
Entering a room: P1 room gifts, prioritized above the P0 catch-up downloads.
Opening the gift panel: P2 visible gifts, triggered dynamically as the user scrolls (like lazy prefetch in an image list).
Storage and eviction
Local cache can’t grow forever. Manage it with LRU + a capacity cap:
// Total cache capacity cap, e.g. 200MB
if (cache.totalSize() + newFile.size > MAX_CACHE_SIZE) {
// Evict least-recently-used, but protect P0 high-frequency gifts
cache.evictLRU(newFile.size, /* protect */ P0_GIFT_IDS);
}Key point: “pin” P0 high-frequency gifts so they’re never evicted — otherwise they get pushed out right after downloading, wasting the bandwidth.

5. Weak-network optimization: play it even when the network is bad
Preloading is easy on Wi-Fi, but in the real world plenty of users are on subways, in elevators, in weak-signal environments. Weak-network optimization is the key to the “tap-to-play” experience.
Concurrency control and priority queue
Under a weak network, bandwidth is limited — downloading 10 assets at once just makes them fight for bandwidth and all end up slow. Use a priority queue + a concurrency cap:
// Max 2 concurrent on weak network, 5 on Wi-Fi
int concurrency = network.isWifi() ? 5 : 2;
downloadExecutor = new PriorityDownloadExecutor(concurrency);// High-priority tasks jump the queue
downloadExecutor.submit(task, task.priority);The gift the user is about to send (P0) gets bumped to the highest priority, jumping to the front of the queue while other preload tasks yield.
Resumable downloads
Large packages easily break off halfway under a weak network. Support resumable downloads — use the HTTP Range header to continue from the break point instead of starting over:
long downloaded = tempFile.length();
Request request = new Request.Builder()
.url(manifest.fileUrl)
.header("Range", "bytes=" + downloaded + "-") // continue from downloaded position
.build();Timeout and retry
Under a weak network, a single request times out easily. Use exponential backoff retry, but cap it to avoid endless retries dragging down the experience:
int maxRetry = 3;
long backoff = 500; // ms
for (int i = 0; i < maxRetry; i++) {
try {
return download(url);
} catch (TimeoutException e) {
Thread.sleep(backoff);
backoff *= 2; // 500ms -> 1s -> 2s
}
}
// All three failed, fall through to degradation logic6. Fallback: what to do when the asset isn’t ready
Even with preloading and weak-network optimization, there will still be cases where the asset isn’t ready — obscure gifts, first-time sends, extreme weak networks. You can’t show the user a blank; you need graceful degradation.
Degradation chain
Degrade level by level, from best experience to worst:
Full effect: asset is ready, play SVGA / VAP / PAG normally.
Static image fallback: asset isn’t ready — play a preset static image (thumbnail or first frame) first, while asynchronously downloading the full asset in the background.
Plain text / gift icon: not even a thumbnail available — show a text bar “XX sent Blazing Warhorse ×1” plus a generic gift icon.
void playGift(Gift gift) {
if (cache.isReady(gift.giftId, gift.version)) {
player.play(cache.getFile(gift)); // 1. full effect
} else {
showStaticFallback(gift.thumbnail); // 2. static image fallback
preloadQueue.enqueue(gift, Priority.HIGHEST); // background catch-up for next time
}
}Key point: degradation isn’t “don’t play,” it’s “play a different way.” The user’s gift must be seen in some form — that’s basic respect for a paying user. Meanwhile the background catch-up download guarantees the same gift plays its full effect the second time.
7. Hit rate and monitoring: data-driven optimization
Whether preloading works well can’t be a gut feeling — it has to be measured. Core metrics:
Preload hit rate = number of sends where the asset was ready / total sends. This is the single most important metric, directly reflecting how effective the preloading strategy is. A low hit rate means preloading isn’t covering the gifts users actually send, and the tiering strategy needs adjusting.
Download latency: P50 / P95 / P99 percentiles. A high P99 means weak-network users have a poor experience and weak-network optimization needs strengthening.
Download failure rate: failures / total downloads. Break it down by network type, CDN node, and asset size to pinpoint whether it’s a CDN problem or the asset is simply too large.
Degradation trigger rate: the share that fell back to static image or text. The higher this value, the weaker the preloading.
// Instrument at send time
void onSendGift(Gift gift) {
boolean hit = cache.isReady(gift.giftId, gift.version);
tracker.track("gift_preload_hit", Map.of(
"giftId", gift.giftId,
"hit", hit,
"tier", gift.tier,
"network", network.type()
));
}With the data in hand, you can optimize precisely: raise the preload priority of low-hit-rate gifts, compress or split slow-to-download assets, and investigate backfill issues on high-failure-rate CDN nodes.
Wrapping up
Six core stages of gift effect asset delivery and preloading:
Asset organization and versioning: manifest + version number + MD5 check, with incremental updates to save bandwidth.
CDN delivery: versioned URL + permanent cache + new-gift warmup, 99%+ hit rate.
Tiered preloading: P0 high-frequency in the background, P1 room gifts on room entry, P2 long-tail on demand.
Weak-network optimization: priority queue + concurrency control + resumable downloads + exponential backoff retry.
Fallback degradation: full effect → static image → text icon, the gift must be seen.
Monitoring: preload hit rate, download latency, failure rate, degradation rate — data-driven tuning.
This post fills in the last piece after the backend architecture — how assets arrive at the right client at the right time. From user tap, to backend push, to asset readiness, to animation playback, the full gift effect pipeline is now closed.
Next up: gift queuing and concurrency control — when a crowd sends gifts simultaneously, how the client queues, batches, and drops to keep the stream both lively and smooth.
Last updated:



