Live Gift Animation Assets, in Practice: Cut File Size in Half Without Visible Difference

Learn six practical ways to reduce live gift animation file size through image formats, video bitrate, resolution, particle density, duration splitting, and export automation.

Last time we covered six runtime optimizations for low-end phones. A question came up in the comments: if the assets themselves are heavy, isn’t runtime optimization just reactive damage control?

Yes. That’s exactly what this piece is about.

A full-screen gift effect ballooning from a few hundred KB to several MB almost never happens because the effect genuinely needs that much data. It happens because nobody made careful cuts at export time — designers exported 1080p PNGs out of habit, particle layers kept multiplying, the whole animation got packed into one file. Each individual decision seems fine; combined, they produce an asset that blows out memory on low-end phones.

This piece works through six reduction directions — image format, video bitrate, resolution strategy, particle density, duration splitting, and automation — with one target: the same gift, visually indistinguishable, at half the file size and decode cost. Everything here comes from live delivery work at XingJi Shop.

1. First, understand what you’re working with

Different formats have different bulk sources, so the reduction approach differs:

FormatWhere the bulk comes fromReduction directionSVGAVector paths + per-frame bitmapsCompress embedded bitmaps, reduce frame countVAPH.264/H.265 video stream + alphaLower bitrate, lower resolution, trim durationPAGVector data + optional bitmap layersControl bitmap layer count, compress bitmaps

Before doing anything, unpack the asset and see what’s inside. SVGA is a zip — just rename and extract. VAP is an MP4 container — use ffprobe. PAG has an official PAGViewer that shows each layer type.

# Check bitrate and resolution of a VAP file
ffprobe -v error -select_streams v:0 \
  -show_entries stream=width,height,bit_rate,codec_name \
  -of default=noprint_wrappers=1 gift.mp4

# Unpack SVGA to inspect embedded bitmaps
cp gift.svga gift.zip && unzip gift.zip -d gift_contents/
ls -lh gift_contents/images/

2. PNG → WebP: smallest change, biggest immediate gain (40–60% smaller)

Knowing where the bulk lives tells you which lever to pull first.

20260904145836_e8149e.png

Bitmaps embedded in SVGA and PAG files are almost always PNG. PNG is lossless but heavy. Switching to WebP at quality 85 produces files that are 40–60% smaller with no perceptible visual difference — the highest-priority reduction with the lowest implementation cost.

import os
from PIL import Image
def png_to_webp(src_dir, quality=85):
    """
    Convert all PNGs in a directory to WebP.
    quality=85 is a good default: imperceptible difference, ~40% of original size.
    For high-transparency particle/glow layers, 80 is fine.
    For main character layers with fine detail, go up to 90.
    """
    for fname in os.listdir(src_dir):
        if not fname.lower().endswith('.png'):
            continue
        src_path = os.path.join(src_dir, fname)
        dst_path = src_path.replace('.png', '.webp')
        img = Image.open(src_path).convert('RGBA')
        img.save(dst_path, 'WEBP', quality=quality, method=6)
        src_kb = os.path.getsize(src_path) // 1024
        dst_kb = os.path.getsize(dst_path) // 1024
        print(f"{fname}: {src_kb}KB → {dst_kb}KB ({dst_kb/src_kb*100:.0f}%)")png_to_webp('./gift_contents/images/')

One thing to verify: confirm your SVGA player library supports WebP-embedded images. Most current versions do; older ones need an update before you switch.

3. H.264 → H.265 for video-type formats: 30–50% bitrate reduction

For VAP, the bulk is the video bitrate. H.265 (HEVC) delivers the same visual quality at 30–50% lower bitrate than H.264. Modern mobile chipsets support H.265 hardware decode almost universally, so the migration cost is low.

# Re-encode a H.264 VAP as H.265 with ffmpeg
# -crf 28 is a good starting point for gift effects (range: 26–30)
# -preset slow trades encoding time for better compression (offline, so that's fine)
ffmpeg -i input_vap.mp4 \
  -c:v libx265 -crf 28 -preset slow \
  -tag:v hvc1 \
  -movflags +faststart \
  output_vap_h265.mp4

One more gain available here: frame rate. A one-shot entrance animation doesn’t need 60fps — 30fps is more than enough, and it cuts the frame count (and bitrate) in half. Only looping animations that need to feel perfectly smooth are worth keeping at 60fps.

4. Resolution by device tier: don’t feed 1080p assets to a 720p screen

20260904145837_332675.png

Design files are usually exported at 1080p or higher. But a full-screen gift on a 720p phone displays at 720p — sending a 1080p asset decodes twice the data, uses twice the memory, then scales down for display. Pure waste.

Output three resolution tiers and pair them with the runtime detectTier() call from the previous article:

