技術 約4分で読めます

Astroでローカル動画を遅延読み込み:preload="metadata"とrehypeプラグイン

前回のYouTube遅延読み込みの続き。今回はローカル動画(WebM/MP4)を軽量化する。

問題:動画ファイルが重い

記事に動画を埋め込むとき、普通に <video> タグを書くとこうなる:

<video src="/images/demo.webm" controls></video>

この書き方だと、ページを開いた時点で動画全体のダウンロードが始まる。見るかどうかわからないのに。

解決策:preload=“metadata” + クリックで再生

2つの仕組みを組み合わせる:

  1. preload="metadata" でメタデータと最初のフレームだけ読み込む
  2. 再生ボタンを表示し、クリックで再生開始

YouTubeの場合はサムネイル画像を i.ytimg.com から取得できたが、ローカル動画にはそういうサービスがない。代わりに preload="metadata" を使う。

<!-- 全体を読み込む(重い) -->
<video src="video.webm"></video>

<!-- メタデータだけ読み込む(軽い) -->
<video src="video.webm" preload="metadata"></video>

preload="metadata" を指定すると、ブラウザは動画の長さ・解像度などのメタデータと、最初の数フレームだけを取得する。これが自動的にサムネイルとして表示される。

実装

1. rehypeプラグインを作成

import type { Root, Element } from 'hast';
import { visit } from 'unist-util-visit';

export function rehypeVideoEmbed() {
  return (tree: Root) => {
    visit(tree, 'element', (node: Element, index, parent) => {
      if (node.tagName !== 'video') return;
      if (!parent || typeof index !== 'number') return;

      const src = node.properties?.src as string | undefined;
      if (!src) return;

      // 元の属性を保持
      const originalProps = { ...node.properties };

      // controlsを除去し、preload="metadata"を設定
      delete originalProps.controls;
      originalProps.preload = 'metadata';

      // ラッパーdivを作成
      const wrapper: Element = {
        type: 'element',
        tagName: 'div',
        properties: {
          className: ['video-embed'],
        },
        children: [
          {
            type: 'element',
            tagName: 'video',
            properties: originalProps,
            children: [],
          },
          {
            type: 'element',
            tagName: 'button',
            properties: {
              className: ['video-play-button'],
              'aria-label': '動画を再生',
            },
            children: [],
          },
        ],
      };

      // 元のnodeをラッパーで置き換え
      (parent as Element).children[index] = wrapper;
    });
  };
}

YouTube版との違い:

  • iframeではなくvideoタグを検出
  • サムネイル画像を追加せず、videoタグ自体を残す(preload="metadata"でサムネイル表示)
  • controls属性を除去(クリック後に付与)

2. Astro設定にプラグインを追加

import { rehypeVideoEmbed } from './src/lib/rehype-video-embed';

export default defineConfig({
  markdown: {
    rehypePlugins: [
      rehypeRaw,
      rehypeYouTubeEmbed,
      rehypeVideoEmbed,  // 追加
    ],
  },
});

3. CSSでスタイリング

.video-embed {
  position: relative;
  width: 100%;
  max-width: 560px;
  margin: 1.5rem 0;
  border-radius: 0.5rem;
  overflow: hidden;
  cursor: pointer;
  background: var(--secondary);
}

.video-embed video {
  width: 100%;
  height: auto;
  display: block;
  transition: opacity 0.2s;
}

.video-embed:hover video {
  opacity: 0.8;
}

.video-embed.playing {
  cursor: default;
}

.video-embed.playing:hover video {
  opacity: 1;
}

.video-play-button {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 68px;
  height: 48px;
  background: rgba(0, 0, 0, 0.7);
  border: none;
  border-radius: 12px;
  cursor: pointer;
  transition: background 0.2s, transform 0.2s;
  display: flex;
  align-items: center;
  justify-content: center;
}

.video-play-button::before {
  content: '';
  border-style: solid;
  border-width: 10px 0 10px 18px;
  border-color: transparent transparent transparent white;
  margin-left: 4px;
}

.video-embed:hover .video-play-button {
  background: rgba(0, 0, 0, 0.9);
  transform: translate(-50%, -50%) scale(1.1);
}

.video-embed.playing .video-play-button {
  display: none;
}

YouTube版と似ているが、再生ボタンの色を黒系に変更(YouTubeの赤ではないので)。

4. クリック時のJavaScript

function initVideoEmbeds() {
  const embeds = document.querySelectorAll('.video-embed');

  embeds.forEach((embed) => {
    if (embed.dataset.initialized) return;
    embed.dataset.initialized = 'true';

    const video = embed.querySelector('video');
    if (!video) return;

    embed.addEventListener('click', () => {
      if (embed.classList.contains('playing')) return;

      embed.classList.add('playing');
      video.controls = true;
      video.play();
    });

    video.addEventListener('ended', () => {
      embed.classList.remove('playing');
      video.controls = false;
    });
  });
}

initVideoEmbeds();
document.addEventListener('astro:page-load', initVideoEmbeds);

YouTube版との違い:

  • iframeを動的生成するのではなく、既存のvideoタグを操作
  • controls属性を動的に付与
  • 再生終了時に元の状態に戻す

使い方

Markdownにvideoタグを書くだけ:

<video src="/images/demo.webm" muted loop playsinline></video>

controlsは書かなくてOK(クリック後に自動付与される)。mutedloopplaysinlineなどの属性はそのまま保持される。

結果

  • 初期読み込みが軽量化:メタデータと最初のフレームだけ取得
  • サムネイル画像不要preload="metadata"でブラウザが自動表示
  • 既存の書き方を変更不要:videoタグをそのまま書ける
  • 再生終了後にリセット:再度クリックで再生可能

YouTubeとローカル動画、両方に遅延読み込みを適用できるようになった。