An experiment in combining JavaScript and CSS Requests

During Peter Farrell’s cfObjective presentation on front end optimizations, one of the many tips he had involved minimizing the amount of HTTP requests your HTML makes. As a simple, and somewhat contrived example, consider the following HTML:

As you can see, I've got jQuery and 4 plugins being loaded. I've also got 3 different style sheets. All in all this reflects 8 additional HTTP requests the page has to make while parsing the HTML, and that's not counting the images that would normally make up an average web page.

As a graphical example of the impact of these scripts, check out the YSlow report:

As you can see, I got a C and an overall score of 70. The first thing YSlow points out to me are the number of HTTP requests. I decided to write my own CFML code to see if I can address this. My code would act as a simple service. I'd pass it a list of files and the code would return all the resources in one request. Before going any further, please note I did this as an experiment. There is a supported, and more full featured, open source project out now you should use instead of my code: combine. I just wrote this for fun - so please keep that in mind. Ok, with that out of the way, let's go through what I built step by step.

<cfsetting enablecfoutputonly="true" showdebugoutput="false">

I begin by enabling cfoutoutput only. This will reduce the whitespace generated by the request. Since I'm serving up JavaScript and CSS files, it also makes sense to disable debug output.

Next we have a root folder setting. You must edit this line before using the script. I felt bad about this at first because it felt like it should be something I externalize - but then I remembered - I'm not building a custom tag here. I'm building a service. So one small amount of setup isn't so bad. Why the array? Well, this (and the next block) are the one part I'm most unsure of. For security reasons, I didn't want you to pass in full paths to files. Therefore, it makes sense to embed a root folder in the service itself. However, many people keep their CSS and JS files separate. I could allow folks to simply use their web root for the root but then they would need to pass in the subfolder for every resource requested. Ie, something like /js/foo.js, /js/goo.js, /js/zoo.js. My solution was to allow for either a simple string value or an array. If you use a string, well, it's used as is. If you use an array, I allow you to pass which index to use for the root in the URL string. More on that in a second. If you don't specify one, then the first array element is used.

So yeah, here is the part I really don't like. As I said, you can use an array of root folders, and you can ask for a specific one via the URL. I use url.root for that value. I didn't want you to pass in a string value of course, so instead, I simply let you pass in the index. This requires some knowledge of the configuration. All in all, this feels a bit wonky. You've never had to really hide the paths of resources before, so why bother? If I were to rewrite this, I'd probably suggest folks use the web root and simply use the subfolders when requesting resources. Actually, I don't have to rewrite it - that works already. So um - consider it deprecated. ;) Ok, carrying on....

<!---
Root Type: This should be .js or .css.
--->
<cfparam name="url.roottype" default=".js">
<!--- Set our content type based on roottype --->
<cfif url.roottype is ".js">
<cfset variables.contenttype = "text/javascript">
<cfelse>
<!--- I set url.roottype just to be anal since we use it again later for file security --->
<cfset url.roottype = ".css">
<cfset variables.contenttype = "text/css">
</cfif>

The next block of code handles setting up a requirement for the type of file being requested. This will allow us to do a security check later on, and it also allows us to use the right content type. I could have simply looked at the first resource requested (is it something.js or something.css), but I felt like being anal about allowed me to really lock it down to *.js or *.css.

There we have the cache set up routine. Notice I use structGet (remember it?) to create a pointer to the scope holding my cache. I do the necessary locks and create the root structure for my cache if I need to.

Now that we have a cache, we can actually use it - if it exists in the cache. Notice too the use of the expires header. YSlow pointed this out to me during my development. To my readers in 2032, I apologize. Also, please tell the alien overlords to be nice to my kids.

<cfset buffer = "">
<cfloop index="res" list="#url.list#">
<!--- For each file, if it contains .., assume it is a hack attempt and immediately barf. --->
<cfif find("..", res)>
<cfabort>
</cfif>
<!--- For each file, if it does not end in js, assume it is a hack attempt and immediately barf. --->
<cfif right(res, len(url.roottype)) is not url.roottype>
<cfabort>
</cfif>
<cfset trueFile = variables.folder & "/" & res>
<!--- If the file doesn't exist, we skip. Don't throw an error because we don't want to be used to scan the system. --->
<cfif fileExists(trueFile)>
<cfset buffer &= fileRead(trueFile)>
</cfif>
</cfloop>

Woot! Finally some real work. Here you can see how we loop over the list of requested files. For each, I'm going to do a quick extension check, and if it passes, and if the file exists, I read the contents into a buffer variable. That's really it. Pretty simple, right?

The final bits then simply handle storing the buffer into the cache and finally returning it to the client. Again, note the user of the expires header. Ok, so how does it look when I use it? Here is a modified form of the original HTML:

As you can see, each of my multiple JS and CSS requests have been turned into one. For the CSS one I have to specify a bit more as most of my defaults are for JS. But really, it isn't that difficult to use. And the result?

Woot! An A! Those of you who know how fragile my ego is will not be surprised to hear this made me do a quick little dance of joy. Anyway, it was really fun to build this, but again, I'll point people to the combine project by Joe Roberts. His also adds compression to the mix for even more performance. The complete code for the script is below. Enjoy.

About Raymond Camden

Raymond is a developer advocate. He focuses on JavaScript, serverless and enterprise cat demos.
If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support.