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.
<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.
// 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();
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.
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.
// 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();
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).
// 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;
}
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.
// 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();
}
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.
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!');
});
});
});