Lazy
This guide details the standard process for implementing lazy loading for images using the @terrahq/lazy library. It is specifically aimed at optimizing the performance of images located within three key project components:
- Sliders — Carousels using
tiny-slider. - Marquee — Infinite marquees using GSAP.
- Load More — Dynamic content loading via AJAX.
Preliminary Project Audit
Section titled “Preliminary Project Audit”Before making any modifications, it is necessary to audit the current state of the project:
- Punky Dependency: If your project doesn’t have Punky installed, see the Projects Without Punky section at the end of this guide for important implementation differences.
- Verify Components: Check if the current project utilizes Sliders, Marquees, or “Load More” buttons/dynamic filters.
- Verify Images: If these components exist, check if they contain
<img>,<picture>tags, or background images. - Decision: If the project includes these elements with images, it is mandatory to proceed with the following steps to migrate and implement the new lazy load library.
Migration: Replacing Libraries
Section titled “Migration: Replacing Libraries”We must remove the old library (blazy) and install the new TerraHQ solution.
Uninstallation
Section titled “Uninstallation”Open your terminal at the project root and run:
npm uninstall blazyInstalling the new library
Section titled “Installing the new library”npm install @terrahq/lazyGlobal Configuration (Core.js)
Section titled “Global Configuration (Core.js)”For Single Page Applications (SPA) using routers like Swup, the library must be initialized upon entering a page and destroyed upon leaving to prevent memory leaks.
Below are the specific lines that need to be modified or added in your Core.js:
import Lazy from '@terrahq/lazy';// ... other imports
class Core { // ... constructor, init, and events methods remain the same
contentReplaced() { // Initialize the Lazy library when new content enters the DOM if (this.lazy?.enable) { const lazySelector = this.lazy?.selector ? this.lazy?.selector : "g--lazy-01"; this.Manager.addInstance({ name: "Lazy", instance: new Lazy({ selector: "." + lazySelector, successClass: `${lazySelector}--is-loaded`, errorClass: `${lazySelector}--is-error`, loadInvisible: true, // Crucial for detecting hidden images }), method: "Core", }); }
this.firstLoad = false; }
willReplaceContent() { // Destroy the observer before leaving the page to prevent memory leaks if (this.lazy.enable) { // Safety check for projects without Punky if (this.debug && typeof this.debug.instance === "function") { this.debug.instance(`❌ Destroy: Lazy`, { color: "red" }); }
if (this.Manager.instances["Lazy"]) { this.Manager.instances["Lazy"].forEach((instance) => { instance.instance.destroy(); }); } this.Manager.cleanInstances("Lazy"); } }}
export default Core;Component Implementation
Section titled “Component Implementation”Sliders (tiny-slider)
Section titled “Sliders (tiny-slider)”We must disable the native lazy loading feature of the slider library and rely on our global @terrahq/lazy instance via Dependency Injection.
A. In slidersConfig.js
Section titled “A. In slidersConfig.js”Remove any native lazy load properties:
// Remove: lazyload: true and lazyloadSelectorconst sliderConfig = { // ... other settings // lazyload: false};B. In Handler.js
Section titled “B. In Handler.js”Inject the Manager and create the transition callback:
this.configSlider = ({element}) => { return { slider: element, config: sliderConfig, Manager: this.Manager, onSlideTransitionEnd: () => { const lazyInstances = this.Manager.getInstances('Lazy'); if (lazyInstances && lazyInstances.length > 0) { lazyInstances[0].instance.revalidate(); } } };};C. In Slider.js
Section titled “C. In Slider.js”Trigger the callback when the slider finishes its transition:
init() { this.slider = tns(this.config); if (this.onSlideTransitionEnd && typeof this.onSlideTransitionEnd === 'function') { this.slider.events.on('transitionEnd', () => { this.onSlideTransitionEnd(); }); }}D. PHP Image Output
Section titled “D. PHP Image Output”Ensure that the images inside the slider are configured properly in the backend. You must pass 'isLazy' => true and 'lazyClass' => 'g--lazy-01' to your image generation function:
<?php$image_tag_args = array( 'image' => $image_to_use, 'sizes' => '(max-width: 810px) 50vw, 100vw', 'class' => 'c--card-m__wrapper__media', 'isLazy' => true, 'lazyClass' => 'g--lazy-01', 'showAspectRatio' => true, 'decodingAsync' => true, 'fetchPriority' => false, 'addFigcaption' => false,);if ($image_to_use) { generate_image_tag($image_tag_args);}?>E. Astro Projects (astro-core)
Section titled “E. Astro Projects (astro-core)”If you’re using astro-core components, make sure to pass the lazy configuration to the Asset component:
<Asset src={imageSrc} alt="Description" lazy={true} lazyClass="g--lazy-01"/>Marquee (GSAP Infinite Marquee)
Section titled “Marquee (GSAP Infinite Marquee)”For marquees animated with GSAP, the IntersectionObserver detects images naturally as they enter the viewport via CSS transforms.
A. In Handler.js
Section titled “A. In Handler.js”It is critical to pass the global Manager to the marquee configuration so it is available if needed.
this.configMarquee = ({element}) => { return { element: element, Manager: this.Manager, // <-- Dependency Injection added here speed: element.getAttribute("data-speed") ? parseFloat(element.getAttribute("data-speed")) : 1, // ... other configurations }};B. InfiniteMarquee.js
Section titled “B. InfiniteMarquee.js”No changes are required in this file. Leave it exactly as it is.
C. PHP Image Output (Backend Configuration)
Section titled “C. PHP Image Output (Backend Configuration)”Just like with sliders, ensure that images inside the marquee are configured properly in the backend. You must pass 'isLazy' => true and 'lazyClass' => 'g--lazy-01' to your image generation function:
<?php$image_tag_args = array( 'image' => $logo['logo'], 'sizes' => 'large', 'class' => 'c--marquee-a__wrapper__item', 'isLazy' => true, 'lazyClass' => 'g--lazy-01', 'showAspectRatio' => true, 'decodingAsync' => true, 'fetchPriority' => false, 'addFigcaption' => false,);generate_image_tag($image_tag_args);?>D. Layout Fixes (Optional / On Error)
Section titled “D. Layout Fixes (Optional / On Error)”If issues occur, wrap the images in a specific container and apply the following HTML, SCSS, and JS adjustments:
HTML (PHP):
<div class="c--marquee-a js--marquee" data-speed="1" data-controls-on-hover="false" data-reversed=<?= $direction ?> > <?php foreach($logos as $key => $logo): ?> <?php if($logo): ?> <div class="c--marquee-a__wrapper"> <?php $image_tag_args = array( 'image' => $logo['logo'], 'sizes' => 'large', 'class' => $key == 0 ? 'c--marquee-a__wrapper__item c--marquee-a__wrapper__item--initial' : 'c--marquee-a__wrapper__item', 'isLazy' => true, 'showAspectRatio' => true, 'decodingAsync' => true, 'fetchPriority' => false, 'addFigcaption' => false, ); generate_image_tag($image_tag_args); ?> </div> <?php endif; ?> <?php endforeach; ?></div>SCSS:
&__wrapper { max-width: 250px; width: 250px; flex-shrink: 0;
&__item { @extend .u--display-block; height: auto; width: 100%; min-height: 72px; object-fit: contain; }}JS (Handler.js):
If you apply the wrapper fix, make sure to modify the selector in your Handler.js configuration to target the new .c--marquee-a__wrapper elements instead of the items directly.
Dynamic Content (Load More / AJAX)
Section titled “Dynamic Content (Load More / AJAX)”When injecting new HTML into the page (e.g., loading more posts or using dynamic filters), we must force the library to scan for the newly added images without affecting the rest of the layout.
A. In Handler.js
Section titled “A. In Handler.js”Inject the Manager into the component:
this.configLoadInsights = ({element}) => { return { dom: { /* ... */ }, query: { /* ... */ }, Manager: this.Manager, // Dependency Injection };};B. In the Component (e.g., LoadInsights.js)
Section titled “B. In the Component (e.g., LoadInsights.js)”Call revalidate() right after the new HTML is injected into the container:
async loadMore(resetHtml, bool = true) { const results = await this.loadMoreServicePost(this.payload.query);
// 1. Inject the new HTML resetHtml ? (this.payload.dom.resultsContainer.innerHTML = results.data.html) : (this.payload.dom.resultsContainer.innerHTML += results.data.html);
// 2. Notify the Lazy library about the NEW images if (this.payload.Manager && this.payload.Manager.getInstances('Lazy')) { const lazyInstances = this.payload.Manager.getInstances('Lazy'); if (lazyInstances.length > 0) { lazyInstances[0].instance.revalidate(); // Scans for new content } }}Projects Without Punky
Section titled “Projects Without Punky”If your project does not have Punky installed, you need to make several adjustments to the implementation.
Core.js Modifications
Section titled “Core.js Modifications”In projects without Punky, you need to manually manage instances using this.instances (defined in Core.js).
A. Initialize the Instances Array
Section titled “A. Initialize the Instances Array”Before adding the Lazy instance, initialize the array:
contentReplaced() { if (this.lazy?.enable) { const lazySelector = this.lazy?.selector ? this.lazy?.selector : "g--lazy-01";
// 1. Initialize the instances array manually this.Manager.instances["Lazy"] = [];
// 2. Add instance without the extra keys this.Manager.addInstance( "Lazy", new Lazy({ selector: "." + lazySelector, successClass: `${lazySelector}--is-loaded`, errorClass: `${lazySelector}--is-error`, loadInvisible: true, // Crucial for detecting hidden images }), ); }
this.firstLoad = false;}B. Update Revalidate Calls
Section titled “B. Update Revalidate Calls”Because instances are structured differently without Punky, you need to update how you access the revalidate() method:
Change from:
lazyInstances[0].instance.revalidate();To:
lazyInstances[0].revalidate();This applies to all components (Slider, LoadMore, etc.) where you call revalidate().
RCS Projects (No Manager)
Section titled “RCS Projects (No Manager)”For very old projects like RCS that don’t have a Manager at all, you’ll need to use the this.instances array defined in Core.js:
// In Core.jsthis.instances = [];
// Add lazy instance directly to this.instancesthis.instances.push( new Lazy({ selector: ".g--lazy-01", successClass: "g--lazy-01--is-loaded", errorClass: "g--lazy-01--is-error", loadInvisible: true, }));Then, in order to use the revalidate() call, you must first pass this.instances to the given script and get the library instances using this.payload.instances["Lazy"], instead of Manager methods:
this.instances["Script"] = new Script({ /** Other params */ instances: this.instances,});const lazyInstances = this.payload.instances?.["Lazy"];if (lazyInstances && lazyInstances.length > 0) { lazyInstances[0].revalidate();}Summary Checklist
Section titled “Summary Checklist”After implementing lazy loading, verify:
- Old
blazylibrary has been uninstalled - New
@terrahq/lazylibrary has been installed -
Main.jsupdated allblazyreferences tolazy -
Core.jshas been updated with proper initialization and cleanup - Debug calls have safety checks (if no Punky)
- Slider configuration disables native lazy loading
- Slider calls
revalidate()on transition end - Marquee receives Manager via dependency injection
- Marquee images have
'isLazy' => trueand'lazyClass' => 'g--lazy-01'in PHP - Load More/AJAX components call
revalidate()after injecting HTML - All PHP image generation includes
'isLazy' => trueand'lazyClass' => 'g--lazy-01' - If no Punky: instances are initialized manually and
revalidate()is called directly (not on.instance) - If no Punky and no Manager: instances must be initialized and pushed manually and sent to each Script, and access them as
this.payload.instances["Lazy] - Test all components to ensure images load correctly
Knowledge Check
Test your understanding of this section
Loading questions...