Lazy-loading Disqus comments with IntersectionObserver

Reading the comments on a blog post can often be very interesting. You'll find people suggesting alternative methods or corrections, discussing their own experiences, or suggesting further topics for reading.

Disqus is one of the most popular comment platforms, with its massive userbase and ease of deployment into pretty much any environment or software platform. However, it's no secret that Disqus contributes heavily towards page weight, often times inflating pages by more than 50%. Victor Zhou has a great post about this on their blog here which I'd definitely recommend reading.

I didn't really want to replace Disqus though - I already had a lot of comments on my blog and didn't want to lose these, or the user context they were tied to. Many users already have a Disqus account after all, so I searched for another way to improve my page load times, even if just slightly.

Initial investigation

For initial testing, I used my blog post about updating your website favicon for dark mode.

As you can see from my initial testing, with Disqus enabled on my blog, the number of initial requests to load my post is almost three times higher, and I'm paying Disqus to remove ads from my embeds - with ads enabled this would be even higher! My page weight varies slightly, but I went from around 700KB, to 1.5MB - Disqus was larger than my entire blog post, including multiple images!

Lazy-loading using IntersectionObserver

I really wanted to get my initial page weight back down to the ~25 requests or so that made my blog super fast. If users read through the entire post and then wanted to leave a comment, Disqus should only be loaded at this time.

The Intersection Observer API seemed like a perfect use-case for this. From the MDN web docs:

The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.

This sounded fantastic - I could wait until the bottom of the post entered the viewport, and then lazy-load the comments as I needed. The IntersectionObserver API is generally well supported in all modern browsers, but I still wanted to provide a basic fallback for older browsers in the form of a "load comments" button. So I added the following to my Ghost template:

<div class="comment-wrap">
	<div id="post_id" data="{{comment_id}}"></div>
	<button class="btn btn-default" id="load_comments">Load comments</button>
	<div class="disqus-container">
		<div id="disqus_thread"></div>
		<script src="{{asset "/js/comment.js"}}"></script>
	    <noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
	</div>
</div>

With this, I planned out my JavaScript for what exactly I wanted to do:

  • Add a click handler to the load_comments button to load comments (if not already loaded)
  • Implement an IntersectionObserver to watch whenever the comment-wrap div entered the viewport, and trigger comments to load (if not already loaded)
  • Ensure that we auto-load the comments for any search engines using a cheap RegExp. There's a lot of debate whether you should let search engines index your comments, but that's a topic for another time.
  • And finally, maintain the Disqus functionality of being able to link to a specific comment using a comment fragment in the URL.
const disqus_identifier = document.getElementById("post_id").getAttribute("data");
const commentsButton = document.getElementById('load_comments');
let is_disqus_loaded = false;
function loadComments(){
	if(!is_disqus_loaded){
		is_disqus_loaded = true;
		const d = document, s = d.createElement('script');
		s.src = 'https://xxx.disqus.com/embed.js';
		(d.head || d.body).appendChild(s);
		commentsButton.classList.add('hidden');
	}
}
// add click handler to comments button to load comments, and emit event to GA
if(commentsButton){
	commentsButton.addEventListener('click', function(){
		if(ga && typeof(ga) === 'function'){
			ga('send', 'event', {
				eventCategory: 'Load Comments',
				eventAction: 'click',
				eventLabel: disqus_identifier
			});
		};
		loadComments();
	});
}
// load comments for search engines to index
if(/bot|google|baidu|bing|msn|duckduckgo|slurp|yandex/i.test(navigator.userAgent)){
	loadComments();
}
// load comments if URL hash contains #comment
if(location && location.hash && location.hash.includes('comment')){
	loadComments();
}
// load comments when comments enter viewport
if(!!window.IntersectionObserver){
	const commentBox = document.querySelector('.comment-wrap');

	const intersectionObserver = new IntersectionObserver(function(entries, observer){
		if(entries && entries[0] && entries[0].isIntersecting){
			loadComments();
			observer.unobserve(commentBox);
		}
	});
	intersectionObserver.observe(commentBox);
}

Here's a quick gif to show you what this looks like when you hit the bottom of my blog post. For demonstration, I've delayed the Intersection Observer from triggering until ~1s after entering the viewport. In production, this is pretty much instantaneous - scroll to the bottom of this post to experience it for yourself.

The Intersection Observer API is fantastic, and can be used for so many different things that would previously require hacky scroll handlers. I recently used it to replace an old scroll handler on my company's website for example, on our "The Difference" page, as well as implemented this same lazy-loading functionality in our company blog. I imagine I'll find many more use-cases over the next few months too.