views:

283

answers:

7

I'm wondering which of the following is going to result in better performance for a page which loads a large amount of javascript (jQuery + jQuery UI + various other javascript files). I have gone through most of the YSlow and Google Page Speed stuff, but am left wondering about a particular detail.

A key thing for me here is that the site I'm working on is not on the public net; it's a business to business platform where almost all users are repeat visitors (and therefore with caches of the data, which is something that YSlow assumes will not be the case for a large number of visitors).

First up, the standard approach recommended by tools such as YSlow is to concatenate it, compress it, and serve it up in a single file loaded at the end of your page. This approach sounds reasonably effective, but I think that a key part of the reasoning here is to improve performance for users without cached data.

The system I currently have is something like this

  • All javascript files are compressed and loaded at the bottom of the page
  • All javascript files have far future cache expiration dates, so will remain (for most users) in the cache for a long time
  • Pages only load the javascript files that they require, rather than loading one monolithic file, most of which will not be required

Now, my understanding is that, if the cache expiration date for a javascript file has not been reached, then the cached version is used immediately; there is no HTTP request sent at to the server at all. If this is correct, I would assume that having multiple tags is not causing any performance penalty, as I'm still not having any additional requests on most pages (recalling from above that almost all users have populated caches).

In addition to this, not loading the JS means that the browser doesn't have to interpret or execute all this additional code which it isn't going to need; as a B2B application, most of our users are unfortunately stuck with IE6 and its painfully slow JS engine.

Another benefit is that, when code changes, only the affected files need to be fetched again, rather than the whole set (granted, it would only need to be fetched once, so this is not so much of a benefit).

I'm also looking at using LabJS to allow for parallel loading of the JS when it's not cached.

Specific questions

  • If there are many tags, but all files are being loaded from the local cache, and less javascript is being loaded overall, is this going to be faster than one tag which is also being loaded from the cache, but contains all the javascript needed anywhere on the site, rather than an appropriate subset?
  • Are there any other reasons to prefer one over the other?
  • Does similar thinking apply to CSS? (I'm currently using a much more monolithic approach to CSS)
+2  A: 

I would definitely go with the non-monolithic approach. Not only in your case, but in general gives you more flexibility when you need something changed or re-configured.

If you make a change to one of these files then you will have to merge-compress and deliver. If you are doing this in an automated way then you are OK.

As far as the browser question "if the cache expiration date for a javascript file has not been reached, then the cached version is used immediately; there is no HTTP request sent at to the server at all", i think that there is an HTTP request made but the with response "NOT MODIFIED". To be sure you should check all the Requests made to the Web Server (using one of the tools available). After the response is given then the browser uses the unmodified resource - the js file or image or other.

Good luck with your B2B.

andreas
If everything is set up correctly, no HTTP request will be made.
Marcel Korpel
Thanks for the tip Marcel
andreas
Thanks Marcel; that is my understanding as well.
El Yobo
Here's an explanation of how local caching and 304s differ: http://stackoverflow.com/questions/1665082/http-status-code-200-cache-vs-status-code-304/1665097#1665097
Colonel Sponsz
here are some gotchas when testing this in firefox http://www-jo.se/f.pfleger/firefox-reload
Josef
+1  A: 

Did you try Google Closure? From what I've read about it it seems quite promissing.

MartyIX
Closure is indeed useful, but doesn't address my question about the comparative benefits of compressing vs loading only required files if the files are cached anyway.
El Yobo
I thought that maybe the result of GC will answer your question.
MartyIX
No, it still doesn't really answer my core question as to which is going to be better performance wise, as it only reduces the files sizes. This is faster when dealing with an empty cache, but my specific area of interest is when the cache is in use.
El Yobo
A: 

Generally it's better to have fewer, larger requests than to have many small requests, since the browser will only do two (?) requests in parallel to a particular domain.

So whilst you say that most users are repeat visitors, when the cache expires there will be many round-trips for the many files, rather than one for a monolithic file.

If you take this to an extreme and have potentially thousands of files with individual functions in them, it would become obvious that this would lead to a huge number of requests when the cache expires.

Another reason to have a monolithic file is for when various parts of the site have different chunks of javascript associated with them, as you again get this in the cache when you hit the first page, saving later requests and round-trips.

If you're worried about the initial hit loading a "large" javascript file you can try loading it asynchronously, using the method described here : http://www.webmaster-source.com/2010/06/07/loading-javascript-asynchronously/

Whichever way you go in the end, remember that since you're setting a far-future modified date, you'll need to change the name of the javascript (and CSS) files when changes are made in them, otherwise clients won't pick up the changes until their cache expires anyway.

PS : Profile it on the different browsers with the differing methods and write it up, as it will prove useful to those who are also stuck on slow JS engines like IE6 :)

