Lazy-Loading Disqus Comments

Lately, I've been obsessed with optimizing performance through lazy-loading. Recently, I've written on how to lazy-load Google Maps and on how to lazy-load responsive Google Adsense. Now it's time for Disqus, a service for embedding comments on your website. It's a great service. It eliminates the headache of developing your own local commenting system, dealing with spam, etc. Recently, I've been working on implementing the widget in one of my projects.

The Problem

Layout-wise, comments usually play a secondary role. In many cases, comments are never seen at all by the visitors, because the don't scroll down to where they are. But guess what? By default, they get loaded on the website no matter what. The wasted bandwidth is a problem.

Take a look at the technical implementation officially recommended by Disqus:

<div id="disqus_thread"></div>
<script>
  (function() {
    var d = document, s = d.createElement('script');
    s.src = '//username.disqus.com/embed.js';
    s.setAttribute('data-timestamp', +new Date());
    (d.head || d.body).appendChild(s);
  })();
</script>

Here's what they say: "Place the following code where you'd like Disqus to load". Say you're a good developer and you usually insert all of the <script src="..."></script> fragments right before the closing tag </body>. Then, one fine day, you decided to implement Disqus comments on your website and placed the above code somewhere in the middle of the document where the commenting section was meant to be.

What happens? The very first JavaScript file to start downloading is username.disqus.com/embed.js. That does not necessarily mean it will be downloaded first, but it's the first one in the line of JavaScript files that gets the browser's attention. Shouldn't the first be reserved for the main JavaScript file of your website? There are many things (like "sleeping" <button>'s, etc.) that could go wrong when your main JavaScript file is late to load, especially if you were not following the principles of graceful degradation or progressive enhancement back then when you developed that website.

This also interferes with other external resources on your website, like images and CSS files. Imagine yourself using a smartphone under 2G network conditions and waiting for the comments widget to load because you came for a kitten photo.

I did a test. Turns out the Disqus widget with zero comments weighs 2.49 MB! A bunch of networks requests for JavaScript, CSS, image, and font files that in many cases unreasonably slow down serving the other, perhaps critical parts or functions of your website.

Disqus Comments: No Lazyload

The Solution: Tiny JavaScript Plugin

In order to be able to lazy-load Disqus, I developed a tiny JavaScript plugin which does the job. No matter where the comments zone is, above or below the viewport, it won't get loaded if there isn't any reason to:

Disqus Comments: Lazyloaded

The plugin itself is a tiny piece of JavaScript. I made two versions of it: vanilla and jQuery. I called it disqusLoader. You can grab the files here:

Here's you you set it up. First, you need to insert an element into HTML where you want the comments section to be:

<div class="disqus"></div>

Then, initialize the plugin like this:

// vanilla
disqusLoader( '.disqus', { scriptUrl: '//username.disqus.com/embed.js' });

// jQuery
$.disqusLoader( '.disqus', { scriptUrl: '//username.disqus.com/embed.js' });

"That's great, but what about Disqus-specific config," you may ask. Sure, there's one more argument available which accepts a Disqus-native value. There are also a few more plugin-related options:

var options =
{
  scriptUrl: '//username.disqus.com/embed.js',
  /*
    @type: string (url)
    @default: none
    @required
    URL of Disqus' executive JS file. The value is memorized on the first function call
    and ignored otherwise because Disqus allows only one instance per page at the time.
  */

  laziness: 1,
  /*
    @type: int (>=0)
    @default: 1
    Sets the laziness of loading the widget: (viewport height) * laziness . For example:
    0 - widget load starts when at the least a tiny part of it gets in the viewport;
    1 - widget load starts when the distance between the widget zone and the viewport is no more than the height of the viewport;
    2 - 2x viewports, etc.
  */

  throttle: 250,
  /*
    @type: int (milliseconds)
    @default: 250
    Defines how often the plugin should make calculations during the
    processes such as resize of a browser's window or viewport scroll.
    250 = 4 times in a second.
  */

  /*
    @type: function
    @default: none
    Disqus-native options. Check Disqus' manual for more information.
  */
  disqusConfig: function()
  {
    this.page.title       = 'Page Title';
    this.page.url         = 'http://url.to/your-website';
    this.page.identifier  = 'unique-identifier';
  }
};

// vanilla
disqusLoader( '.disqus', options );

// jQuery
$.disqusLoader( '.disqus', options );

Check it out for yourself:

