-
-
Save milllan/d822784e319fb52f3a11a7a553461296 to your computer and use it in GitHub Desktop.
Sample section lazyload using <noscript> - DRAFTING
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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