In the recently published article “How to Load the YouTube IFrame Player API Without Breaking Other Plugins,” we discussed how to prevent conflicts in environments where multiple plugins attempt to initialize the YouTube API simultaneously by incorporating a spirit of mutual concession (polling, sharing existing players, and random jitter) into the code.
The WordPress plugin “KU Sticky Video for YouTube,” which incorporates this design, had been operating smoothly on this site without any conflict issues. However, a user reported that the sticky video feature stopped working when used in combination with lazy-loading (LazyLoad) plugins. Upon verification, this was indeed the case!
The polling method discussed in the previous article assumes that the video player (iframe) itself already exists in a static state within the DOM at the time of page load. However, in a lazy-loading environment where video players are loaded dynamically in response to user actions such as scrolling or clicking, the target iframe has not yet been generated at page load. Admittedly, this was a blind spot.
This article explains a dynamic and asynchronous binding design for the YouTube API using MutationObserver to overcome the new challenge of coexisting with LazyLoad plugins.
Why Does It Not Work? The YouTube API Under LazyLoad
To improve initial page load performance, LazyLoad plugins delay the loading of images and iframes using approaches such as the following:
- Attribute rewriting: The plugin assigns a placeholder or empty value to the
srcattribute, moving the actual URL to a separate attribute such asdata-src. Once the element approaches the viewport during scrolling, JavaScript writes the actual URL back to thesrcattribute. - Dynamic DOM insertion: The
iframeelement itself is not generated in the DOM until it enters the viewport. Instead, a placeholder image of the video player is displayed. When the play button on the placeholder image is clicked, theiframeis dynamically generated and displayed in its place.
In contrast, many YouTube-related plugins, including KU Sticky Video for YouTube, execute document.querySelectorAll('iframe') at timings such as DOMContentLoaded to perform initialization and register event listeners on the elements existing at that moment. However, when LazyLoad is enabled, the iframe elements for embedded YouTube videos are either not yet generated at this timing, or are intentionally generated in an incomplete state.
Plugins dependent on the YouTube API attempt to search for target iframes under these conditions. However, no matching iframes are found. As a result, the initialization process is skipped, and the plugin’s functionality—in the case of KU Sticky Video for YouTube, the sticky video feature that follows the scroll—never triggers.
The polling-based countermeasure implemented in the previous article was also largely ineffective against LazyLoad plugins. Since polling begins immediately after page load, the delay in execution is at most a few seconds. Meanwhile, it is impossible to predict when a reader will scroll down the article. While one could theoretically poll indefinitely, polling requires periodic timer events, making indefinite waiting impractical.
After all, a reader might step away to the restroom immediately after opening the page, bump into a colleague in the hallway on the way back, chat for a while, and only begin scrolling the article after finally returning to their seat. If setInterval continues to run for minutes, or potentially over an hour, while waiting for them, the browser cannot enter an idle state. Especially on mobile devices, this would result in the birth of a “mysteriously battery-draining page.” We would certainly like to avoid that.
The Solution: Dynamic Binding Using MutationObserver
Detecting Videos and Preventing Double Initialization with MutationObserver
To accommodate various lazy-loading approaches, we configure the MutationObserver to monitor two types of mutation events simultaneously: the dynamic addition of DOM nodes and changes to the src attribute of existing elements.
function initMutationObserver() {
const observer = new MutationObserver(function(mutations) {
const detectedIframes = [];
mutations.forEach(function(mutation) {
// Pattern A: Monitor the dynamic addition of DOM nodes
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(function(node) {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.tagName === 'IFRAME') {
detectedIframes.push(node);
} else {
// Extract nested iframes as well
const nested = node.querySelectorAll('iframe');
nested.forEach(iframe => detectedIframes.push(iframe));
}
}
});
}
// Pattern B: Monitor changes to the src attribute of existing elements
else if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
const node = mutation.target;
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'IFRAME') {
detectedIframes.push(node);
}
}
});
// Filter and process only YouTube iframes from the detected ones
const youtubeIframes = detectedIframes.filter(isYouTubeIframe);
if (youtubeIframes.length > 0) {
handleNewIframes(youtubeIframes);
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['src'] // Restrict monitoring to the src attribute to minimize performance impact
});
}
This allows the implementation to instantly capture the appearance of a video, regardless of the mechanism the LazyLoad plugin uses to delay it. Specifying attributeFilter: ['src'] limits the attribute monitoring to src, minimizing unnecessary overhead.
Furthermore, since a MutationObserver triggers frequently, detection events can occur multiple times for the same iframe. If double initialization occurs, it would hinder not only coexistence with other plugins but also the plugin’s own processing. To reliably prevent this double processing, we assign a custom data- attribute to the iframe under process to manage its state.
function isYouTubeIframe(iframe) {
if (!iframe || iframe.tagName !== 'IFRAME') {
return false;
}
// Exclude elements already processed by this plugin
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 => {
// Mark as processed
iframe.setAttribute('data-ku-sticky-processed', 'true');
// Execute initialization and binding for this iframe
initPlayingMode([iframe]);
});
}
Elements with the data-ku-sticky-processed attribute are immediately excluded from future MutationObserver detections, reliably avoiding unnecessary loops and conflict errors.
Dynamically Triggered Conflict Avoidance Process
This is the core of the “dynamic binding” design. While the polling and random jitter introduced in the previous article were triggered based on a static timing like page load, the current design using MutationObserver shifts to a dynamic model: every time a video appears on the page, an asynchronous process starting from conflict avoidance (mutual concession) to safe binding is initiated specifically for that video.
function initPlayingMode(iframes) {
if (iframes.length === 0) return;
// 1. Insert the YouTube API script if it has not been loaded yet
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. Start polling for this video, waiting for other plugins to complete initialization
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++;
}
// Check if other plugins have already created a player instance for this 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;
}
}
}
// Termination condition: shared target found or wait time limit exceeded
const shouldStop = allFound || apiReadyAttempts >= 3 || attempts >= 30;
if (shouldStop) {
clearInterval(checkInterval);
if (!setupCompleted) {
setupCompleted = true;
// 3. Apply random jitter (0 to 100ms delay)
const jitterDelay = Math.random() * 100;
setTimeout(() => {
setupPlayers(iframes);
}, jitterDelay);
}
}
}, 100);
}
With this asynchronous design, the following scenario executes autonomously. The actors involved are as follows:
- KU Sticky Video for YouTube plugin: The subject of this article. Referred to as “this plugin.”
- LazyLoad plugin: A plugin that reduces page weight by lazy-loading YouTube videos during scrolling or other actions. Examples include Lazy Load for Videos.
- Other plugins dependent on the YouTube API: Third-party plugins that use the YouTube API to apply various effects to YouTube videos on the page. Referred to as “other plugins.”
The primary goal of this article is to make this plugin coexist with the lazy-loading plugin (No. 2). While coexistence with other plugins (No. 3) was achieved in the previous article, our objective here is to ensure that coexistence with other plugins remains intact even after achieving compatibility with LazyLoad plugins.
The actual execution flow is as follows:
- When a visitor scrolls down the page or performs a similar action, the LazyLoad plugin loads the YouTube video (iframe).
- The
MutationObserverof this plugin detects its appearance, checks for the presence of the API, and starts polling. - Simultaneously, the other plugin initiates its own initialization (player creation) process for the same iframe.
- This plugin polls the initialization state of the other plugin every 100ms. If it detects that the other plugin has created a player instance using
YT.get(), it safely shares events with that instance. - If the other plugin does not create a player instance, this plugin creates its own player instance after a random jitter delay.
By keeping the process for each video completely independent, the necessary processing is executed at the exact moment the video appears, regardless of how long the reader waits before scrolling. This implementation maximizes coexistence not only with LazyLoad plugins but also with other YouTube API-dependent plugins (refer to the previous article for details on the role of random jitter).
Conclusion: Coexistence Adapting to an Expanded Time Dimension
In plugin development, conflicts with lazy loading (LazyLoad) are a common challenge. Previously, initialization order was adjusted within a tight window of a few seconds immediately after page load. LazyLoad, however, introduces an unpredictable and extended time dimension dictated by the reader’s scroll speed. The KU Sticky Video for YouTube plugin previously failed to account for this variable time frame, which was a significant oversight.
One could avoid conflicts by declaring incompatibility with LazyLoad plugins and requesting users to disable them, but this compromises the user’s desire to optimize site loading speed. Fortunately, the combination of MutationObserver and dynamic asynchronous polling implemented here allows the KU Sticky Video for YouTube plugin to operate stably while preserving the performance benefits of lazy-loading plugins.
To achieve coexistence through mutual concession, it is crucial to detect changes in the environment and trigger actions accordingly. I am reminded that this approach is an essential design pattern for ensuring that various third-party scripts coexist smoothly and perform as intended in the complex WordPress ecosystem.
The dynamic binding design introduced in this article has already been deployed in updates for the free version (v1.9.0 and later) and the Pro version (v1.4.0 and later) of “KU Sticky Video for YouTube.” The sticky video feature should now work reliably even when lazy loading is active. If you have any feedback, please feel free to let us know via the contact form!
Top comments (0)