Quick Answer
Lazy loading defers off-screen content downloads, prioritizing visible content for faster LCP and better user experience. The problem: D2C homepage is masterpiece with high-res hero video, carousel of 20 "Best Sellers," Instagram feed widget, customer review section with 50 avatars. Browser tries to download everything when user lands. Instagram widget blocks main thread. 50th avatar (at bottom) competes for bandwidth with hero video. LCP hits 4.5 seconds. Google penalizes ranking. Users bounce. The solution: Lazy loading tells browser "Prioritize hero video. Don't touch Instagram widget or footer images until user scrolls down." Not about removing content, about sequencing it. 4 strategies: (1) Native image lazy loading = loading="lazy" attribute on <img> tags for product grids below fold, footer logos, blog thumbnails. DON'T use on hero image (first visible image) = lazy loading hero waits to fetch = increases LCP time. Always eager-load hero. Implementation: <img loading="lazy" width="512" height="512"/> with width/height prevents Layout Shift (CLS). (2) OWL dynamic imports = heavy "Product Configurator" or "3D Viewer" requires 2MB Three.js library, don't load on homepage. Use loadScript("/lib/heavy_library.js") in onWillStart, browser ONLY downloads when component mounted. Use t-if not CSS d-none (component doesn't exist until condition met). (3) Facade pattern = third-party scripts (Intercom, Drift, OdooLivechat) are performance killers, download massive bundles for tiny icon. Render fake button first, load real script on hover/click = saves 500KB JavaScript from initial page load, creates illusion of instant availability. (4) Infinite scroll = instead of loading 100 products (heavy DOM), load 20, when user scrolls to bottom fetch next 20. Use IntersectionObserver API, Odoo native Website Builder has "Infinite Scroll" option. Impact: 150+ Odoo systems implemented, smart lazy loading = difference between "heavy" site and "snappy" site, better Google ranking, no bounce.
The Performance Problem
Your D2C homepage is a masterpiece. It features a high-res Hero Video, a carousel of 20 "Best Sellers," an Instagram feed widget, and a customer review section with 50 avatars.
The Reality Check
• Browser tries to download everything the moment user lands
• Instagram widget blocks the main thread
• 50th avatar (at bottom) competes for bandwidth with Hero Video
• LCP (Largest Contentful Paint) hits 4.5 seconds
• Google penalizes your ranking. Users bounce.
The Solution: Lazy Loading
You tell the browser: "Prioritize the Hero Video. Don't touch the Instagram widget or the footer images until the user actually scrolls down to them."
We've implemented 150+ Odoo systems. Smart lazy loading is the difference between a "heavy" site and a "snappy" site. It's not about removing content; it's about sequencing it.
Strategy 1: Native Image Lazy Loading
This is the low-hanging fruit. Modern browsers support the loading="lazy" attribute on <img> tags.
When to Use
✓ Product grids below the fold
✓ Footer logos
✓ Blog post thumbnails
When NOT to Use
⚠️ Critical Rule:
The Hero Image: The first image the user sees. If you lazy load this, the browser waits to fetch it, increasing your LCP time. Always eager-load the hero.
Implementation (QWeb)
<!-- HERO IMAGE (Eager Load - Default) -->
<img t-att-src="image_data_uri(product.image_1920)"
class="hero-img"/>
<!-- PRODUCT GRID (Lazy Load) -->
<t t-foreach="products" t-as="product">
<div class="product-card">
<img t-att-src="image_data_uri(product.image_512)"
loading="lazy"
width="512"
height="512"
alt="Product Name"/>
</div>
</t>
Best Practice:
Always include width and height attributes when lazy loading. This prevents "Layout Shift" (CLS) where the text jumps around as images pop in.
Strategy 2: Lazy Loading OWL Components
You have a heavy "Product Configurator" or "3D Viewer" widget. It requires a 2MB JavaScript library (Three.js). You don't want to load that library on the Homepage.
The Solution: Dynamic Import.
Implementation
/** @odoo-module */
import { Component, onWillStart, useState } from "@odoo/owl";
import { loadScript } from "@web/core/assets";
export class HeavyWidget extends Component {
setup() {
this.state = useState({ isLoaded: false });
onWillStart(async () => {
// Browser ONLY downloads this file when component is mounted
await loadScript("/my_module/static/lib/heavy_library.js");
this.state.isLoaded = true;
});
}
}
Now, ensure this component is only mounted when needed. If it's inside a tab that is hidden by default, use t-if instead of CSS hiding.
<!-- WRONG: Component renders (and downloads script) but is invisible -->
<div class="d-none">
<HeavyWidget/>
</div>
<!-- RIGHT: Component does not exist until condition is met -->
<t t-if="state.activeTab === '3d_view'">
<HeavyWidget/>
</t>
Strategy 3: Deferring Third-Party Scripts (Facade Pattern)
You have a "Chat with Support" widget (Intercom, Drift, OdooLivechat). These scripts are notorious performance killers. They download massive bundles just to show a tiny icon.
The Solution: Render a fake button (a Facade) first. Only load the real script when the user hovers or clicks.
Implementation Steps
1. HTML: Create a static HTML button that looks exactly like the Chat bubble
2. JS: Add a "one-time" event listener to it
const chatButton = document.querySelector('.fake-chat-btn');
chatButton.addEventListener('mouseover', () => {
// User is interested! Now load the real script.
const script = document.createElement('script');
script.src = "https://widget.intercom.io/...";
document.body.appendChild(script);
// Remove the fake button or hide it as the real one initializes
}, { once: true });
Saves 500KB JavaScript
Creates the illusion of instant availability while saving the initial page load from 500KB of JavaScript.
Strategy 4: Infinite Scroll (Lazy Pagination)
Instead of loading 100 products on the shop page (heavy DOM), load 20. When the user scrolls to the bottom, fetch the next 20.
Odoo Native Support
The Website Builder has an "Infinite Scroll" option for the Shop page.
1. Go to Shop
2. Click Edit
3. Select the Product Grid
4. Enable Infinite Scroll
Custom Implementation (OWL)
Use the IntersectionObserver API.
setup() {
this.observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
this.loadMoreProducts();
}
});
onMounted(() => {
this.observer.observe(document.querySelector('.load-more-trigger'));
});
}
Lazy Loading Decision Matrix
| Content Type | Lazy Load? | Method |
|---|---|---|
| Hero Image | NO (Eager load) | Default <img> tag |
| Product Grid Images | YES | loading="lazy" |
| Footer Logos | YES | loading="lazy" |
| Heavy JS Library (Three.js) | YES | loadScript() in onWillStart |
| Chat Widget (Intercom/Drift) | YES | Facade pattern (load on hover) |
| Product List (100 items) | YES | Infinite scroll (IntersectionObserver) |
Your Action Items
Image Audit
❏ Go to your Homepage. Right-click → Inspect
❏ Check the <img> tags below the fold. Do they have loading="lazy"?
❏ Check the Hero image. Does it NOT have loading="lazy"?
Script Audit
❏ Use Google PageSpeed Insights
❏ Look at "Remove unused JavaScript." Is there a huge file for a feature (like a Map or Chat) that isn't being used immediately?
❏ Implement the "Facade" pattern or dynamic imports
Layout Stability
❏ Are your lazy-loaded images causing the page to jump? Add width and height attributes to the HTML
Free Frontend Performance Workshop
Is your site heavy? We'll implement native lazy loading on your QWeb templates, refactor your heavy OWL components to use dynamic imports, build "Facades" for your third-party chat/analytics scripts, fix Cumulative Layout Shift (CLS) caused by images. Make your site feel lightweight, even with heavy content.
