DEV Community

ahmet gedik
ahmet gedik

Posted on

Building a Lazy-Loading Video Grid for Mobile Performance

A page with 30 YouTube iframe embeds loads 26MB of JavaScript and makes 467 network requests. Here's how I reduced that to 1.8MB and 42 requests on TopVideoHub.

The Problem

Each YouTube iframe embed loads:

  • ~800KB of JavaScript
  • 15+ network requests
  • The full YouTube player interface

Multiply by 30 videos on a category page and you get a page that's unusable on mobile connections — exactly where most of our Asia-Pacific users browse.

The Facade Pattern

Replace each iframe with a static thumbnail and CSS play button. The real iframe loads only when clicked.

HTML Structure

<div class="video-grid">
    <article class="video-card" data-video-id="dQw4w9WgXcQ">
        <div class="video-facade">
            <img src="https://i.ytimg.com/vi/dQw4w9WgXcQ/mqdefault.jpg"
                 alt="Video title here"
                 loading="lazy"
                 width="320" height="180"
                 decoding="async">
            <button class="play-btn" aria-label="Play video">
                <svg width="68" height="48" viewBox="0 0 68 48">
                    <path class="play-bg" d="M66.5 7.7s-.7-4.7-2.8-6.8C60.7-2.1 57.2-2.1 55.6-2.3 46.5-3 34-3 34-3s-12.5 0-21.6.7c-1.6.2-5.1.2-8.1 3.2C2.2 3 1.5 7.7 1.5 7.7S.8 13.3.8 18.8v5.2c0 5.5.7 11.1.7 11.1s.7 4.7 2.8 6.8c3 3.1 6.9 3 8.7 3.3 6.3.6 26.8.9 26.8.9s12.5 0 21.6-.8c1.6-.2 5.1-.2 8.1-3.1 2.1-2.1 2.8-6.8 2.8-6.8s.7-5.5.7-11.1v-5.2c0-5.5-.7-11.1-.7-11.1z" fill="red"/>
                    <path d="M27 33V11l18.4 11L27 33z" fill="#fff"/>
                </svg>
            </button>
        </div>
        <h3 class="video-title">Video title here</h3>
        <p class="video-channel">Channel Name</p>
    </article>
</div>
Enter fullscreen mode Exit fullscreen mode

CSS

.video-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: 1rem;
    padding: 1rem;
}

.video-facade {
    position: relative;
    aspect-ratio: 16 / 9;
    background: #0a0a0a;
    cursor: pointer;
    overflow: hidden;
    border-radius: 8px;
}

.video-facade img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    transition: transform 0.2s;
}

.video-facade:hover img {
    transform: scale(1.05);
}

.play-btn {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    background: none;
    border: none;
    cursor: pointer;
    opacity: 0.8;
    transition: opacity 0.2s;
    padding: 0;
}

.video-facade:hover .play-btn {
    opacity: 1;
}

.video-facade iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    border: none;
}
Enter fullscreen mode Exit fullscreen mode

JavaScript

document.addEventListener('DOMContentLoaded', () => {
    document.querySelectorAll('.video-facade').forEach(facade => {
        facade.addEventListener('click', function handleClick() {
            const card = this.closest('.video-card');
            const videoId = card.dataset.videoId;

            const iframe = document.createElement('iframe');
            iframe.src = `https://www.youtube.com/embed/${videoId}?autoplay=1&rel=0`;
            iframe.allow = 'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture';
            iframe.allowFullscreen = true;
            iframe.loading = 'lazy';

            // Remove thumbnail and play button, add iframe
            this.innerHTML = '';
            this.appendChild(iframe);
            this.removeEventListener('click', handleClick);
        });
    });
});
Enter fullscreen mode Exit fullscreen mode

Responsive Thumbnails

YouTube provides thumbnails at different sizes. Pick the right one:

function thumbnailUrl(string $videoId, string $size = 'mq'): string {
    $sizes = [
        'default'  => 'default',      // 120x90
        'mq'       => 'mqdefault',     // 320x180
        'hq'       => 'hqdefault',     // 480x360
        'sd'       => 'sddefault',     // 640x480
        'maxres'   => 'maxresdefault', // 1280x720
    ];
    $file = $sizes[$size] ?? $sizes['mq'];
    return "https://i.ytimg.com/vi/{$videoId}/{$file}.jpg";
}
Enter fullscreen mode Exit fullscreen mode

For grid cards, mqdefault (320x180, ~15KB) is the sweet spot — sharp enough on mobile, light enough for fast loading.

Performance Results

Measured on TopVideoHub category pages (30 videos):

Metric Before After
Page Weight 26.4 MB 1.8 MB
Requests 467 42
LCP 4.2s 1.1s
CLS 0.32 0.01
FID 380ms 12ms

The aspect-ratio: 16/9 CSS eliminates layout shift entirely. No more jumping content as thumbnails load.

This technique works on any site with multiple video embeds. The implementation is simple, the performance gains are massive, and it requires zero dependencies.

Top comments (0)