View Demo

You can also contribute or follow the project on GitHub.

Disqus Callbacks

Callbacks are great because you can react to user's actions. There's only one kind of callback officially documented by Disqus. Looking at the source code of `embed.js` file you can see more pre-defined types of callbacks:

Disqus Comments: Callbacks List In The Source Code Of embed.js

However, looks like only two of them are enabled: onNewComment and onReady. Which is enough for a tiny but noticeable improvement: a loading indicator.

Complementary Loading Indication

Loading the Disqus widget usually consists of two parts:

  1. Loading the `embed.js` file
  2. Loading the inner assets and performing other types of network requests

Disqus itself takes care of the 2nd step which they indicate with the animated image. But what about the 1st step? There are many reasons why loading external JavaScript files could take tens of seconds. The catch here is that no matter what the network conditions are, users will still be informed there's a commenting feature available on your site. User experience is in the details!

Disqus Comments: a Complementary Loading Indication

The technical approach is simple: a new HTML element and JavaScript callback function which helps to hide the element:

<div class="disqus-placeholder">Loading comments...</div>
<div class="disqus"></div>
// vanilla
disqusConfig: function()
{
  this.callbacks.onReady = [function()
  {
    var el = document.querySelector( '.disqus-placeholder' );
    if( el.classList )
      el.classList.add( 'is-hidden' ); // IE 10+
    else
      el.className += ' ' + 'is-hidden'; // IE 8-9
  }];
}

// jQuery
disqusConfig: function()
{
  this.callbacks.onReady = [function()
  {
    $( '.disqus-placeholder' ).addClass( 'is-hidden' );
  }];
}
.disqus-placeholder.is-hidden { display: none; }

You can see this in action on the demo page. But first, clean the browser's cache and throttle the network speed.

Multiple Instances Or Sites At Once?

While working on the technique, I discovered a couple of important limitations that exist today...

It's Impossible To Have Multiple Script Sources On a Single Page

Once the script file (e.g. //username.disqus.com/embed.js) loads, the global variable window.DISQUS is created, but only if it wasn't set previously (which is a bad sign, but let's dig deeper). So, I've done a test. I initialized the widget from the source script A. Then freed up some space for the future variable window.DISQUS = undefined and initialized the widget of the source B. However, the result was a mess: callback functions were fired multiple times, the comments got duplicated, etc. Obviously, the current codebase of Disqus is not designed to support multiple variables and to operate individually with each widget instances.

It's Impossible To Have Multiple Widgets On a Single Page At Once

There's a public JavaScript method function reset() available within the DISUQS object. If you've had any technical experience with Disqus, you may know that the widget is inserted into an element which has disqus_thread value for the id. I've done a test with two elements: loaded the widget within the first element, removed the ID attribute, and appended it to the second element. Finally, I called the reset function, expecting the second instance just to appear next to the first one. However, calling the function to load a new instance also destroys any previously initialized widgets. Unfortunately, today Disqus is designed only for a single instance at the given time.

It's Possible To Reload Widget In Real-Time

There's one good thing! Even though it is not possible to have multiple widget instances at once, you can still destroy the old ones and load in new ones. Let's turn this theory into practice with probably the most typical situation: tabs. All you need to do is call the plugin each time the new tab is activated:

<div class="tabcontent" data-disqus-id="venus" data-disqus-title="Venus"></div>
<div class="tabcontent" data-disqus-id="earth" data-disqus-title="Earth"></div>
<div class="tabcontent" data-disqus-id="mars" data-disqus-title="Mars"></div>
// call this function every time a tab is clicked:

var initDisqus = function( content )
{
  disqusLoader( content,
  {
    scriptUrl:    '//username.disqus.com/embed.js',
    disqusConfig: function()
    {
      this.page.identifier  = content.getAttribute( 'data-disqus-id' );
      this.page.title     = content.getAttribute( 'data-disqus-title' );
    }
  });
}

You can see this in action or view the full code on the demo page.

Closing Thoughts

This post is not about the flaws in Disqus. It's about the mistakes we developers make. Our world is full of tools and it is up to us how we use them. Even though these tools solve particular problems, they usually bring some others along if we don't take the appropriate care when implementing them. Every choice to take the easiest way turns into lost users, decreased conversions, and increased bouncing rates. You can already lazy-load Disqus, Google Adsense, Google Maps, social media buttons, you can also develop and share custom techniques. Be responsible!