Skip to content

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.

Before making any modifications, it is necessary to audit the current state of the project:

  1. 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.
  2. Verify Components: Check if the current project utilizes Sliders, Marquees, or “Load More” buttons/dynamic filters.
  3. Verify Images: If these components exist, check if they contain <img>, <picture> tags, or background images.
  4. 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.

We must remove the old library (blazy) and install the new TerraHQ solution.

Open your terminal at the project root and run:

Terminal window
npm uninstall blazy
Terminal window
npm install @terrahq/lazy

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;

We must disable the native lazy loading feature of the slider library and rely on our global @terrahq/lazy instance via Dependency Injection.

Remove any native lazy load properties:

// Remove: lazyload: true and lazyloadSelector
const sliderConfig = {
// ... other settings
// lazyload: false
};

Inject the Manager and create the transition callback:

src/js/handler/slider/Handler.js
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();
}
}
};
};

Trigger the callback when the slider finishes its transition:

src/js/handler/slider/Slider.js
init() {
this.slider = tns(this.config);
if (this.onSlideTransitionEnd && typeof this.onSlideTransitionEnd === 'function') {
this.slider.events.on('transitionEnd', () => {
this.onSlideTransitionEnd();
});
}
}

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);
}
?>

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"
/>

For marquees animated with GSAP, the IntersectionObserver detects images naturally as they enter the viewport via CSS transforms.

It is critical to pass the global Manager to the marquee configuration so it is available if needed.

src/js/handler/marquee/Handler.js
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
}
};

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);
?>

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.

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.

Inject the Manager into the component:

src/js/handler/loadInsights/Handler.js
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:

LoadInsights.js
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
}
}
}

If your project does not have Punky installed, you need to make several adjustments to the implementation.

In projects without Punky, you need to manually manage instances using this.instances (defined in Core.js).

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;
}

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().

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.js
this.instances = [];
// Add lazy instance directly to this.instances
this.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:

src/js/Main.js
this.instances["Script"] = new Script({
/** Other params */
instances: this.instances,
});
src/js/../Script.js
const lazyInstances = this.payload.instances?.["Lazy"];
if (lazyInstances && lazyInstances.length > 0) {
lazyInstances[0].revalidate();
}

After implementing lazy loading, verify:

  • Old blazy library has been uninstalled
  • New @terrahq/lazy library has been installed
  • Main.js updated all blazy references to lazy
  • Core.js has 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' => true and 'lazyClass' => 'g--lazy-01' in PHP
  • Load More/AJAX components call revalidate() after injecting HTML
  • All PHP image generation includes 'isLazy' => true and '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...