技術
約4分で読めます
Astroでローカル動画を遅延読み込み:preload="metadata"とrehypeプラグイン
前回のYouTube遅延読み込みの続き。今回はローカル動画(WebM/MP4)を軽量化する。
問題:動画ファイルが重い
記事に動画を埋め込むとき、普通に <video> タグを書くとこうなる:
<video src="/images/demo.webm" controls></video>
この書き方だと、ページを開いた時点で動画全体のダウンロードが始まる。見るかどうかわからないのに。
解決策:preload=“metadata” + クリックで再生
2つの仕組みを組み合わせる:
preload="metadata"でメタデータと最初のフレームだけ読み込む- 再生ボタンを表示し、クリックで再生開始
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(クリック後に自動付与される)。muted、loop、playsinlineなどの属性はそのまま保持される。
結果
- 初期読み込みが軽量化:メタデータと最初のフレームだけ取得
- サムネイル画像不要:
preload="metadata"でブラウザが自動表示 - 既存の書き方を変更不要:videoタグをそのまま書ける
- 再生終了後にリセット:再度クリックで再生可能
YouTubeとローカル動画、両方に遅延読み込みを適用できるようになった。