Performance Guide
Optimize Movi-Player for best performance.
Bundle Size Optimization
Use Only What You Need
// ❌ Imports everything (~410KB)
import "movi-player";
// ✅ Import only demuxer (~45KB)
import { Demuxer } from "movi-player/demuxer";
// ✅ Import only player (~180KB)
import { MoviPlayer } from "movi-player/player";Dynamic Import
// Load player only when needed
const playButton = document.getElementById("play");
playButton.onclick = async () => {
const { MoviPlayer } = await import("movi-player/player");
const player = new MoviPlayer({
source: { type: "url", url: "video.mp4" },
canvas: document.getElementById("canvas"),
});
await player.load();
await player.play();
};Memory Optimization
Cache Configuration
// Reduce cache for memory-constrained environments
const player = new MoviPlayer({
source: { type: "url", url },
canvas,
cache: {
maxSizeMB: 50, // Default is 100MB
},
});Cleanup
// Always destroy players when done
player.destroy();
// React example
useEffect(() => {
const player = new MoviPlayer({ ... });
return () => {
player.destroy();
};
}, []);
// Vue example
onUnmounted(() => {
player.destroy();
});Single Player Instance
// Avoid multiple concurrent players
let currentPlayer: MoviPlayer | null = null;
function loadVideo(url: string) {
// Destroy previous player
if (currentPlayer) {
currentPlayer.destroy();
}
currentPlayer = new MoviPlayer({
source: { type: "url", url },
canvas,
});
}Decoding Optimization
Hardware-First (default)
// "auto" tries hardware (WebCodecs) first and falls back to software on failure.
const player = new MoviPlayer({
source: { type: "url", url },
canvas,
decoder: "auto", // default
});Force Software Decoding
// Use when hardware decode is producing visual artifacts on a particular file.
const player = new MoviPlayer({
source: { type: "url", url },
canvas,
decoder: "software",
});Decoder Selection Logic
"auto" (default)
└── Try WebCodecs (GPU-accelerated, low CPU)
└── On configure/decode failure → fall back to FFmpeg WASM (software)
"software"
└── Always FFmpeg WASM — universal format support, higher CPUThere is no "hardware" value
DecoderType is "auto" | "software". There's no explicit "hardware" flag — "auto" already prefers hardware, and falls back automatically when hardware can't handle a stream.
Rendering
// "canvas" is the only renderer today.
const player = new MoviPlayer({
source: { type: "url", url },
canvas,
renderer: "canvas",
});DRM/HLS playback paths internally hand off to a native <video> element + EME, but that is selected automatically when drm: true is configured — there is no separate "mse" renderer setting.
Network Optimization
Preload Metadata
<!-- Preload only metadata -->
<movi-player src="video.mp4" preload="metadata"></movi-player>Lazy Loading
// Don't load until needed
const player = new MoviPlayer({
source: { type: "url", url },
canvas,
});
// Load only when user clicks play
playButton.onclick = async () => {
await player.load();
await player.play();
};Adaptive Buffer Sizing
For programmatic-API multi-quality, drive HLS streams (the only supported quality-switching path) and let hls.js handle ABR. For non-HLS sources, scale the prefetch window based on connection quality:
function tuneBufferForConnection(player: MoviPlayer) {
const connection = (navigator as any).connection;
if (!connection) return;
const downlink = connection.downlink; // Mbps
if (downlink < 3) {
player.setMaxBufferSize(60); // 60 MB on slow links
} else if (downlink < 10) {
player.setMaxBufferSize(150);
} else {
player.setMaxBufferSize(400); // Generous on fast connections
}
}Thumbnail Generation
Efficient Thumbnail Generation
getPreviewFrame() runs on an isolated WASM instance, so it doesn't disturb playback. Generate frames sequentially — parallel calls would just queue on the same WASM worker.
async function generateThumbnails(
player: MoviPlayer,
count: number = 10,
): Promise<Blob[]> {
const duration = player.getDuration();
const interval = duration / count;
const thumbnails: Blob[] = [];
for (let i = 0; i < count; i++) {
const time = i * interval;
const blob = await player.getPreviewFrame(time);
if (blob) thumbnails.push(blob);
}
return thumbnails;
}Thumbnail Caching
const thumbnailCache = new Map<number, string>();
async function getThumbnail(player: MoviPlayer, time: number): Promise<string | null> {
const key = Math.floor(time / 10) * 10; // 10s buckets
if (thumbnailCache.has(key)) {
return thumbnailCache.get(key)!;
}
const blob = await player.getPreviewFrame(key);
if (!blob) return null;
const url = URL.createObjectURL(blob);
thumbnailCache.set(key, url);
return url;
}Event Throttling
Throttle Time Updates
let lastUpdate = 0;
player.on("timeUpdate", (currentTime: number) => {
const now = Date.now();
// Limit to 4 updates per second
if (now - lastUpdate < 250) return;
lastUpdate = now;
updateUI(currentTime);
});Debounce Seek
let seekTimeout: number;
progressBar.oninput = () => {
clearTimeout(seekTimeout);
seekTimeout = window.setTimeout(() => {
const time = parseFloat(progressBar.value);
player.seek(time);
}, 100);
};Monitoring Performance
FPS Counter
let frameCount = 0;
let lastTime = performance.now();
player.on("frame", () => {
frameCount++;
const now = performance.now();
if (now - lastTime >= 1000) {
console.log("FPS:", frameCount);
frameCount = 0;
lastTime = now;
}
});Memory Monitoring
function logMemoryUsage() {
if ((performance as any).memory) {
const memory = (performance as any).memory;
console.log("Memory:", {
usedJSHeapSize: `${(memory.usedJSHeapSize / 1024 / 1024).toFixed(2)} MB`,
totalJSHeapSize: `${(memory.totalJSHeapSize / 1024 / 1024).toFixed(2)} MB`,
});
}
}
setInterval(logMemoryUsage, 5000);Best Practices
- Destroy players when done
- Use appropriate module (demuxer vs player vs element)
- Match decoder to content (hardware for common codecs)
- Throttle frequent events
- Cache thumbnails
- Select appropriate quality
- Use lazy loading
- Monitor memory usage