Legooolas
Because we do set a far future date and rename files when they change (as you've pointed out) the cache for any given file expires extremely infrequently; we roll out updates monthly and most files are not changed, even then, so still don't expire. The thing that nobody has yet addressed is whether there is a performance hit (especially in slow browsers like IE6) for actually intepreting that monolithic file on every page load, rather than interpeting a much smaller set of javascript (albeit in multiple files).
El Yobo
+1  A: 

Even though you are dealing with repeat-visitors, there are many reasons why their cache may have been cleared, including privacy and performance tools that delete temporary cache files to "speed up your computer".

Merging and mini-fying your script doesn't have to be an onerous process. I write my JavaScript in separate files, nicely spaced out to be readable to me so it is easier to maintain. However, I serve it via a script page that combines all of the scripts into a single script and mini-fies it all - so one script gets sent to the browser with all my scripts in. This is the best of both worlds as I work on a collection of JavaScript files that are all readable, and the visitor gets one compressed JavaScript file, which is the recommendation for reducing the HTTP requests (and therefore the queue time).

Sohnee
A: 

I've used the following for both CSS and Javascript -- most of my pages in Google Speed report being 94-96/100 and they load very fast (always within a second, even if there are 100kb's of Javascript).

1. I have a PHP function to call files -- this is a class and stores all the unique files that are asked for. My call looks something like:

javascript( 'jquery', 'jquery.ui', 'home-page' );

2. I spit out a url-encoded version of these strings combined together to call a dynamic PHP page:

<script type="text/javascript" src="/js/?files=eNptkFsSgjAMRffCP4zlTVmDi4iQkVwibbEUHzju3UYEHMffc5r05gJnEX8IvisHnnHPQN9cMHZeKThzJOVeex7R3AmEDhQLCEZBLHLMLVhgpaXUikRMXCJbhdTjgNcG59UJyWSVPSh_0lqSSp0KN6XNEZSYwAqt_KoBY-lRRvNblBZrYeHQYdAOpHPS-VeoTpteVFwnNGSLX6ss3uwe1fi-mopg8aqt7P0LzIWwz-T_UCycC2sQavrp-QIsrnKh"></script>

3. That dynamic PHP page takes decodes the string and creates an array of the files that will needed to be called. A cache_file path is created:

$compressed_js_file_path = $_SERVER['DOCUMENT_ROOT'] . '/cache/js/' . md5( implode( '|', $js_files ) ) . '.js';

4. It checks to see if that file path already exists in the cache, if so, it just reads the file:

