kasuのブログ

勉強していく

YouTube の iframe を埋め込んだまま PageSpeed Insights のスコアを改善する

4行で

  • YouTube から提供される動画の埋め込みコードをそのまま使うと PageSpeed Insights のスコアが落ちる。
  • しかし、lite-youtube-embed などのライブラリでダミー画像を表示しておくと再生するまでは偽物感が残ってしまう。イヤだ!
  • そこで、ページが表示された後に iframe を遅延読み込みすることにした。
  • ブラウザの iframe の遅延読み込み機能window.onload では効果がなく、IntersectionObserver を使うことで PageSpeed Insights のスコアが改善できた。

PageSpeed Insights

最近、個人サイト kasu.dev の静的サイトジェネレータを Astro(astro.build)に置き換えました。

ふと PageSpeed Insights(https://pagespeed.web.dev/)で計測してみたら、思っていたよりもスコアが低かったのでなんとかします。

診断結果をみると、4つあるYouTubeの埋め込みに関わるリソースの取得が原因のようです。

実際に埋め込みをなくして測ると100点になったので、やっぱりアイツが犯人です。

スコアの改善方法

PageSpeed Insights ではページの読み込み時に行われる iframe 関連のリソース取得がスコアに大きく影響していました。 そこで、ページの読み込み時と iframe 関連リソースの取得タイミングをズラす方法を考えてみます。

再生するまでダミー要素を表示する

よくある改善方法として、ダミーの要素を用意しておきフォーカスやクリックされたタイミングで実際の iframe に置き換える方法があります。

YouTube の動画IDからサムネイル画像を取得してそれっぽく見せるやつですね。いくつかライブラリもありそうです。

github.com github.com

ライブラリのデモなどを見ていただくと分かるのですが、あくまでダミーなので埋め込まれた本物のYouTubeのUIと比べると違和感が残ってしまいます。 そのため今回は個人サイトで使用することを見送りました。

また、頑張って UI を精巧に真似ることができたとしても YouTube 側の更新に追従していくことが大変でしょう。

ブラウザの iframe の遅延読み込み機能

web.dev

上記の記事では iframe 固有の遅延読み込みの動作について説明があり、iframe を遅延読み込みするには loading="lazy"style="visibility: hidden;" を指定すれば良いとあります。

実際に手元でも遅延読み込みされている様子を確認できましたが、PageSpeed Insights のスコアに改善が見られないので別の方法を検討していきます。

また、記事内で紹介されていた aFarkas/lazysizes なら期待できるかもしれませんが、今回の要件には不必要な実装が多く含まれていそうだったので詳しくはみていません。

iframe を直接操作して遅延読み込みする

ブラウザの機能に頼ることは難しいことが分かりました。 そこで、今回は iframe を直接操作して遅延読み込みを行うことにします。

window.onload

window.onload にはページリソースの読み込みが完了した段階で呼ばれるコールバック関数を登録できるので、このタイミングで iframe を表示してみます。

その結果... PageSpeed Insights のスコアは改善しませんでした。setTimeout などで遅延読み込みのタイミングをより遅らせると改善するかもしれません。

実装例

window.onload 版のコンポーネント

---
export interface Props {
  id: string;
}
const { id } = Astro.props;
---

<div class="youtube-embed aspect-w-16 aspect-h-9">
  <iframe
    title="YouTube Embed"
    class="border-0"
    data-src={`https://www.youtube.com/embed/${id}`}
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
    allowfullscreen={true}></iframe>
</div>

<script>
  window.addEventListener("load", () => {
    const targets = document.querySelectorAll(".youtube-embed");
    targets.forEach((target) => {
      const embed = target;
      const iframe = embed.children[0] as HTMLIFrameElement | null;
      if (!iframe) return;

      const src = iframe.getAttribute("data-src");
      if (!src) return;

      iframe.setAttribute("src", src);
      iframe.onload = () => iframeOverlay?.remove();
    });
  });
</script>

IntersectionObserver

なんとかしたい個人サイトでは、ファーストビューではなくページのスクロール後に YouTube の埋め込み動画が登場します。 そこで今回は IntersectionObserver を用いて、ページのスクロールで iframe を表示したい要素に近づいたタイミングで遅延読み込みを行います。

実装例

実際のコンポーネント YouTubeEmbed.astro は下記のようになりました。

<!-- 利用例 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
  <YouTubeEmbed id="h4VJGNNSQnw" />
</div>
---
// YouTubeEmbed.astro
export interface Props {
  id: string;
}
const { id } = Astro.props;
---

<div class="youtube-embed aspect-w-16 aspect-h-9">
  <iframe
    title="YouTube Embed"
    class="border-0"
    data-src={`https://www.youtube.com/embed/${id}`}
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
    allowfullscreen={true}></iframe>
</div>

<script>
  const callback: IntersectionObserverCallback = (entries, observer) => {
    entries.forEach((entry) => {
      if (!entry.isIntersecting) return;

      const embed = entry.target;
      const iframe = embed.children[0] as HTMLIFrameElement | null;
      if (!iframe) return;

      const src = iframe.getAttribute("data-src");
      if (!src) return;

      iframe.setAttribute("src", src);
      observer.unobserve(embed);
    });
  };

  const observer = new IntersectionObserver(callback, {
    rootMargin: "1000px",
  });

  const targets = document.querySelectorAll(".youtube-embed");
  targets.forEach((target) => observer.observe(target));
</script>

Astro の事情を反映した実装箇所はイカ

Script bundling
pass-frontmatter-variables-to-scripts
動作例

IntersectionObserver で遅延読み込みしてみた様子がこちら。

このままだと、iframe の読み込み時に白い背景が表示されてしまい、ダークモードのサイトでは眩しいと思うのでなんとかしていきます。

読み込み中の iframe の上に Placeolder を表示しておき、iframe の読み込み完了時に呼ばれる onload を使って Placeolder を隠すのはどうでしょうか。

<div class="youtube-embed aspect-w-16 aspect-h-9">
    <iframe>...</iframe>
+   <div class="w-full h-full bg-base-300"></div>
</div>

<script>
    ...
    const iframe = embed.children[0] as HTMLIFrameElement | null;
+   const iframeOverlay = embed.children[1] as HTMLDivElement | null;
+   if (!iframe || !iframeOverlay) return;
    ...
    iframe.setAttribute("src", src);
+   iframe.onload = () => iframeOverlay.remove();
    ...
</script>

最終的なコードはこちら

---
export interface Props {
  id: string;
}
const { id } = Astro.props;
---

<div class="youtube-embed aspect-w-16 aspect-h-9">
  <iframe
    title="YouTube Embed"
    class="border-0"
    data-src={`https://www.youtube.com/embed/${id}`}
    allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
    allowfullscreen={true}></iframe>
  <div class="w-full h-full bg-base-300"></div>
</div>

<script>
  const callback: IntersectionObserverCallback = (entries, observer) => {
    entries.forEach((entry) => {
      if (!entry.isIntersecting) return;

      const embed = entry.target;

      const iframe = embed.children[0] as HTMLIFrameElement | null;
      const iframeOverlay = embed.children[1] as HTMLDivElement | null;
      if (!iframe || !iframeOverlay) return;

      const src = iframe.getAttribute("data-src");
      if (!src) return;

      iframe.setAttribute("src", src);
      iframe.onload = () => iframeOverlay.remove();

      observer.unobserve(embed);
    });
  };

  const observer = new IntersectionObserver(callback, {
    rootMargin: "1000px",
  });

  const targets = document.querySelectorAll(".youtube-embed");
  targets.forEach((target) => observer.observe(target));
</script>

実際に動作を確認してみると、Placeolder によって眩しさを防ぐことができています。

動画が目に見えて遅延して表示されることついては、読み込みのタイミングを調整したりダミーのサムネイル画像を表示しておくことで、より違和感ない改善を期待できそうです。

結果

無事遅延読み込みをすることができるようになったので、再度 PageSpeed Insights のスコアを測りました。 その結果がこちら!