先日公開した「他プラグインの動作を妨げずにYouTube IFrame Player APIを読み込む方法」では、複数のプラグインが同時にYouTube APIを初期化しようとする環境下で、いかに「譲り合い(ポーリング、既存プレイヤーへの相乗り、ランダムジッター)」を行い競合を防ぐかについて解説しました。

この設計を組み込んだWordPressプラグイン「KU Sticky Video for YouTube」は、当サイト上では競合トラブルを起こすことなく順調に動作していました。しかし、あるユーザーから遅延読み込み(LazyLoad)系のプラグインを併用すると追従機能が動作しないという報告が寄せられました。検証してみるとその通りでした!

前回記事のポーリング方式は、ページロード時に動画プレイヤー(iframe)自体はすでに静的な状態でDOM内に存在していることを前提としています。しかしスクロールやクリックなど訪問者のアクションに応じて後から動的に動画プレイヤーを読み込んでいく遅延読み込み(LazyLoad)の環境では、ページロードの時点では所定の形式のiframeは生成されていません。ここは正直、盲点でした。

今回は、この「LazyLoadプラグインとの共存」という新たな課題を克服するための、MutationObserverを用いたYouTube APIの動的かつ非同期のバインディング設計について解説します。


なぜ動作しない?LazyLoad適用時のYouTube API

LazyLoad系プラグインは、Webページの初期表示のパフォーマンスを向上させるために、たとえば以下のようなアプローチで画像やiframeの読み込みを遅延させます。

これに対してKU Sticky Video for YouTubeをはじめ多くのYouTube系プラグインは、DOMContentLoadedなどのタイミングで document.querySelectorAll('iframe') を実行し、その時点で存在する要素に対して初期化処理やイベントリスナーの登録を行います。しかしLazyLoadが有効の場合、このタイミングではYouTube埋め込み動画のiframe要素はまだ生成されていないか、意図的に不完全な状態で生成されているかのいずれかです。

YouTube API依存プラグインはその状態のもとで処理対象のiframeを探そうとします。しかし、条件に適合するiframeは存在しません。このため初期化処理は見送られ、プラグインの機能──KU Sticky Video for YouTubeプラグインの場合でいえば、動画をスクロールに追従させる機能──は永遠に発動しなくなってしまうわけです。

前回の記事で実装したポーリングによる対策も、LazyLoad系プラグインに対してはほとんど無力だったといわざるを得ません。ポーリング開始のタイミングはページロード直後ですから、処理の発動が遅延されるといってもしょせんページ読み込み後の3秒間やそこらです。一方、読者がいつ記事をスクロールするかなんて誰にも読めないわけですから。もちろん3秒といわず、ずっとポーリングし続けるという対応方法も考えられます。しかしポーリングという手法は定期的にタイマーイベントを発生させるため、無期限に待機し続けるのは現実的ではありません。

だって読者はもしかしたら記事を開いた直後にトイレに席を外すかもしれないし、トイレから戻る途中の廊下で同僚と出くわすかもしれない。そこでしばらく雑談してから再び席について、やっと記事をスクロールし始めるかもしれません。そのときまで setInterval を何分も、ことによったら1時間以上も回し続けながら待つ…となったら、どうなるか。ブラウザはアイドリング状態に移行できず、特にモバイル端末なんかでは「奇妙にバッテリー消費の激しいページ」の爆誕ってことになっちゃいます。そういうのは避けたい。


解決策: MutationObserverを用いた動的バインディング

この問題を解消するためには、ページの初期ロード時だけでなく「後から動画が出現した瞬間」をリアルタイムで捉えて初期化プロセスを起動する仕組みがどうしても必要になります。そこで浮上するのが、モダンなブラウザであれば標準搭載されている MutationObserver(変更監視のためのAPI)です。

MutationObserverによる動画検知と二重初期化の防止

LazyLoadの複数の手法に対応するため、MutationObserverには「動的なDOMノードの追加」と「既存要素のsrc属性の書き換え」という2つの変更イベントを並行して監視させます。

