Skip to content

Instantly share code, notes, and snippets.

@milllan
Forked from khoipro/sample-lazyload-sections.php
Last active June 13, 2025 20:30
Show Gist options
  • Save milllan/d822784e319fb52f3a11a7a553461296 to your computer and use it in GitHub Desktop.
Save milllan/d822784e319fb52f3a11a7a553461296 to your computer and use it in GitHub Desktop.
Sample section lazyload using <noscript> - DRAFTING
<?php
/**
* miLL Advanced Lazyload Sections
* Version: 5.0 (Server-Side JS Generation)
*
* =================================================================================
*
* DESCRIPTION
*
* This script intelligently lazy-loads specified HTML sections on WordPress pages
* to improve initial page load times (lower DOM size) and Core Web Vitals scores.
*
* Its key feature is its server-side conditional logic, which generates and injects
* ONLY the minimal JavaScript required for the chosen lazy-loading strategy. This
* ensures maximum performance by not sending unnecessary code to the browser.
*
* =================================================================================
*
* KEY FEATURES
*
* 1. **Optimized Performance:** Only the JavaScript for the selected trigger method
* is added to the page, resulting in a smaller footprint.
* 2. **Multiple Trigger Methods:** Choose the best strategy for any situation:
* - `intersection_observer`: High-performance, for content below the fold.
* - `scroll_and_resize`: For content hidden by CSS (like tabs or accordions).
* - `mouseover`: For content that appears on hover (like menus or tooltips).
* 3. **Parent Element Trigger:** The `mouseover` method can be attached to a
* parent element (e.g., a menu link) to load a child element (e.g., a dropdown),
* mimicking natural site interaction.
* 4. **Robust & Safe:** Uses PHP's `DOMDocument` for reliable HTML parsing, avoiding
* issues with `str_replace` or regular expressions.
* 5. **Accessible Fallback:** Automatically wraps content in `<noscript>` tags,
* ensuring it remains accessible if JavaScript is disabled or fails.
*
* =================================================================================
*
* HOW TO CONFIGURE
*
* Adjust the variables in the "Configuration" section of the function below.
*
* --- VARIABLE GUIDE ---
*
* $target_section_php_classes (string)
* - What: Space-separated CSS classes PHP uses to find the element.
* - Example: 'related-posts-section another-class'
*
* $target_section_js_selector (string)
* - What: The corresponding CSS selector JavaScript will use.
* - Example: '.related-posts-section.another-class'
*
* $target_element_tag (string)
* - What: The HTML tag of the element to target.
* - Example: 'div', 'section', 'aside'
*
* $js_trigger_method (string) - THIS IS THE MOST IMPORTANT SETTING
* - What: Defines the lazy-load strategy.
* - Options:
* - 'intersection_observer': For content that is simply off-screen.
* - 'scroll_and_resize': For content that is on-screen but hidden by CSS
* (e.g., `visibility: hidden`, `opacity: 0`, `display: none`).
* - 'mouseover': For content that should load on user hover.
*
* $mouseover_trigger_parent_selector (string)
* - What: **Only used if `$js_trigger_method` is 'mouseover'**.
* This is the CSS selector of the parent element that the user will
* actually hover on to trigger the loading.
* - Example: '.menu-item-has-children'
*
* $intersection_observer_root_margin (string)
* - What: **Only used if `$js_trigger_method` is 'intersection_observer'**.
* Defines a margin around the viewport to trigger loading earlier.
* - Example: '500px 0px 500px 0px' (loads when element is 500px away)
*
* =================================================================================
*
* CONFIGURATION EXAMPLES
*
* ---
* Scenario 1: Lazy Loading a "Related Posts" Section Below the Fold
* ---
* The section is not visible on initial load. `intersection_observer` is perfect.
*
* $target_section_php_classes = 'related-posts-wrapper';
* $target_section_js_selector = '.related-posts-wrapper';
* $target_element_tag = 'div';
* $js_trigger_method = 'intersection_observer';
*
* ---
* Scenario 2: Lazy Loading a Navigation Menu Dropdown (e.g., Woodmart theme)
* ---
* The dropdown content should only load when a user hovers over the parent menu item.
*
* $target_section_php_classes = 'wd-dropdown-menu wd-dropdown';
* $target_section_js_selector = '.wd-dropdown-menu.wd-dropdown';
* $target_element_tag = 'div';
* $js_trigger_method = 'mouseover';
* $mouseover_trigger_parent_selector = '.menu-item-has-children'; // The <li> user hovers on
*
* ---
* Scenario 3: Lazy Loading Hidden Content in Tabs or Accordions
* ---
* The tab panels are in the DOM but hidden with `display: none`. They only become
* visible when a tab is clicked. `scroll_and_resize` will detect this visibility change.
*
* $target_section_php_classes = 'tab-content-panel';
* $target_section_js_selector = '.tab-content-panel';
* $target_element_tag = 'div';
* $js_trigger_method = 'scroll_and_resize';
*
* =================================================================================
*
* IMPORTANT NOTE:
* After making changes, always clear all caches (WP Rocket, server cache, browser cache)
* to see the effect.
*
*/
//add_filter('the_content', 'miLL_lazyload_home_sections', 1000);
add_filter('rocket_buffer', 'miLL_lazyload_sections', 1000);
function miLL_lazyload_sections( $content ) {
// --- Main Configuration ---
$target_section_php_classes = 'wd-dropdown-menu wd-dropdown';
$target_section_js_selector = '.wd-dropdown-menu.wd-dropdown';
$target_element_tag = 'div';
// *** CHOOSE YOUR LAZY-LOAD TRIGGER METHOD HERE ***
$js_trigger_method = 'mouseover'; // Options: 'intersection_observer', 'scroll_and_resize', 'mouseover'
// --- Advanced Configuration ---
$mouseover_trigger_parent_selector = 'li.menu-item-has-children';
$intersection_observer_root_margin = '400px';
$apply_to_logged_in_users = false;
$lazyload_trigger_class = 'miLL-lazy-section';
$lazyload_loaded_class = 'miLL-lazy-section--loaded';
$comment_text = 'Lazyload section by miLL';
// --- End Configuration ---
// (The robust DOM parsing logic remains unchanged)
if ( ! $apply_to_logged_in_users && is_user_logged_in() ) { return $content; }
if ( empty(trim($content)) || empty(trim($target_section_php_classes)) || empty(trim($target_element_tag)) ) { return $content; }
if (stripos($content, '<html') === false && stripos($content, '<body') === false) { return $content; }
$doc = new DOMDocument();
libxml_use_internal_errors(true);
if (!$doc->loadHTML($content, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD)) { libxml_clear_errors(); return $content; }
libxml_clear_errors();
$xpath = new DOMXPath($doc);
$sections_modified_count = 0;
$classes_to_find = explode(' ', trim($target_section_php_classes));
$xpath_class_conditions = [];
foreach ($classes_to_find as $class) { if (!empty($class)) { $xpath_class_conditions[] = "contains(concat(' ', normalize-space(@class), ' '), ' " . trim($class) . " ')"; } }
if (empty($xpath_class_conditions)) { return $content; }
$query = "//descendant::" . $target_element_tag . "[" . implode(" and ", $xpath_class_conditions) . "]";
$target_nodes = $xpath->query($query);
if ($target_nodes && $target_nodes->length > 0) {
foreach ($target_nodes as $section_node) {
if (!$section_node instanceof DOMElement) { continue; }
$current_classes = $section_node->getAttribute('class');
if (strpos(' ' . $current_classes . ' ', ' ' . $lazyload_trigger_class . ' ') === false) { $section_node->setAttribute('class', trim($current_classes . ' ' . $lazyload_trigger_class)); }
$noscript_node = $doc->createElement('noscript');
while ($section_node->hasChildNodes()) { $child = $section_node->firstChild; $section_node->removeChild($child); $noscript_node->appendChild($child); }
$section_node->appendChild($noscript_node);
$comment_node = $doc->createComment(' ' . $comment_text . ' ');
if ($section_node->nextSibling) { $section_node->parentNode->insertBefore($comment_node, $section_node->nextSibling); } else { $section_node->parentNode->appendChild($comment_node); }
$sections_modified_count++;
}
}
if ($sections_modified_count > 0) {
// Escape all JS variables
$escaped_js_selector = esc_js($target_section_js_selector);
$escaped_lazy_trigger_class = esc_js($lazyload_trigger_class);
$escaped_lazy_loaded_class = esc_js($lazyload_loaded_class);
$escaped_root_margin_config = esc_js($intersection_observer_root_margin);
$escaped_mouseover_trigger_selector = esc_js($mouseover_trigger_parent_selector);
// --- DYNAMICALLY BUILD THE JAVASCRIPT ---
$js_trigger_logic = '';
switch ($js_trigger_method) {
case 'intersection_observer':
$js_trigger_logic = <<<JS
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadSectionContent(entry.target);
obs.unobserve(entry.target);
}
});
}, { rootMargin: '{$escaped_root_margin_config}', threshold: 0.01 });
sectionsToLazyLoad.forEach(section => observer.observe(section));
}
JS;
break;
case 'scroll_and_resize':
$js_trigger_logic = <<<JS
let debounceTimer;
const debounce = (func, d=150) => { return (...a) => { clearTimeout(debounceTimer); debounceTimer=setTimeout(()=>func.apply(this,a),d);};};
const isVisible = (el) => { const s=window.getComputedStyle(el); if(s.display==='none'||s.visibility==='hidden'||parseFloat(s.opacity)===0) return false; const r=el.getBoundingClientRect(); if(r.width===0&&r.height===0) return false; const vh=(window.innerHeight||document.documentElement.clientHeight); const vw=(window.innerWidth||document.documentElement.clientWidth); return(r.top<=vh)&&((r.top+r.height)>=0)&&(r.left<=vw)&&((r.left+r.width)>=0);};
const processQueue = () => sectionsToLazyLoad.forEach(s => { if (!s.classList.contains(lazyLoadedClass) && isVisible(s)) loadSectionContent(s); });
const dProcess = debounce(processQueue, 200);
document.addEventListener('DOMContentLoaded', processQueue); window.addEventListener('load', processQueue); window.addEventListener('scroll', dProcess); window.addEventListener('resize', dProcess);
JS;
break;
case 'mouseover':
$js_trigger_logic = <<<JS
const parentSelector = '{$escaped_mouseover_trigger_selector}';
if (!parentSelector) {
console.warn("miLL: 'mouseover' trigger is used, but no parent selector is defined. Falling back to self-hover.");
sectionsToLazyLoad.forEach(section => {
section.addEventListener('mouseover', () => loadSectionContent(section), { once: true });
});
} else {
sectionsToLazyLoad.forEach(section => {
const triggerElement = section.closest(parentSelector);
if (triggerElement) {
triggerElement.addEventListener('mouseover', () => loadSectionContent(section), { once: true });
} else {
console.warn('miLL: Could not find parent (' + parentSelector + ') for section:', section);
section.addEventListener('mouseover', () => loadSectionContent(section), { once: true });
}
});
}
JS;
break;
}
// Assemble the final script with the common parts and the chosen trigger logic
$js_to_inject = <<<JAVASCRIPT
<script id="miLL-lazyload-sections-js">
// <![CDATA[
(function() {
const jsSelector = '{$escaped_js_selector}.{$escaped_lazy_trigger_class}';
const lazyLoadedClass = '{$escaped_lazy_loaded_class}';
const sectionsToLazyLoad = document.querySelectorAll(jsSelector);
if (!sectionsToLazyLoad.length) { return; }
const loadSectionContent = (sectionEl) => {
if (!sectionEl || sectionEl.classList.contains(lazyLoadedClass)) { return; }
const noscriptTag = sectionEl.getElementsByTagName('noscript')[0];
if (!noscriptTag) { return; }
const htmlContent = noscriptTag.textContent || noscriptTag.innerHTML;
if (htmlContent) {
const tempDiv = document.createElement('div');
tempDiv.innerHTML = htmlContent.trim();
while (tempDiv.firstChild) {
sectionEl.insertBefore(tempDiv.firstChild, noscriptTag);
}
sectionEl.removeChild(noscriptTag);
sectionEl.classList.add(lazyLoadedClass);
sectionEl.classList.remove('{$escaped_lazy_trigger_class}');
console.log('miLL: Lazy-loaded section via {$js_trigger_method}', sectionEl);
}
};
// --- Begin Trigger-Specific Logic ---
{$js_trigger_logic}
// --- End Trigger-Specific Logic ---
})();
// ]]>
</script>
<!-- miLL Lazyload Sections JS -->
JAVASCRIPT;
$script_fragment = $doc->createDocumentFragment();
@$script_fragment->appendXML($js_to_inject);
$body_nodes = $xpath->query('//body');
if ($body_nodes->length > 0) { $body_node = $body_nodes->item(0); $body_node->appendChild($script_fragment); }
else { if ($doc->documentElement) { $doc->documentElement->appendChild($script_fragment); } }
$content = $doc->saveHTML();
}
return $content;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment