Skip to content

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:

  1. Run a background server task to extract metadata once.
  2. Serve a JSON index containing the items and their layout metadata.
  3. 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.

html
<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>
javascript
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:

tsx
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 ​

MetricTargetSolution
Initial Bundle Size<15KBDynamically import the player module only when required.
Max Concurrent Players≤ 3 activeOnly hydrate the focused element; destroy or disable players that scroll out of view.
Parsing Overhead0ms on UI threadLoad JSON metadata ahead of time if rendering many elements.
ThumbnailsOptimized WebP/AVIFAlways show highly compressed static previews for the baked image (layer −1) before loading the player.