Sonar – Detecting When Objects are in View, and Doing Something

by David Artz on November 21st, 2008

Recent research by Clicktales shows that a range of 15% to 20% of page views actually reach the very bottom of the page and 32% to 36% of page views are unlikely to reach the 1000 pixel line. Yet we still serve up this unseen content and spend bandwidth on functionality, images, and rich media that users may never even get to in their journey.

The Sonar library was developed to test objects to see if they are visible on screen so we can make decisions on whether or not we should load them in. Sonar can also be used in cases where you may want to only perform certain actions (such as an ad refresh) if the user is actually viewing the object.

Introducing Sonar Detection

At the core of Sonar is its ability to detect if an object is visible on the user's screen. The detect() method is used to do this, and returns true or false if the element is within a certain threshold from the edge of the screen. For example:

var detected = sonar.detect(document.getElementById("myobject"), 400);

In the above code, the detected variable will be set to true if the "myobject" node is within 400 pixels from the edge of the screen, or false if not.

This can be useful for only doing certain events when an object is visible on the screen, for example refreshing advertisements. You would not want to refresh advertisements invisible to the user, and Sonar detection can be used to do this.

Polling Objects with Sonar Detection

Sonar comes with a built in polling feature that polls the objects with sonar.detect() as the user scrolls. Once an object is detected, it executes a callback function that you can define. Think of it as a way to turn Sonar detection into an event you can hook on to any object.

To poll objects with Sonar, do the following:

sonar.add(
{
	obj: document.getElementById("myobject-div"),
	call: function(object)
	{
		// example code to embed a flash player
		swfobject.embedSWF("player.swf", "myobject-div", "640", "500", "9.0.0",
		{}, {file: "my-flash-movie.flv", fullscreen: true},
		{allowfullscreen: true, allowscriptaccess: "always"});
	},
	px: 400
});

In the above example, a Flash player will be injected into the "myobject-div" when the user scrolls to it. Let's examine the construct a little further.

Worth noting is that the callback function is passed back the object in the argument, making it easy to reference the object that triggered the detection. An example of this is coming up next.

Once all objects have been detected, Sonar politely removes itself from the onScroll event, and no longer polls for them.

Extending the Sonar Library

By now your mind is probably racing with all the cool things you can do with this simple library. One of the things we have wanted to do for a while is figure out a way to only load images when they are needed. A case study follows.

Let's pick on a popular blog that provides amazing live-blogging coverage of MacWorld and other conferences. A run of Pagetest shows us the extent of the damage:

Engadget MacWorld 2008 Pagetest Results

Wouldn't it be great if we could stop the 50 images below the fold from loading, and load them when Sonar detects them? What would that look like?

"Ionized" Engadget MacWorld 2008 Pagetest Results

Saving 50 requests and 4.4 MB of bytes shaved over 25 seconds off this live blog's load time!

The Sonar Ion (Images on Need) Extension

Ion is a simple function that does the following:

All that is required to get this going is including the following line of code:

sonar.ion(1000);

That's it! The argument is the distance off the edge of the screen we want to consider the object within range of detection.

If you are interested in the guts of how this works, take a look at the code below. Pay special attention to the call property function, which is triggered once the image is within range of sonar.detect().

sonar.ion = function(distance)
{
	// Grab all the images on the page.
	var	default_distance = 0, // Default distance.
		images = document.getElementsByTagName('img'),
		transparent_pixel = "http://www.artzstudio.com/i/x.gif", // Needed for Firefox.
		i;

	distance = distance || default_distance;

	// Loop through all the images.
	//
	for (i in images)
	{
		// If the image is not on the screen within the distance...
		if (!sonar.detect(images[i], distance))
		{
			// Temporarily store the true source as a temporary
			// property (__src) of this object.
			images[i].__src = images[i].src;

			// Replace the source with a blank image (Firefox) or a
			// blank string (IE). No solution for Safari, as it
			// downloads all images regardless of what we do.
			images[i].src = (document.all) ? "" : transparent_pixel;

			// Add the image to the sonar object, where it will then
			// be polled as the user scrolls and execute the callback
			// function once it is within range.
			sonar.add({
				obj : images[i],
				px : distance,
				call : function(object)
				{
					// Set the blank image source to the true value.
					object.src = object.__src;

					// Set the temporary property to undefined so
					// the sonar.detect() function is no longer run on it.
					object.__src = undefined;
				}
			});
		}
	}
};

Ion works great in IE and Firefox, but in Safari no matter what we tried we could not stop the download of the images. We even tried ripping them out of the DOM - no dice. Don't let this stop you from giving it a shot yourself, (and telling me how you did it)!

Conclusion & Final Thoughts

Sonar helps us bridge the gap in loading anything - content, JavaScript, rich media, images - only when needed by users.

Think of combining Sonar with ideas like loading script elements or stylesheets into the document as needed.

Leave a comment with your thoughts, or ideas for the library!