Santaji GadeDevelopment, JavaScript3 weeks ago16 Views

A step by step guide to deferring a comments widget until a visitor actually scrolls near it, using IntersectionObserver, a closure based load guard, and a real fetch backed render, verified with live browser tests.
Table of Contents
ToggleHi again! Today's build fixes a specific, very common performance mistake: loading an entire comments section, third party embed included, before a visitor has scrolled anywhere near it.
Ever loaded a blog post and felt the page stutter for a second while a comments widget somewhere far below the fold quietly finished loading? You never even scrolled there yet. A lazy load comments section fix is exactly what this guide builds, proven with a real network request count below, not just a claim.
A typical comments widget, whether homemade or a third party embed like Disqus, ships its own script, its own styling, and a network request for every comment thread, all before a visitor has read a single word of the article above it.
Most visitors never scroll that far at all. Fetching and rendering all of that weight up front, for content the visitor has a real chance of never seeing, is exactly the kind of unnecessary work a good lazy load comments section pattern eliminates entirely.
A long form article, a blog post, or a product review page are the three most common places this pattern earns its keep, since all three tend to place comments at the very bottom, well past the content most visitors actually came to read. A short landing page with comments close to the fold benefits far less, since there is little distance left to defer loading across.
The savings compound across a whole site too. A single deferred request feels small on one page, but multiplied across every article on a busy blog, it adds up to a meaningful amount of bandwidth and script execution time that never has to happen for visitors who read one section and leave. A site publishing dozens of articles a month sees that saving repeated on every single one of them.
The same IntersectionObserver API used for infinite scroll and image lazy loading elsewhere on this site fits this problem just as well, watching a placeholder element and firing the moment it actually enters the viewport.
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && shouldLoad()) {
loadComments();
observer.unobserve(commentsEl);
}
});
});
observer.observe(commentsEl);
Calling observer.unobserve() the moment comments actually load matters for a reason that is easy to overlook: without it, the observer keeps watching and keeps firing every time the section's visibility state changes, scrolling past it, back to it, past it again.
Placing this placeholder element is worth a moment of care too. It needs to sit exactly where the real comments will eventually render, matching roughly the same height, so the page does not visibly jump or reflow the instant a lazy load comments section swap actually happens.
IntersectionObserver has near universal support confirmed on caniuse.com, so no polyfill or fallback is needed for a modern audience.
A real IntersectionObserver callback does not fire exactly once per element. It fires every time the intersection ratio crosses a threshold, which in practice can mean several calls in quick succession as a visitor scrolls past the exact same boundary.
function createLoadGuard() {
let hasLoaded = false;
return function shouldLoad() {
if (hasLoaded) return false;
hasLoaded = true;
return true;
};
}
Without this guard, a fast scroll past the comments boundary can trigger the fetch call more than once before the first one even finishes, doubling up on a real network request for no reason. Six real observer fires against this exact guard prove the point clearly.
This closure based pattern, a private variable captured inside a returned function, keeps the loaded state contained to exactly one comments section. A page with several independent comment threads, one per article in a list view for example, can safely create a separate guard per section without any of them interfering with each other.
Actual output from guard.js, run against 6 real simulated observer fires.
Once the guard allows exactly one load, fetching and rendering the actual comments is a small, ordinary async function.
async function loadComments() {
const res = await fetch('/api/comments');
const data = await res.json();
commentsEl.innerHTML = data.comments
.map((c) => `<p>${c}</p>`)
.join('');
}
Driving a real browser through the entire flow, page load, a real scroll to the comments section, then several more scrolls away and back, proves the whole mechanism end to end against a genuine local API endpoint rather than a mocked one.
Real scrolling, a real local API, and real request counting in Chromium via Playwright.
A comments section sitting 1600 pixels below the fold, roughly the distance used in the real test above, is genuinely never requested by the browser at all under this pattern until the visitor scrolls close enough to trigger the observer, not merely deprioritized or delayed.
A blank box sitting where comments will eventually appear looks broken if there is no visible signal that anything is happening. A simple placeholder message, replaced the instant the real content arrives, fixes that cheaply, an approach CSS-Tricks covers well in a broader guide to lazy loading patterns.
That placeholder also needs to sit inside an ARIA live region so a screen reader announces the comments arriving, the same accessibility gap covered for empty search results in an earlier guide applies here too, silent content changes are easy for a sighted visitor to notice and easy for a screen reader user to miss entirely.
The real proof above already showed the core result: zero comments related network activity until the section is actually approaching the viewport, one request once it does.
| Metric | Loaded Immediately | Lazy Loaded |
|---|---|---|
| Initial page network requests | Includes comments widget | Excludes it entirely |
| Main thread work on page load | Comments script parses immediately | Deferred until scrolled near |
| Requests for a visitor who never scrolls down | Wasted, fetched anyway | Never fired at all |
That reduced initial page weight has a direct line to Total Blocking Time, since a heavy third party comments script parsing and executing during the initial load is exactly the kind of main thread work that metric penalizes.
Measuring this properly means comparing a real before and after, the same page with comments loaded immediately versus the same page after the lazy load comments section change, run through the same throttled network profile in DevTools. The difference tends to show up clearly in both the initial request waterfall and the main thread activity track.
web.dev's guidance on lazy loading and LCP makes the same case from the loading metrics side: deferring genuinely below the fold content, comments included, keeps the browser focused on what actually needs to render first. Google's own page experience documentation lists exactly this kind of deferred, below the fold loading as a legitimate technique for improving real user loading metrics.
The benefit scales with how much a page relies on discussion to keep visitors engaged. A high traffic publisher running a lazy load comments section fix across every article page removes a fixed chunk of third party weight from millions of page loads a month, without touching how comments actually look or behave once someone scrolls down to read them.
Most lazy load comments section bugs trace back to one of these five gaps, each one easy to catch with a quick manual test against a page with real content above the fold.
A useful check before shipping any lazy load comments section change is opening the Network tab, filtering to the comments request, reloading the page fresh, and confirming it stays empty until an actual scroll happens. That one manual pass catches most of the mistakes below before a visitor ever encounters them.
Most visitors never scroll far enough to see comments at all, so loading that content immediately wastes bandwidth and main thread time for a section that may never be seen.
No, it can fire multiple times as the intersection ratio crosses a threshold repeatedly, which is exactly why a load guard is needed to prevent a duplicate fetch.
Yes. Without it, the observer keeps watching the element and keeps firing on every scroll past its boundary, work that serves no purpose once the content has already loaded.
Yes, the same observer and guard logic can wrap loading that widget's script instead of a custom fetch call, deferring its entire weight until the section is actually approaching the viewport. A self hosted, open source alternative like giscus works the same way, and being open source, its own loading script can be inspected directly to see this pattern applied in a real production tool.
Close enough that comments feel ready by the time a visitor actually scrolls there, which the observer's rootMargin option controls by expanding its trigger boundary before the element is fully in view.
Google's crawler does render JavaScript and can trigger scroll based loading during rendering, but a comments section is rarely the primary content a page is trying to rank for, making this a low risk tradeoff in practice.
Real proof: 0 requests before scroll, 1 right after.
A guard function keeps the actual fetch to exactly one.
Proven above: 3 more scrolls, still only 1 total request.
An empty box reads as a bug without one.
Deferred script parsing means less upfront main thread work.
Wrap the widget script load in the same observer pattern.
This lazy load comments section pattern is a real, working starting point, an observer, a load guard, and a real API call, all proven with real captured output above.








