[Posted by Mark Atherton]
Summary
Cozi has developed an ASP.NET control to combine multiple JavaScript or CSS files into a single reference, thus improving performance. The control handles problems of cache expiry dates and versioning, is easy to use, and can be switched on and off for debugging and testing.
Background
We’d like to organize our JavaScript and CSS files logically, without having to worry about performance. For instance, as we write more and more object-oriented JavaScript, we might want to put each JavaScript class into its own file. However, doing so would increase the number of round trips between the browser and the Web server.
This performance penalty is especially bad for JavaScript files since each one may contain a document.write which could change the sense of everything after it—so the browser can only download one at a time.
Unfortunately each extra JavaScript file adds about 200ms to page load time—the exact time varies by a dizzying array of factors (DSL vs Cable, how far it is from the browser to the server, etc, etc).
CSS files are not quite as bad as page parsing does not have to stop whilst they are being downloaded; however, the page cannot be correctly rendered until they have all been received.
Caching can help mitigate this delay, although it has its own problems.
Caching
To get maximum end-user performance, a Web site needs to set expiration headers on as many files as possible, especially static web content like images, CSS, JavaScript, etc. These headers tell the browser that it does not need to check back with the server for an updated version of a given file for some defined period.
Once that period has expired (or if no expiration is set), the browser will typically send a 'conditional get' giving the 'last-modified-time' of the file. If the file has not changed, the Web server can respond with a '304 Not Modified' response and not send down the data again.
Unfortunately the browser still needs to wait for the response before continuing. Therefore, it is best if the browser has a copy of the file AND it knows that it can be used without referring back to the Web server.
Versioning
Caching can also interfere with file versioning.
The easiest way to enabling caching is to tell your Web server that a whole directory of files can be cached on the client for a week (or day, or whatever). Unfortunately when you edit one of the cached files and deploy the new version, you have a problem because existing clients will run with their locally cached copy of the file until they reach the end of the expiration period.
How we solved these problems at Cozi
At Cozi we are developing an increasingly rich Web-based tool for making life simpler for families, including a shared family calendar, shopping list management, and more.
Our engineers need to have quite a few JavaScript and CSS files so that several of us can work on the site without trampling on each other’s code. However, this created the performance problems described above.
Part of our solution was to combine CSS and JS files into groups that get downloaded as one blob, saving many round trips.
Our requirements for the combining tool were:
1. It had to be easy to use.
2. You must be able to switch it on and off for testing and debugging scenarios (on a page-view-by-page-view basis).
3. It must support long cache expiration dates on these combined files.
4. It must solve the versioning problem.
To implement the solution, we wrote an ASP.NET control that wraps around our <link> and <script> HTML statements.
For example here’s a simplified version what the CSS references for our home page looks like:
<client:CombinerControl runat="server">
<link rel="stylesheet" href="/styles/sIFR-screen.css" type="text/css"/>
<link rel="stylesheet" href="/styles/Cozi.css" type="text/css"/>
<link rel="stylesheet" href="/styles/FooterToolbar.css" type="text/css" />
...
</client:CombinerControl>
The combiner outputs a reference to an Http Handler which will serve the combined file (I've put in some line breaks to make this a little more readable):
<link rel="stylesheet" type="text/css" ←
href="../Combiner/Combiner.ashx?ext=css ←
&ver=9b0d6e4e ←
&type=text%2fcss ←
&files=!styles*sIFR-screen*Cozi*FooterToolbar*" />
As far as the browser is concerned, this is just a reference to a CSS file. It neither knows nor cares that it is programmatically generated.
A few parameters are passed to the handler (Combiner.ashx). They are:
Parameter |
Meaning |
ext |
The file extension of the files, this is to save having to pass the file extension for each file separately. |
ver |
This is how we handle versioning the CSS files. The combiner calculates the ‘version’ of the combined file by mushing together the last-modified-time of all the constituent files. If any one of the files changes, this parameter will change and the browser will request a new combined file. |
type |
This is the Content-Type the handler should return. |
files |
This is the list of files to be combined. It is somewhat compressed to reduce the length of the URL generated. ! means the following token is a new directory that applies until the next change of directory is seen. * means a file to be combined. ' replaces / for legibility, as / would have to be encoded as %2f |
Here’s a similar code snippet combining JavaScript code:
<WebClientCode:CombinerControl ID="CombineScript" runat="server">
<script src="script/third-party/jquery.js" type="text/javascript"></script>
<script src="script/third-party/sifr.js" type="text/javascript"></script>
<script src="script/third-party/soundmanager.js" type="text/javascript"></script>
<script src="script/cozi_date.js" type="text/javascript"></script>
</WebClientCode:CombinerControl>
The combiner outputs a reference to an Http Handler which will serve the combined file:
<script src="../Combiner/Combiner.ashx?ext=js ←
&ver=59169b00 ←
&type=text%2fjavascript ←
&files=!script'third-party*jquery*sifr*soundmanager*!script*cozi_date*" ←
type="text/javascript"></script>
The combiner control meets our requirements thus:
1. It had to be easy to use
It is, you just wrap the files you want: <client:CombinerControl runat="server">
2. You must be able to switch it on and off for testing and debugging scenarios
By passing a parameter on the Query String you can turn this behaviour off for testing and the control will merely output the list of files as though the control itself didn’t exist.
3. It must support setting long cache expiration dates on these combined files
The handler that serves the combined file automatically sets a very long expiration on the combined file, no configuration is needed.
4. It must solve the versioning problem
Because browsers cache based on the full, case-sensitive URL of the resource, you can set ‘infinite’ expiration, as if any file changes, a different reference will be written out and the browser will request a new file.
Gotchas
As always, we ran into a few gotchas:
- You can’t combine CSS files with different media tags; all the CSS files must have the same media type.
- You must preserve the order of the files; both for script and CSS files, order can matter.
- You must remove UTF-8 BOM marks. It would be OK to have a BOM mark at the very start but many of our JS files had BOM marks at the start and when we combined them, these BOMs ended up in the middle of the combined output and caused the browsers problems as they were interpreted as strange characters.
- It’s very hard to combine script files that take query string parameters; luckily it is quite unusual for static script files to take parameters so this wasn’t much of a limitation.
Further work
Our combiner also manipulates the contents of the CSS and JS files for other reasons, unrelated to main work of combining the files. For example, we might blog about how we use the combiner to load balance and edge-cache our images in another post.
Related web links
http://www.thinkvitamin.com/features/webapps/serving-javascript-fast
Great article on combiner-type issues, albeit rather PHP- and Apache-centric.
http://developer.yahoo.com/performance/index.html#rules
Rules for high performance Web sites.





This is a really interesting article, I was thinking about implementing a resource combiner much like this one. Is the code available for this or is it something internal you just wanted to blog about?
All the same, very cool stuff guys.
Posted by: James | May 18, 2008 at 06:04 AM
Thanks James, we are considering making the code available but right now we're just too busy.
If more people are interested, do let us know.
Thanks.
Posted by: Mark Atherton | May 27, 2008 at 03:57 PM
Nice article! Code samples would be nice, how did you solve requirement 4, versioning?
Thanks
Posted by: Marco | May 30, 2008 at 01:28 PM
We handled versioning with the 'ver' parameter. When the control generates the link to the combiner handler it includes this ver parameter.
We don't actually use the ver parameter ourselves, it's just there so that each version of the files gets cached individually by the browser.
The control calculates the 'ver' parameter by hashing together the last-modified-time of all the component files.
Posted by: Mark Atherton | May 30, 2008 at 02:12 PM
I'd love to see the source code if you get around to posting it ...
Posted by: rob cherny | July 30, 2008 at 08:58 PM
Very nice. I've been looking into how to set far future expires headers on css and js files. This is a very elegant solution, with the added benefit of script combining. I can't believe there's no easy way to do it .net out of the box.
p.s. I would love to look at the source for the ashx
Posted by: Greg Woods | August 14, 2008 at 08:29 AM
Great solution!
Another Gotcha: In order to get the most out of this technique, it will be important to remember to include every required script/stylesheet for every page of your site/application in the combiner on every page.
An alternative would be to only bundle your main (core) script/stylesheet files in this way, then add page-specific files separately.
This is important because each time the combined script URL changes (for example: if its different for each page), the browser will need to download whole combined file again. This may result in worse performance than if you just left the files as separate references.
Keep up the good work!
Posted by: Andrew Ramsden | September 22, 2008 at 04:18 AM
Just what I was looking for! Any plans to release this to the public?
Posted by: kiko | October 01, 2008 at 03:31 AM