PDA

View Full Version : Javascript loading progress bar, forcing browser update, in PHP



davidhw
25 Nov 2008, 12:33 AM
My Ext.js based web application has two loading issues that I guess are fairly common in this community:
1. Long load times for JS files (around 700kb to load), users getting bored and not waiting...
2. Users' browsers not updating recent javascript and CSS, but using cached old versions - creating errors

This PHP function (below) helps to load Javascript files while showing a smooth-moving progress bar.
.js files are timestamped, based on their actual file modified time, so that the users' browser will always load the most recent file.

You can see it in action at www.oothanks.com

(http://www.oothanks.com)NB: In my set up I have divided my ext.js file (a full build, including prototype loader) into several (11) sections - simply by opening it in a textfile editor, and copying a page of lines at a time into new js files - named ext-d1.js to ext-d11.js. They load in order. I did this for a previous non-smooth version of this loader, and I still do it with this loader. I doubt that the rate calculation that smooths the progress bar would work well with a very large JS file. See the example below...

A function is also provided to load CSS files with timestamps.
Timestamps are hashed, because I felt like it.

Here's the PHP:


<?php
// JS and CSS with Timestamp #################################################
function show_stamped_JS($file) {
$t = filemtime($_SERVER['DOCUMENT_ROOT'] . preg_replace("/^(.*)\.js\??(.*)$/", '$1.js', $file));
$file = $file . ((strpos($file, ".js?") === false) ? "?" : "&");
echo "<script src='{$file}v=" . md5(date("YmdHis", $t)) . "' type='text/javascript' language='JavaScript'></script>";
}
function show_stamped_CSS($file) {
$t = filemtime($_SERVER['DOCUMENT_ROOT'] . $file);
echo "<link rel='stylesheet' type='text/css' href='$file?v=" . md5(date("YmdHis", $t)) . "'></link>";
}
// JSLoader ##################################################################
function JSLoader($ar, $div) {
// $ar is an array of JS files to be loaded, JS Loader reports progress with a progress bar etc
// $div the id of the div where the progress will be reported
if (!defined('JSLOADER_JS_SET')) {
define('JSLOADER_JS_SET',true);
// load the JSloader javascript
?>
<!-- JSLoader Script -->
<script language='JavaScript' type='text/javascript'>
var JSLoader = function(){
// namespaced private variables & functions
var JSL = {
totalSize:0, sizeSoFar:0, currentTotal:0, div:"",
startTime:"", rate:0, timerOn:false, intrval:100,
drawBox: function(){
JSL.div.innerHTML = "<div class='JSLoader-barholder'>"
+ "<div class='JSLoader-bar' id='JSLoaderBar' style='width:0%;'>"
+ "&nbsp;Loading:&nbsp;<span id='JSLoaderText'>0</span>%"
+ "</div>"
+ "</div>";
JSL.bar = document.getElementById('JSLoaderBar');
JSL.barText = document.getElementById('JSLoaderText');
},
update: function(fr) {
// shows the box to fraction fr (of 1)
if (JSL.bar === undefined) {
JSL.drawBox();
}
pc = Math.floor(fr * 100);
JSL.bar.style.width = pc + "%";
JSL.barText.innerHTML = pc;
},
timeUpdate: function(){
if (JSL.timerOn) {
JSL.currentTotal = JSL.currentTotal + JSL.rate;
if (JSL.currentTotal <= JSL.totalSize) {
JSL.update(JSL.currentTotal / JSL.totalSize);
setTimeout(JSL.timeUpdate, JSL.intrval);
}
}
}
};
return {
init: function(tS, divId){
// sets up everything
JSL.totalSize = tS;
JSL.div = document.getElementById(divId);
JSL.startTime = new Date();
JSL.rate = 1000;
JSL.timerOn = false;
JSL.currentTotal = 0; // the virtual amount of bytes
JSL.sizeSoFar = 0; // the actual amount of bytes
},
startFile: function(fName, fSize){
// call this before a file loads, after the previous file has finished
if (JSL.sizeSoFar == 0) {
JSL.update(0);
JSL.timerOn = true;
setTimeout(JSL.timeUpdate, JSL.intrval);
} else {
// timer is on,
// recalculate the 'rate' - ie the increment each intrval
var t = new Date();
var currRate = (JSL.sizeSoFar == 0) ? 0 : JSL.sizeSoFar / (t.getTime() - JSL.startTime.getTime());
var timeRemaining = (JSL.totalSize - JSL.sizeSoFar) / currRate;
if (timeRemaining != 0) {
JSL.rate = JSL.intrval * ((JSL.totalSize - JSL.currentTotal) / timeRemaining);
}
}
JSL.sizeSoFar += fSize;
},
complete: function(){
// call this to end it all!
JSL.timerOn = false;
JSL.update(1);
}
}
}();
</script>
<?php
} // eof first run JS

// calculate total file size
// changing file names to get rid of any ?thing=wotsit
$t = 0;
$fName = "";
foreach ($ar as $f) {
$f1 = preg_replace("/^(.*)\.js\??(.*)$/", '$1.js', $f);
$fName[$f] = $f1;
$t = $t + filesize($_SERVER['DOCUMENT_ROOT'] . $f1);
}
// call the start function
echo "<script type='text/javascript' language='JavaScript'>"
. "JSLoader.init('$t','$div',$width);"
. "</script>";
// load each file, and report in between
foreach ($ar as $f) {
// report
echo "<script type='text/javascript' language='JavaScript'>"
. "JSLoader.startFile('$f'," . filesize($_SERVER['DOCUMENT_ROOT'] . $fName[$f]) . ");"
. "</script>";
show_stamped_JS($f);
}
// complete it
echo "<script type='text/javascript' language='JavaScript'>"
. "JSLoader.complete();"
. "</script>";

}
?>
You might need to alter the path to the files - I've used $_SERVER['DOCUMENT_ROOT'] in this example, I actually use a 'defined' DOC_ROOT constant in my setup.


It needs some STYLE:


.JSLoader-barholder {
border: 1px solid blue;
background: #a50000 url(/images/listitem_red1_center.png) repeat-x;
height: 20px;
width: 100%;
}
.JSLoader-bar {
border: none;
background: green url(/images/listitem_green1_center.png) repeat-x;
height: 100%;
line-height:20px;
}
Obviously replace my images with your own, or just use colours.

to call the function, you need an array of js files, and a div with an id in which to load the progress bar. You CAN have scriptaculous?load=effects in there (it slightly messes up the rate calculation, but with lots of other JS files loading, that gets evened out).

Here's my example of calling the function, CALL THIS IN THE BODY of the document, not the HEAD, while your loading screen is showing:


<?php
// organise the loading of JS scripts...
$JSLoaderAr = array(
'/lib/prototype/prototype.js',
'/lib/scriptaculous/scriptaculous.js?load=effects'
);
for ($n = 1; $n < 12; $n++) {
$JSLoaderAr[] = "/lib/ext2/ext-divide/ext-d$n.js";
}
$JSLoaderAr[] = '/lib/functions.js';
$JSLoaderAr[] = '/lib/fields.js';
$JSLoaderAr[] = '/lib/main.js';
JSLoader($JSLoaderAr, "loadingSpace");
?>

And, for the record, here's how I call the CSS function in the head section of the html document:



<?php show_stamped_CSS("/lib/ext2/resources/css/ext-all.css"); ?>
It all works in FF3 and IE7, don't know about others - sorry!

Anyway - hope this works for you, and you find it helpful.

Datagenn
24 Aug 2009, 1:42 AM
Hi David,

This is a really nice loader, well done on an excellent job.

You're right about the loader not displaying incremental updates on larger files so a bundle of smaller files is needed and works quite nicely.

One question I have is this: do you have a css fix for the % character display in Chrome, for some reason Chrome displays the % character on the next line rather than the current line.

Another thing I see is "0%" displayed when everything is cached but the browser is rendering the application. It seems to work ok on your oothanks domain so perhaps you have updated your PHP?

Finally, there is a small bug in the code provided in the post.. the 'width' variable is missing and needs to be added to the function signature and to the function call as well.

All the best,
Sean

Animal
24 Aug 2009, 2:46 AM
It's not good practice to have so many separate Javascript files though. They should be concatenated into one file which should then be minified and served gzipped.

steffenk
24 Aug 2009, 2:49 AM
i remember a tool for Eclipse/Aptana doing this job automatic, who knows which one it was?

hm - i should know how to search :D

Rockstarapps: http://www.rockstarapps.com/joomla-1.5.8/home.html

davidhw
24 Aug 2009, 5:46 AM
It's not good practice to have so many separate Javascript files though. They should be concatenated into one file which should then be minified and served gzipped.
That's a great theory, but the problem is you can still end up with a very large JS file, and your end users are sitting there going - Hmm, what's going on... Hmm, I can't be bothered to wait while this miscellaneous "loading" graphic just goes round and round... Hmm, I'll go somewhere else.

^^^ Datagen, I'll have a look....

Animal
24 Aug 2009, 5:53 AM
Your Yslow report:

http://i131.photobucket.com/albums/p286/TimeTrialAnimal/yslowreport.jpg

Of course your users are going to get bored.

You neither minify nor gzip, but waste time farting around creating multiple HTTP requests which actually slow down the load time of your page!

davidhw
24 Aug 2009, 5:56 AM
Your Yslow report:



Of course your users are going to get bored.

You neither minify nor gzip, but waste time farting around creating multiple HTTP requests which actually slow down the load time of your page!

Sorry, I missed the place where I asked people to be rude to me on this forum?

VinylFox
24 Aug 2009, 6:02 AM
Sorry, I missed the place where I asked people to be rude to me on this forum?

Animal makes quite a valid point, and you would be smart to take his advice. I actually thought he was just being direct with you, not rude at all.


You neither minify nor gzip, but waste time farting around creating multiple HTTP requests which actually slow down the load time of your page!

davidhw
24 Aug 2009, 6:16 AM
Animal makes quite a valid point, and you would be smart to take his advice. I actually thought he was just being direct with you, not rude at all.

Ok, thanks. I'll consider his advice.

I didn't appreciate his direct approach, but there you go, I suppose that's a matter of opinion.

Animal
24 Aug 2009, 6:33 AM
I assume you're from across the pond then?

You prefer working with steel bladed, earth entrenching horticultural implements?

smit_al
24 Aug 2009, 1:37 PM
I assume you're from across the pond then?

You prefer working with steel bladed, earth entrenching horticultural implements?


Damn, Animal. You crack me up! I'm still giggling.

davidhw
27 Aug 2009, 12:14 AM
Hi David,

This is a really nice loader, well done on an excellent job.

You're right about the loader not displaying incremental updates on larger files so a bundle of smaller files is needed and works quite nicely.

Subject to the advice about concatenating, minifying, and gzipping above... thank you!

Here are some attempts at answers to your issues



One question I have is this: do you have a css fix for the % character display in Chrome, for some reason Chrome displays the % character on the next line rather than the current line.

You could try including the % sign within the span with id=JSloaderText, instead of outside it,
This line:

+ "&nbsp;Loading:&nbsp;<span id='JSLoaderText'>0</span>%"
changes to

+ "&nbsp;Loading:&nbsp;<span id='JSLoaderText'>0%</span>"
and this line

JSL.barText.innerHTML = pc;to

JSL.barText.innerHTML = pc + "%";

Another thing I see is "0%" displayed when everything is cached but the browser is rendering the application. It seems to work ok on your oothanks domain so perhaps you have updated your PHP?

I think what happens is that I destroy the bar in my onload code, so that it hits 100% and then vanishes pretty soon after that.



Finally, there is a small bug in the code provided in the post.. the 'width' variable is missing and needs to be added to the function signature and to the function call as well.

You're absolutely right.

function JSLoader($ar, $div) {becomes

function JSLoader($ar, $div, $width) {and

JSLoader($JSLoaderAr, "loadingSpace");becomes

JSLoader($JSLoaderAr, "loadingSpace", 325);

I've got an updated version running on another site which, surprisingly, also includes a minifying routine. I'll get that up below, or message it to you.

D.

iamleppert
28 Aug 2009, 4:29 PM
Your Yslow report:

http://i131.photobucket.com/albums/p286/TimeTrialAnimal/yslowreport.jpg

Of course your users are going to get bored.

You neither minify nor gzip, but waste time farting around creating multiple HTTP requests which actually slow down the load time of your page!

What about in the case where you have a very large Ext application? Should you just serve the entire thing in a giant javascript file? Or should you break it up? When to know?

davidhw
28 Aug 2009, 11:15 PM
What about in the case where you have a very large Ext application? Should you just serve the entire thing in a giant javascript file? Or should you break it up? When to know?

Why not try out both ways, see how the site loads, and go with whichever seems best to you?

The progress bar that this thread is about needs more than one JS file to work - it has to calculate some kind of load rate. That breaks one of the YSlow rules - but: it might be that the difference in load time is minimal (despite breaking the rule), but by breaking up into a few JS files and using a progress bar your users get a better idea of what's going on.

I've been working on my sites, minifying and gzipping as advised (humble pie duly eaten), and the results have been pretty impressive: for example, my slow-loading ext.js file of 430kb has become just 130kb, and now my loading progress bar barely has time to show itself before the site has loaded.

Having said that, I've still got 8 JS files loading, and YSlow gives me a C on "Make fewer HTTP requests". I like my loading progress bar, having spent so much time "farting around" on it, so instead of joining up my JS files, I'm keeping them separate so that the bar can calculate timing info.

I'll post my minifying/ gzipping JSLoader soon. Alternatively, ignore my progress bar, and use Google's minify http://code.google.com/p/minify/