Architecture & Implementation Guide

High-Performance Video-Like Image Sequence Playback

Based on the technique by zhangxinxu · Original Article Reference

Viral H5 pages often rely on full-screen video, but suffer from iOS autoplay restrictions, impossible DOM interaction layering, and rigid playback speeds. By preloading DOM elements into memory and swapping them dynamically, we achieve buttery-smooth 60fps-capable playback with dynamic text overlays, customizable frame rates, and zero decode flicker.

01 / PROBLEM STATEMENT

The Limitations of Video & Naive <img src="..."> Swapping

Videos in H5 cannot autoplay without user interaction on iOS and forbid inline HTML/DOM structures. However, the common alternative of swapping the src attribute on a single <img> is flawed: each src mutation forces the browser's rendering engine to re-decode and repaint, causing dropped frames and stutter on mobile devices.

Code: naive-player.js (Anti-pattern)
// NAIVE APPROACH: Repeatedly mutating .src on a single <img>
var singleImg = document.getElementById('naive-img');
var frameIndex = 1;

function playNaiveSequence() {
    // Modifying src triggers asynchronous image decode & layout invalidation
    singleImg.src = imageUrls[frameIndex];
    
    frameIndex++;
    if (frameIndex <= 36) {
        setTimeout(playNaiveSequence, 42); // ~24 fps
    }
}
playNaiveSequence();
Result: Interactive Comparison Showcase
Naive Frame
DOM Node Count: 1 static <img>
Current Frame: 1 / 36
Decode Repaint Status: Idle
Mobile Risk: Decode Latency & Frame Skips
Mutating src continuously fails to take advantage of GPU-cached decoded bitmap memory. On low-to-mid range devices, white flashing or playback lag is frequently observed.
02 / CORE HIGH-PERFORMANCE METHOD

In-Memory DOM Object Preloading & Element Swapping

Zhangxinxu's recommended solution is simple yet optimal: instantiate all image DOM elements in memory (within a store object). When playback triggers, append the current frame's preloaded DOM node while removing the previous frame's node. Exactly one image DOM element exists in the document at any moment.

Code: high-performance-playback.js
// 1. Preload image DOM objects into an in-memory store
var store = {
    length: 36,
    1: img1, 2: img2, /* ... */ 36: img36
};

// 2. High-performance O(1) DOM swapping loop
var index = 1;
container.innerHTML = '';

var step = function () {
    // Detach previous frame DOM node to maintain low memory
    if (store[index - 1]) {
        container.removeChild(store[index - 1]);
    }
    // Mount current frame pre-cached DOM element
    container.appendChild(store[index]);

    index++;
    if (index <= store.length) {
        // 42ms corresponds to standard ~24 fps
        setTimeout(step, 42);
    }
};
step();
Result: Live DOM Swapping Flying Kiss Player
Scrub Timeline: Frame 1
Container Children Count: 1 Node (Zero Leak)
Active Node in DOM: <img store[1]>
Frame Render Interval: 42ms (~24 fps)
Visual Fluidity: Hardware-Accelerated
03 / DOM INTERACTIVITY

Seamless DOM Overlays & Climax Touch Hotspots

Because frames are standard DOM objects rather than a closed video decoding pipeline, we can wrap frames in dynamic <div> elements. This lets us inject personalized user information (such as custom names) and attach interactive tap hotspots at key moments (e.g., catching the flying kiss).

Code: interactive-wrapper.js
// Wrap frame in a DOM container featuring dynamic user data & interactive hotspots
function createInteractiveFrame(frameImg, frameIndex, userName) {
    var wrapper = document.createElement('div');
    wrapper.className = 'frame-wrapper';
    wrapper.appendChild(frameImg);

    // 1. Dynamic personalized text overlay
    var badge = document.createElement('div');
    badge.className = 'user-badge-overlay';
    badge.innerHTML = '💖 To: <strong>' + userName + '</strong>';
    wrapper.appendChild(badge);

    // 2. Trigger interactive touch button on climax frame (Frame 20)
    if (frameIndex === 20) {
        var hotspot = document.createElement('button');
        hotspot.className = 'hotspot-interactive-btn';
        hotspot.innerText = '✨ Catch the Kiss!';
        hotspot.onclick = function() { onKissCaptured(); };
        wrapper.appendChild(hotspot);
    }
    return wrapper;
}
Result: Live Personalized Interactive Scene
Personalized Target: Xiao Zhang
Interaction Climax: Frame 20 (Touch Event Enabled)
Interactive Event Status: Waiting for playback...
Try playing the scene! On Frame 20, a glowing touch button will spawn directly on the flying kiss. Tap it to trigger a custom in-scene reward event.
04 / PERFORMANCE & FRAME RATE

18 FPS vs. 24 FPS: Balancing Visual Persistence & Bandwidth

Human eyes cannot distinguish 18 fps from 24 fps during fast-paced H5 marketing animations. Lowering playback rate from 24 fps (42ms delay) to 18 fps (55ms delay) reduces total asset requests and download bandwidth by ~25%, yielding immense performance gains for mobile networks.

Code: frame-rate-controller.js
// 18 FPS (~55ms) preserves visual fluidity while slashing 25% of network payloads
var FPS_CONFIG = {
    fps18: { fps: 18, delay: 55, desc: 'H5 Recommended (Save 25% Data)' },
    fps24: { fps: 24, delay: 42, desc: 'Standard Film Rate' },
    fps12: { fps: 12, delay: 83, desc: 'Stylized Low Rate' }
};

function playAtRate(targetFps) {
    var interval = Math.round(1000 / targetFps);
    clearTimeout(timer);
    var next = function() {
        swapDomNode();
        timer = setTimeout(next, interval);
    };
    next();
}
Result: Live Frame Rate Switcher & Data Savings Calculator
Select Playback Frame Rate:
5-Second H5 Campaign Bandwidth Comparison (at 50KB/frame)
Standard 24 FPS (120 Frames) 6.0 MB
Optimized 18 FPS (90 Frames) 4.5 MB (-25%)
✓ Saved 1.5 MB data transfer and 30 fewer DOM swaps per user session!
05 / WORKFLOW & ASSET PIPELINE

Video to Sequence: Photoshop Export & Node.js Batch Renaming

To prepare image assets without an After Effects designer, devs can import video frames to Photoshop layers, run Export Layers to Files..., and use a lightweight Node.js script to batch rename exported files into clean sequential numeric keys (1.jpg, 2.jpg, ...) for store[index] lookup.

Code: rename.js (Node.js Script from Article)
var fs = require("fs");

fs.readdir('./', function(err, files) {
    files.forEach(function(filename) {
        if (/jpg$/.test(filename) == false) { return; }
        // Determine old and new file paths
        var oldPath = './' + filename, newPath = './' + filename.split(' ')[1];
        // Execute rename
        fs.rename(oldPath, newPath, function() {
            console.log(filename + ' renamed successfully!');
        });
    });
});
Result: Interactive Node.js Terminal Simulator
Photoshop Export Directory (Before/After):
Virtual Node.js Terminal Output:
user@h5-build:~$ ls -l
Found 8 exported JPEG sequence frames from Photoshop.
Ready to batch-rename.