function initMutationObserver() {
    const observer = new MutationObserver(function(mutations) {
        const detectedIframes = [];

        mutations.forEach(function(mutation) {
            // パターンA: 動的なDOMノードの追加を監視
            if (mutation.type === 'childList') {
                mutation.addedNodes.forEach(function(node) {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        if (node.tagName === 'IFRAME') {
                            detectedIframes.push(node);
                        } else {
                            // 入れ子の中にiframeがある場合も抽出
                            const nested = node.querySelectorAll('iframe');
                            nested.forEach(iframe => detectedIframes.push(iframe));
                        }
                    }
                });
            } 

            // パターンB: 既存要素のsrc属性の書き換えを監視
            else if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
                const node = mutation.target;
                if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'IFRAME') {
                    detectedIframes.push(node);
                }
            }
        });

        // 検出されたiframeの中からYouTubeのものだけをフィルタリングして処理
        const youtubeIframes = detectedIframes.filter(isYouTubeIframe);
        if (youtubeIframes.length > 0) {
            handleNewIframes(youtubeIframes);
        }
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true,
        attributes: true,
        attributeFilter: ['src'] // src属性の監視に絞ることでパフォーマンスへの影響を抑制
    });
}

これにより、LazyLoadプラグインがどのような機序で動画の出現を遅らせているとしても、出現する際には瞬時にそれを捕捉することが可能になります。attributeFilter: ['src']で属性の監視対象を src に限定しているのは、不要な処理の発生を極力避けるためです。

また、MutationObserver は非常に細かく発動するため、同じ iframe に対しても何度も検出イベントが発生することになります。もし二重に初期化が走ると他プラグインとの相乗りはおろか、自プラグインの処理すら覚束なくなります。この二重処理を確実に防ぐため、処理を開始した iframe にはカスタムの data- 属性を付与して状態管理(二重実行防止)を行います。

function isYouTubeIframe(iframe) {
    if (!iframe || iframe.tagName !== 'IFRAME') {
        return false;
    }

    // すでに本プラグインで処理中のものは除外
    if (iframe.getAttribute('data-ku-sticky-processed') === 'true') {
        return false;
    }
    
    const src = iframe.getAttribute('src') || '';
    return src.indexOf('youtube.com') !== -1 || 
           src.indexOf('youtube-nocookie.com') !== -1 || 
           src.indexOf('youtu.be') !== -1;
}

function handleNewIframes(iframes) {
    iframes.forEach(iframe => {
        // 処理済みマークを付与
        iframe.setAttribute('data-ku-sticky-processed', 'true');
        
        // このiframeに対する初期化・バインディング処理を実行
        initPlayingMode([iframe]);
    });
}

data-ku-sticky-processed 属性が付与されている要素は、次回の MutationObserver の検知対象から即座に排除されるため、不要なループや競合エラーを確実に回避できます。

動的に発動する競合回避プロセス

ここが「動的バインディング」設計の肝です。前回の記事で紹介したポーリングやランダムジッターは、処理を遅延させるとはいってもページロードという静的なタイミングを起点に走る処理でした。しかし今回の設計ではMutationObserverを使うことで「動画がページ内に出現するたびに、その動画を起点とした独自の競合回避(譲り合い)から安全なバインディング(紐付け)に至るプロセスが非同期にスタートする」という動的なモデルへとシフトしています。

function initPlayingMode(iframes) {
    if (iframes.length === 0) return;

    // 1. YouTube APIスクリプトが未ロードなら挿入
    if (!window.YT && !document.querySelector('script[src*="youtube.com/iframe_api"]')) {
        const tag = document.createElement('script');
        tag.src = 'https://www.youtube.com/iframe_api';
        const firstScriptTag = document.getElementsByTagName('script')[0];
        firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
    }

    // 2. この動画に対して、他プラグインの初期化完了を待つポーリングを開始
    let attempts = 0;
    let apiReadyAttempts = 0;
    let setupCompleted = false;
    const checkInterval = setInterval(() => {
        attempts++;
        const isApiReady = window.YT && typeof window.YT.Player === 'function';
        if (isApiReady) {
            apiReadyAttempts++;
        }

        // 該当iframeに対して、他プラグインがすでにプレイヤーインスタンスを作ったか
        let allFound = false;
        if (isApiReady && typeof window.YT.get === 'function') {
            allFound = true;
            for (let j = 0; j < iframes.length; j++) {
                const id = iframes[j].id;
                const player = (id ? window.YT.get(id) : null) || window.YT.get(iframes[j]);
                if (!player) {
                    allFound = false;
                    break;
                }
            }
        }

        // 終了条件(相乗り対象が見つかる、または待機時間が超過)
        const shouldStop = allFound || apiReadyAttempts >= 3 || attempts >= 30;
        if (shouldStop) {
            clearInterval(checkInterval);
            if (!setupCompleted) {
                setupCompleted = true;

                // 3. ランダムジッター(0〜100msの遅延)を適用
                const jitterDelay = Math.random() * 100;
                setTimeout(() => {
                    setupPlayers(iframes);
                }, jitterDelay);
            }
        }
    }, 100);
}