if( file_exists( $compressed_js_file_path ) ) {
    echo file_get_contents( $compressed_js_file_path );
} else {

5. If it doesn't exist, it compresses all the javascript into one "monolith" file, but realize it has ONLY the necessary javascript for that page, not for the entire site.

if( $fh = @fopen( $compressed_js_file_path, 'w' ) ) {
    fwrite( $fh, $js );
    fclose( $fh );
}

// Echo the compressed Javascript
echo $js;

I've given you excerpts of the code. The program you use to compress javascript is completely up to you. I use this with both CSS and Javascript so that all those file requires 1 HTTP request, ever, the result is cached on the server (simply delete that file if you change something), and it has only the necessary Javascript & CSS for that page.

Kerry
This reduces amount of HTTP requests on load, however completely gets rid of any kind of caching for the component files across the site.
Dmitri Farkov
But it is cached server-side and gets called much faster. It can be the same call on multiple pages, and so that will still be called. From personal experience it has been seen to work MUCH faster.
Kerry
Even so, the time the page needs to get back to the client clearly suppresses the whole point of caching. The number 1 on your list is smart and it would work great by having number 2 call the files one by one.
Frankie
I disagree -- both from study and from experience. The pages I use load much faster than others, it reduced the HTTP requests drastically (you don't have to send 8 separate requests), the files ARE cached both server-side and there is no reason the browser won't cache them either.
Kerry
Yeah, as long as Kerry's PHP code spits out far-future cache headers and expirations, his solution is great up to a certain amount of code. Once you start getting over 100 Kb of javascript, splitting those combined files into multiple requests is the way to go.
Jordan
Lets say Kerry has 3 JS files (A, B and C). Using his system there can be 6 possible combinations for the user to cache (A, AB, ABC, B, BC, C). Were him to serve the files separate the worst case scenario would have been 3 files. Go monolithic or go DFN. Don't do this.
Frankie
That's silly to dismiss because it has potential to be bad. I don't necessarily agree with it being set up page-specifically like Kerry has it, but it's a great solution for mostly-monolithic JS when you want to load additional scripts on certain sections or pages.I would set it up so that it's mostly ABC, and an occasional DE that isn't necessary on every page. This gives you the ability to do mostly monolithic and DFN on edge cases.
Jordan
@ Frankie -- then don't include them that way. You choose what you include or don't include. But from my experience and my sites, each page has vastly different functions from other ones, and probably on average 100-300 lines of jquery custom to that page.
Kerry
@Kerry I understand how your system works and I agree with it. I think it's very wise to pre-cache the Javascript. I would even set it up on deployment and run it against Closure Compiler for efficiency. The only thing I'm disagreeing with is having more monolithic files than you actually need. Say you need script A on the first page and script AB on the second. You'll be serving the user a larger javascript base than needed. On the first call script A will be served. On the second call both A and B will be served when only B needed to be served as A was already in cache.
Frankie
@Jordan could not agree more. Going DFN doesn't mean you have to serve only A or B or C. Means exactly what you said. ABC go here, DEF go here and here.
Frankie
Sure, and Kerry's example is essentially a homegrown solution to DFN. I'm not sure why you advocate DFN in your answer yet say Kerry's solution is inherently wrong. Is my comparison between what they're doing incorrect?
Jordan
@Jordan all I'm trying is to suck a little less! ;) Let's see if I can explain why I think his solution is flawed. IMO Kerrys solution is a hybrid. Having the advantages and disadvantages of both. By having all files compressed (ABC) and sent to the client his system works as a monolith. However every single extra file he needs, E or F will force the user to pull again ABC (if that's the case). Like Dmitri put it in the first comment the user would already have ABC in cache and that JS could start running right away. Instead the request will be larger than needed and there will be no cache...
Frankie
@Frankie, I don't think so. Check out how he has the hash of the required files. That would of course be different for ABC vs. just EF or DEF.The problem is if you start making requests like ABC, DE, DEF, and EF all on the same page, rather than just ABCDEF or ABC and DEF.With his system, you don't have to pull ABC again because it would be cached in the browser (and on the server too, so it would be shared across concurrent users).
Jordan
@Jordan its pretty obvious I may have misinterpreted Kerry. Were the system to work like you say, I would agree but I was leaned to believe (maybe wrongly) that he always compressed all needed JS into a file. Say page 1 has AB, page 2 has ABC. Kerry's system would serve AB compressed on page 1 and ABC compressed on page 2. I would serve AB compressed and C compressed. As AB were already on cache only C would be transfered.
Frankie
I think we're on the same page now on the ideal way that it should be implemented, regardless of whether or not Kerry's particular implementation handles it exactly that way (it appears to). Good discussion all around either way.
Jordan
Wow, you guys have made a very good discussion. I do think Frankie has correctly interpreted me, but I'm not entirely sure myself. Right now the only way your browser will cache the js files for a page itself are if 2 pages on the site contain the exact same js files (page 1 includes ABC and page 2 includes ABC), because the encryption is based on the name of the files themselves, they are different. It seems that you both have an idea of what it should be and agree on that, could you help me understand and then perhaps I will make that correction?
Kerry
It does cache everything server-side for all users, technically speaking you are always pulling a server-side cached file with all the js necessary, unless you *happen* to be the first user to ever look at that page.
Kerry
@Frankie,@Jordan: interesting discussion. I don't see any way around the problem that Frankie has pointed out (where AB on page 1 + ABC on page 2 transfers two large files instead of using the cached AB on the second request), as it's impossible to reliably know that AB is already in the client's cache. That said, I still like @Kerry's approach and have implemented something very similar, both for CSS and JS; given that most of my users will be hanging around a while, for 99% of the time they will still be using the cache.
El Yobo
+2  A: 

I really think you need to do some measurement to figure out if one solution is better than the other. You can use JavaScript and log data to get a clear idea of what your users are seeing.

First, analyze your logs to see if your cache rate is really as good as you would expect for your userbase. For example, if each html page includes jquery.js, look over the logs for a day--how many requests were there for html pages? How many for jquery.js? If the cache rate is good, you should see far fewer requests for jquery.js than for html pages. You probably want to do this for a day right after an update, and also a day a few weeks after an update, to see how that affects the cache rate.

Next, add some simple measurements to your page in JavaScript. You said the script tags are at the bottom, so I assume it looks something like this?

<html>
<!-- all your HTML content... -->
<script src="jquery.js"></script>
<script src="jquery-ui.js"></script>
<script src="mycode.js"></script>

In that case, you time how long it takes to load the JS, and ping the server like this:

<html>
<!-- all your HTML content... -->

<script>var startTime = new Date().getTime();</script>

<script src="jquery.js"></script>
<script src="jquery-ui.js"></script>
<script src="mycode.js"></script>

<script>
var endTime = new Date().getTime();
var totalTime = endTime - startTime; // In milliseconds
new Image().src = "/time_tracker?script_load=" + totalTime;
</script>

Then you can look through the logs for /time_tracker (or whatever you want to call it) and see how long it's taking people to load the scripts.

If your cache rate isn't good, and/or you're dissatisfied with how long it takes to load the scripts, then try moving all the scripts to a concatenated/minified file on one of your pages, and measure how long that takes to load in the same way. If the results look promising, do the rest of the site.

Annie
+3  A: 

I would say that the most important thing to focus on is the perception of speed.

First thing to take into consideration, there is no win-win formula out there but a threshold where a javascript file grows into such a size that it could (and should) be split.

GWT uses this and they call it DFN (Dead-for-now) code. There isn't much magic here. You just have to manually define when you'll need a need a new piece of code and, should the user need it, just call that file.

How, when, where will you need it?
Benchmark. Chrome has a great benchmarking tool. Use it extensivelly. See if having just a small javascript file will greatly improve the loading of that particular page. If it does by all means do start DFN'ing your code.

Apart from that it's all about the perception.

Dont let the content jump!
If your page has images set up their width's and heights up front. As the page will load with the elements positioned right where their are supposed to be, there will be no content fitting and adjusting the users perception of speed will increase.

Defer javascript!
All major libraries can wait for page load before executing javascript. Use it. jQuery's goes like this $(document).ready(function(){ ... }. It doesn't wait for parsing the code but makes the parsed code fire exactly when it should. After page load, before image load.

Important things to take into consideration:

  1. Make sure js files are cached by the client (all the other stand short compared to this one).
  2. Compile your code with Closure Compiler
  3. Deflate your code, its faster than Gziping it (on both ends)

Apache example of caching

// Set up caching on media files for 1 month
<FilesMatch "\.(gif|jpg|jpeg|png|swf|js|css)$">
    ExpiresDefault A2629744
    Header append Cache-Control "public, proxy-revalidate"
    Header append Vary "Accept-Encoding: *"
</FilesMatch>

Apache example of deflating

// compresses all files for faster transfer
LoadModule deflate_module modules/mod_deflate.so
AddOutputFilterByType DEFLATE text/html text/plain text/xml font/opentype font/truetype font/woff
<FilesMatch "\.(js|css|html|htm|php|xml)$">
   SetOutputFilter DEFLATE
</FilesMatch>

And last, and probably least, serve your Javascript from a cookie-less domain.

And to keep your question in focus, remember that when you have DFN code you'll have several smaller javascript files that, precisely for being split, wont have the level of compression Closure can give you with a single one. The sum of the parts isn't equal to the whole in this scenario.

Hope it helps!

Frankie