
Based on an article on detection connection speed using JavaScript, I implemented a more completed speed detection solution which you can drop into your project with minimal fuss. An example would be to detect the user’s connection speed to show either high or low bandwidth version of the website. All the ingredients you need are an image, a small JavaScript snippet, and some imagination. There is no dependency and no other 3rd party library needed.
The main idea is that we load an image programmatically and measure the time it takes the browser to load such an file. Once an image is done loading, the browser will fire the onLoad event and we can capture the end time. I included two different versions of the code. The first one is quite simple with the image being loaded only once. The second one is more sophisticated, loading the image multiple times and taking the average loading time to arrive at a more accurate answer.
The key technique being used here are closure and passing functions as arguments. These two techniques are very commonly used in modern JavaScript frameworks such as Prototype, Scriptaculous, jQuery, Ext, YUI, etc.
In order for this technique to work, we need to have an image with an average size. Too small of an image won’t give us a very good result while too big of an image will cause more lags and affect the user’s experience. For your convenience, I included the a JPG image around 58.5KB in size as our testing image. We will need the exact size of the image in Bytes in order to calculate the bitrates or byte-rates.
Simple Javascript Speed Detection
Insert this code snippet into the page where you want to conduct the speed test
var SpeedTest = function() {
/*
From: http://techallica.com/kilo-bytes-per-second-vs-kilo-bits-per-second-kbps-vs-kbps/
256 kbps 31.3 KBps
384 kbps 46.9 KBps
512 kbps 62.5 KBps
768 kbps 93.8 KBps
1 mbps ~ 1000kbps 122.1 KBps
*/
};
SpeedTest.prototype = {
imgUrl: "speedtest.jpg" // Where the image is located at
,size: 59917 // bytes
,run: function( options ) {
if( options && options.onStart )
options.onStart();
var imgUrl = this.imgUrl + "?r=" + Math.random();
this.startTime = (new Date()).getTime() ;
var testImage = new Image();
var me = this;
testImage.onload = function() {
me.endTime = (new Date()).getTime();
me.runTime = me.endTime - me.startTime;
if( options && options.onEnd )
options.onEnd( me.getResults() );
};
testImage.src = imgUrl;
}
,getResults: function() {
if( !this.runTime )
return null;
return {
runTime: this.runTime
,Kbps: ( this.size * 8 / 1024 / ( this.runTime / 1000 ) )
,KBps: ( this.size / 1024 / ( this.runTime / 1000 ) )
};
}
}
First, we need to specify where we would load the image from (in this case, the relative path to the page) and the exact size of that image in Kilobytes. Since we don’t want the browser to cache the image, which short-circuits our test, we append a random number to the original image’s url (line 19). Before we run the test, we check to see if we need to run the onStart() functions being passed in. The idea is that you can show on the page with some messages or pop-up to notify about the test is about to run, simply by passing in an inline function.
The crux of the detection is from line 23 to line 30. If you are new to JavaScript or you are not comfortable with Closure, you should investigate a bit more since Closure is a critical and extremely powerful part of Object-Oriented JavaScript. In its simplest form of explanation, Closure means an anonymous function from a different execution context can gain access to another function’s internal properties.
In this case, after downloading the image, the browser fires the image’s onLoad event at a later time in a different execution context (the context of the current browser) other than context of our SpeedTest object. Without a closure reference via the variable “me” (line 23), which holds the reference to the SpeedTest object, we can no longer access to the startTime and endTime properties of the current SpeedTest object.
Now to run the test and obtain the results, all you have to do is to initialize a new SpeedTest object, execute the run() method, and pass in your custom actions. Currently 2 custom events onStart() and onEnd() are supported:
var st = new SpeedTest();
st.run({
onStart: function() {
alert('Before Running Speed Test');
}
,onEnd: function(speed) {
alert( 'Speed test complete: ' + speed.Kbps + ' Kbps');
// put your logic here
if( speed.Kbps < 200 )
{
alert('Your connection is too slow');
}
}
});
The Multi-trial speed test
As with any kind of measurements, one single measure is subjected to certain errors and inaccuracy. In our case, the degree of inaccuracy is much higher since connection speed fluctuates constantly. Other factors would affect the single-pass test is the initial DNS look-up speed and overhead, temporal network latency, slow browser, etc. Hence we can further improve our answers by taking multiple tests and average the results to get a more accurate number.
Below is a more sophisticated speed-detection code, which we can specify how many times the test is run. Typically 3 runs is good enough and it will not impact on the user’s experience.
var SpeedTest = function() {
/*
From: http://techallica.com/kilo-bytes-per-second-vs-kilo-bits-per-second-kbps-vs-kbps/
256 kbps 31.3 KBps
384 kbps 46.9 KBps
512 kbps 62.5 KBps
768 kbps 93.8 KBps
1 mbps ~ 1000kbps 122.1 KBps
*/
};
SpeedTest.prototype = {
runCount: 3 // how many times we want to run the test for
,imgUrl: "speedtest.jpg" // Where the image is located at
,size: 59917 // bytes
,run: function( options ) {
this.results = []; // reset the results
this.callback = ( options && options.onEnd ) ? options.onEnd : null;
this.runTrial(0, options);
}
,runTrial: function(i, options ) {
var imgUrl = this.imgUrl + "?r=" + Math.random();
var me = this;
var testImage = new Image();
testImage.onload = function() {
me.results[i].endTime = ( new Date() ).getTime();
me.results[i].runTime = me.results[i].endTime - me.results[i].startTime;
if ( i < me.runCount - 1 )
me.runTrial( i + 1 ); // run the next trial
else
{
// Execute the callback
if( me.callback )
me.callback( me.getResults() );
}
};
this.results[i] = { startTime: ( new Date() ).getTime() };
testImage.src = imgUrl;
}
,getResults: function() {
var totalRunTime = 0;
for( var i = 0; i < this.runCount; i++ )
{
if( !this.results || !this.results[i].endTime )
return null; // exit if we found no endTime. --> test’s not done yet
else
totalRunTime += this.results[i].runTime;
}
var avgRunTime = totalRunTime / this.runCount;
return {
avgRunTime: avgRunTime
,Kbps: ( this.size * 8 / 1024 / ( avgRunTime / 1000 ) )
,KBps: ( this.size / 1024 / ( avgRunTime / 1000 ) )
};
}
}
Even though our test method has changed, you can still use the same exact code as above to run the speed-test. The answer will be much more accurate this time. Basically we run multiple trials back-to-back, each time measuring the runtime and store it into an array, then average and get the final result.
Another small difference is that we have to store the reference to the user’s custom onEnd() method as a property of the SpeedTest object so that at the end of the test, we can execute it and return the answer to the user.
Final words
I hope you enjoy this article and make good use of the code. I packaged the 2 versions and the image into a zip file so just go ahead and have some fun. Go speed-racer!
Download

’ + msg;
Prompt.display( msg, options );
}
, _center: function(){
$(’prompt’).style.left = ( ( document.viewport.getWidth() - parseInt( $(’prompt’).offsetWidth ) ) / 2 ) + ‘px’ ;
}
,show: function(){
Prompt._center();
new Effect.Move ($(’prompt’),{ x: parseInt( $(’prompt’).style.left ), y: 0, mode: ‘absolute’, duration: 0.2 });
}
,hide: function( msg) {
new Effect.Move ($(’prompt’),{ x: parseInt( $(’prompt’).style.left ), y: -30, mode: ‘absolute’, duration: 0.1 });
}
}
I guess my relationship with the Lenovo 3000 N100 laptop is a complicated one: as much as I hated the Home/End/Page up/Page down combo madness, I truely missed that 2 extra buttons that my Dell D830 doesn’t have. Now instead of just flicking my finger to hit Home or End, I have to move my entire arm to reach for either End or Home. Even though now I won’t make any mistakes with Home/End jumping as with the Lenovo, I have to slow down the typing with the Dell…. NO! Only if Dell put the 2 extra home/end button next to their Arrow buttons… Why can’t we just live happily with each other? Is it really that hard to design a good working keyboard, or is it me that just being picky about laptop keyboard?