この非同期設計により、以下に説明するようなシナリオが自律的に実行されます。登場人物は以下の3者です。

  1. KU Sticky Video for YouTubeプラグイン: 本記事の主役。「本プラグイン」と表記します。
  2. LazyLoad系プラグイン: YouTube動画をページスクロールなどに伴って遅延読み込みさせることでページを軽くするプラグイン。Lazy Load for Videosなど。
  3. その他のYouTube API依存プラグイン: YouTube APIに依存して、ページ内のYouTube動画にさまざまな効果を付与しようとする第3、第4のプラグイン。「その他プラグイン」と表記します。

本プラグインを2番のLazyLoad系プラグインと共存させることが本記事の直接の目的です。
3番のその他プラグインとの共存は前回の記事で達成されていましたが、本記事では「LazyLoad系プラグインとの共存を達成してなお、その他プラグインとの共存関係が壊れないこと」が目標となります。

実際の動作は以下の通りです。

  1. 訪問者がページをスクロールするなどした際、LazyLoad系プラグインによってYouTube動画(iframe)がロードされる。
  2. 本プラグインMutationObserver がその出現を検知し、APIの存在チェックとポーリングを開始する。
  3. 同時に、同じiframeに対してその他プラグインも自身の初期化(プレイヤー作成)処理を開始する。
  4. 本プラグインは100msごとにその他プラグインの初期化状態をポーリングで監視する。その他プラグインがプレイヤーを生成したことを YT.get() で検出できれば、安全にそのインスタンスへイベントを相乗りさせる。
  5. もしその他プラグインがプレイヤーを作成しなかった場合は、ランダムジッター(遅延のばらつき)を経た後に、本プラグイン自身がプレイヤーを安全に新規作成する。

動画ごとにプロセスを完全に独立させることで、読者がページ表示の何分後にスクロールを開始したとしても、「動画が出現したまさにその瞬間」に必要な処理だけがピンポイントで実行されるようになりました。これによりLazyLoad系プラグインだけではなく、第3第4のYouTube API依存プラグインとの共存可能性をも最大化した実装となったことがおわかりいただけるかと思います(ランダムジッターの役割については前回記事をご参照ください)。


まとめ: 「時間軸の広がり」に寄り添う共存のあり方

プラグイン開発において、遅延読み込み(LazyLoad)との競合は非常によくある課題です。これまではページロード直後のわずか数秒間のせめぎあいの中にあった初期化処理の順番調整に、LazyLoadは「読者のスクロール速度」という予測不能かつ長大な時間軸を持ち込みます。KU Sticky Video for YouTubeプラグインでも、以前はこの「無限に広がりうる時間軸の存在」を考慮できておらず、大きな盲点となっていました。

競合を避けるために「LazyLoadには非対応です」といって他プラグインの使用を断念してもらう選択肢もありますが、それではサイト全体の表示速度を改善したいというユーザーの希望を損なうことになってしまいます。幸いなことに今回実装した MutationObserver と動的非同期ポーリングの組み合わせによって、LazyLoad系プラグインによる遅延読み込みの恩恵をそのまま活かしながらKU Sticky Video for YouTubeプラグイン自体もまた安定して動作できるようになりました。

他プラグインとの「譲り合い」による共存を実現するため、状況の変化を検知する仕組みを取り入れ、変化に応じて自身の処理を発動する。こうしたアプローチはYouTube APIに限らず、様々なサードパーティ製スクリプトが複雑に絡み合うWordPress環境で、お互いが心地よく共存しユーザーの思い描く動作を実現するために不可欠な設計パターンではないか、と改めて痛感しているところです。

本記事でご紹介した動的バインディング設計を組み込んだ「KU Sticky Video for YouTube」の通常版(v1.9.0以降)、およびPro版(v1.4.0以降)は、すでにアップデートが配信されています。LazyLoadが有効な環境でも安心して動画追従機能をお試しいただけるかと思います。お気づきの点があれば引き続き、お問い合わせフォームなどからお気軽にお知らせください!