Performance & Image Lists
When rendering a list or grid of OIM interactive images (e.g., in a product catalog, showcase gallery, or feed), optimizing performance is critical. Instantiating dozens of <oim-player> components simultaneously can consume high memory and block the main UI thread during PNG metadata parsing.
This guide covers best practices and code patterns for showing interactive images in lists efficiently.
1. Core Strategies
1.1 Progressive Hydration (Hover/Click to Load)
Instead of rendering a full <oim-player> for every item on page load, render a static <img> representing the preview thumbnail. When the user hovers or clicks on the card, replace or upgrade the element to a fully interactive <oim-player>.
1.2 Lazy Loading (Viewport-based)
Use IntersectionObserver to detect when a card enters the viewport. Only load the OIM metadata and instantiate the custom element when it becomes visible.
1.3 Pre-extracted Metadata Lists
In high-scale catalogs, do not force the client to extract metadata from PNGs on the fly in a grid. Instead:
- Run a background server task to extract metadata once.
- Serve a JSON index containing the items and their layout metadata.
- Pass the pre-loaded metadata object directly to the player's
.setMetadata(data)method or via property bind instead of having each player fetch and read the binary chunks again.
2. Implementation Examples
Vanilla JavaScript Grid (IntersectionObserver + Hover Upgrade)
This example keeps list cells light by using a standard <img> wrapper, and upgrades them to <oim-player> only when they enter the viewport and the user hovers over them.
<div class="oim-grid">
<!-- Card template -->
<div class="oim-card" data-src="/images/product1.oim.png" data-lang="en">
<div class="player-container">
<!-- Static fallback thumbnail -->
<img src="/images/product1-thumb.png" alt="Product 1" class="thumbnail" />
</div>
<h3>Product Title</h3>
</div>
</div>const cards = document.querySelectorAll('.oim-card');
const observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const card = entry.target;
setupInteractiveTriggers(card);
observer.unobserve(card); // Only setup triggers once
}
});
}, { rootMargin: '100px' });
cards.forEach(card => observer.observe(card));
function setupInteractiveTriggers(card) {
const container = card.querySelector('.player-container');
const src = card.dataset.src;
const lang = card.dataset.lang;
// Upgrade to player on mouse hover (or click)
card.addEventListener('mouseenter', () => {
if (container.querySelector('oim-player')) return; // Already upgraded
const player = document.createElement('oim-player');
player.setAttribute('src', src);
player.setAttribute('lang', lang);
player.className = 'interactive-player';
// Swap thumbnail with active player
const thumb = container.querySelector('.thumbnail');
if (thumb) thumb.style.display = 'none';
container.appendChild(player);
}, { once: true }); // Trigger once
}React List Integration with Lazy Loading
Here is how you can implement a high-performance interactive image card in React:
import React, { useState, useRef, useEffect } from 'react';
interface OimCardProps {
id: string;
thumbnailUrl: string;
oimUrl: string;
title: string;
}
export function OimCard({ id, thumbnailUrl, oimUrl, title }: OimCardProps) {
const [isNearScreen, setIsNearScreen] = useState(false);
const [isInteractive, setIsInteractive] = useState(false);
const cardRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsNearScreen(true);
observer.disconnect();
}
},
{ rootMargin: '100px' }
);
if (cardRef.current) {
observer.observe(cardRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div
ref={cardRef}
className="oim-grid-card border border-neutral-800 rounded-xl overflow-hidden bg-neutral-900"
onMouseEnter={() => isNearScreen && setIsInteractive(true)}
onTouchStart={() => isNearScreen && setIsInteractive(true)}
>
<div className="relative aspect-video flex items-center justify-center bg-black">
{!isInteractive ? (
<img
src={thumbnailUrl}
alt={title}
className="w-full h-full object-cover transition-opacity duration-300"
/>
) : (
<oim-player
src={oimUrl}
lang="en"
class="w-full h-full"
id={`player-${id}`}
/>
)}
</div>
<div className="p-4">
<h4 className="text-sm font-semibold text-white">{title}</h4>
</div>
</div>
);
}3. Performance Checklist
| Metric | Target | Solution |
|---|---|---|
| Initial Bundle Size | <15KB | Dynamically import the player module only when required. |
| Max Concurrent Players | ≤ 3 active | Only hydrate the focused element; destroy or disable players that scroll out of view. |
| Parsing Overhead | 0ms on UI thread | Load JSON metadata ahead of time if rendering many elements. |
| Thumbnails | Optimized WebP/AVIF | Always show highly compressed static previews for the baked image (layer −1) before loading the player. |