import os
import subprocess
def export_by_tier(src_mp4, output_dir):
    """
    HIGH → 1080p (flagship phones)
    MID  → 720p  (mid-range)
    LOW  → 540p  (low-end phones)
    """
    os.makedirs(output_dir, exist_ok=True)
    tiers = {
        'high': ('1920x1080', '26'),
        'mid':  ('1280x720',  '28'),
        'low':  ('960x540',   '30'),
    }
    for tier, (scale, crf) in tiers.items():
        out = os.path.join(output_dir, f"gift_{tier}.mp4")
        subprocess.run([
            'ffmpeg', '-i', src_mp4,
            '-vf', f'scale={scale}',
            '-c:v', 'libx265', '-crf', crf, '-preset', 'slow',
            '-tag:v', 'hvc1', '-movflags', '+faststart',
            '-y', out
        ], check=True)
        size_kb = os.path.getsize(out) // 1024
        print(f"{tier}: {out} ({size_kb} KB)")export_by_tier('gift_original.mp4', './output/')

The combined size of three tiers is usually smaller than one 1080p file alone, because the mid and low tiers — which serve the majority of users — are so much lighter.

5. Cut particle density: halve the count, keep the feel

Particles are where “a little more” thinking accumulates silently — a layer of stars, then a light scatter on top, then a trailing glow. Each looks good in isolation. Together they spike both file size and GPU load.

Three practical tests:

Occlusion test. Turn off one particle layer completely. Can you tell? If not, delete it. Density-halving test. Reduce particle count by 50% (in After Effects, just halve the Particles/sec value). Does the luminous feel survive? In most cases 60% of the original particle count is visually indistinguishable from 100%. Size over count. A few large particles hit harder visually than many tiny ones, and generate fewer GPU draw calls.

This step needs no code — adjust in After Effects while previewing, then verify file size after export.

6. Split entrance from loop: the loop only needs 20–40% of the combined file size

Most gift effects bundle the entrance animation and the looping animation into one file, totaling 6–8 seconds. But the entrance plays once; the loop is what the user actually watches.

When they’re packed together: — Total file size = entrance size + loop size; both get downloaded and cached even though the entrance is used once. — A 2-second loop buried inside a 6-second file reloads the full file on every iteration.

The fix: two files.

gift_enter.svga   ← entrance animation, plays once
gift_loop.svga    ← looping animation, seamless repeat

A loop section is typically 1.5–3 seconds — 20–40% of the combined file. The one requirement: the loop file’s first and last frames must be identical (or close enough) to stitch seamlessly onto the end of the entrance.

7. Automate it: make reduction a process, not a one-off

20260904145837_13fa56.png

Running these steps manually every time means eventually forgetting. Wire them into a script that runs automatically whenever a designer submits new assets:

import os
import shutil
import subprocess
from PIL import Image
def optimize_gift_package(src_dir, out_dir, tier='mid'):
    """One-command gift asset optimizer. Outputs a before/after size report."""
    os.makedirs(out_dir, exist_ok=True)
    scale_map = {'high': '1920x1080', 'mid': '1280x720', 'low': '960x540'}
    crf_map   = {'high': '26',        'mid': '28',       'low': '30'}
    original_total = 0
    optimized_total = 0    for fname in os.listdir(src_dir):
        src = os.path.join(src_dir, fname)
        original_total += os.path.getsize(src)        if fname.lower().endswith('.png'):
            dst = os.path.join(out_dir, fname.replace('.png', '.webp'))
            # Lower quality for background/particle layers; higher for main character layers
            q = 80 if any(kw in fname.lower() for kw in ('bg', 'particle', 'light')) else 88
            Image.open(src).convert('RGBA').save(dst, 'WEBP', quality=q, method=6)        elif fname.lower().endswith('.mp4'):
            dst = os.path.join(out_dir, fname)
            subprocess.run([
                'ffmpeg', '-i', src,
                '-vf', f"scale={scale_map[tier]}",
                '-c:v', 'libx265', '-crf', crf_map[tier], '-preset', 'slow',
                '-tag:v', 'hvc1', '-movflags', '+faststart', '-y', dst
            ], check=True, capture_output=True)        else:
            dst = os.path.join(out_dir, fname)
            shutil.copy2(src, dst)        optimized_total += os.path.getsize(dst)    ratio = optimized_total / original_total * 100
    saved = (original_total - optimized_total) // 1024
    print(f"\nResult ({tier} tier): "
          f"{original_total//1024} KB → {optimized_total//1024} KB "
          f"({ratio:.0f}% — saved {saved} KB)")optimize_gift_package('./raw_gift/', './optimized_gift/', tier='mid')

Hook this into your delivery pipeline or CI so every new gift gets automatically reduced before it ships.

Wrapping up

Six levers, ordered by impact:

  1. PNG → WebP — smallest change, 40–60% size reduction; do this first.

  2. Resolution by device tier — pair with runtime tier detection; ship three variants once.

  3. H.264 → H.265–30–50% bitrate reduction for VAP and other video-based formats.

  4. Particle density halved — visually indistinguishable, GPU load drops noticeably.

  5. Entrance/loop split — the loop file is 20–40% of the combined size.

  6. Automate the pipeline — make reduction a process that happens every time, not a task you remember occasionally.

Runtime optimization fixes how well effects run on device. Asset reduction fixes how heavy they are before they ever arrive. Together, they close the full loop on low-end phone performance.

At XingJi Shop, both of these standards — the runtime checklist and the asset-reduction pipeline — are baked into how we deliver effects to clients. An effect that ships light runs light.

Next up: iOS integration in practice — Metal rendering, where it diverges from Android’s OpenGL ES path, and the common crash patterns to watch for.

Last updated: