Question

A client of mine continually complains that their site isn't fast enough... which is kind of odd to me because when they hired me a couple months ago, they couldn't keep the server running for 24 hours because it was caching too much and running out of memory. So I removed most of the caching, optimized their database, got them on a new server that's at least 5x as powerful (both amount of RAM and speed of processors -- they have 4x as many processors now each with nearly the same clock speed), and they're complaining that "our site has NEVER been so SLOOOOOW!".

So I'm going through the list of all the things that Google says will improve performance, in the hopes that eventually they'll either be happy with it or they'll realize that their site is doing a lot of work and there are limits on their server and some pages will take a few seconds to load, just like you can't build an entire car by hand in an hour. All the research I've done suggests that Firefox should use its cache if I've specified Cache-Control, Expires and Last-Modified headers, but no matter how I set them in ColdFusion, both Firefox and Chrome refuse to use the cache. I can manually set the 304 response header, which seems to cause Firefox to return nothing if it hasn't already cached the image. (?!!)

So I have this code. Pretty simple. Pretty much exactly what all the documentation says it should be. And FireBug consistently reports (despite restarts, cache clearing, forced reloads, etc) that the content was created seconds ago, instead of 7 days ago like I'm setting in the cfheader. Chrome on the other hand reports the Last Modified Date as a week ago, but then reports that the max-age is 0 in the request header. (?!!) (Cache is enabled in the Chrome devtools, so I know it's not that.) The response header from Chrome shows the correct max-age value. (EDIT: This was confusion on my part about the display of data in FireBug and Chrome's developer tools - so it turns out that, yes, the response headers were received by both browsers correctly.)

<cfheader name="Content-Type" value="image/png" />
<cfheader name="Cache-Control" value="max-age=604800, private, must-revalidate" />
<cfheader name="Expires" value="#getHTTPTimeString(dateadd('d', 1, now()))#" />
<cfheader name="Last-Modified" value="#getHTTPTimeString(dateadd('d', -7, now()))#" />
....
<cfcontent type="image/png" variable="#toBinary(img)#" />

Given that Chrome reports the response headers correctly, this seems like it just doesn't work the way the HTTP standards tutorials I've read (which claim to be based on the standards) say it's supposed to work... but I can't be sure. I'm not an expert on HTTP certainly, so I could be overlooking something. This is not a complex application. Hence the reason why I'm so frustrated and tearing my hair out.

Any help is greatly appreciated. Thanks!

EDIT

So to give a little more info on the accepted answer, here's what my solution looks like in ColdFusion. The above code basically stays as it is in the page (index.cfm for example). Then to make use of the caching hints returned by the browsers, I have something like this in an Application.cfc

<cfcomponent output="false">

    <cffunction name="onRequest" access="public" output="true">
        <cfargument name="targetPage" type="string" required="true" />
        <cfset var modDate = getIfMOdifiedSince() />

        <cfif isDate(modDate) and datediff('d', modDate, now()) lte 7>
            <!--- the browser has a cached copy that's less than a week old, let the browser use it --->
            <cfheader name="Content-Type" value="image/png" />
            <cfheader statuscode="304" statustext="Not Modified" />
        <cfelse>
            <!--- the browser hasn't seen it in 7 days (or possibly ever), return the image --->
            <cfheader name="Last-Modified" value="#getHTTPTimeString(now())#" />
            <cfinclude template="#targetPage#" />
        </cfif>
    </cffunction>

    <cffunction name="getIfModifiedSince" access="private" output="false" returntype="string">
        <cfset var head = getHTTPRequestData().headers />
        <cfreturn iif(structKeyExists(head, "if-modified-since"), "head['if-modified-since']", de("")) />
    </cffunction>

</cfcomponent>

This may be a bit simpler than what you need. This particular code doesn't actually check to see if the image has been modified, because that's not something we're really worried about. This app only serves a specific set of images, so we know anything being served from this app will be a PNG so we can safely set the content type header to image/png and because the images really don't change, I'm not bothering with any kind of actual date checking. I probably could really allow the images to live indefinitely, but for now I'm letting the browser have them for a week at a time. I may increase that duration later as their business grows.

The real crux of the work, the thing that makes the caching work is that the onRequest() method checks the if-modified-since request header and that when present and within the desired timespan, it truncates the entire request by NOT including the target page (so none of the remaining page code will execute) and instead only sets the 403 Not Modified status response header.

In the long-run, just as I suspected, it is actually a really simple thing, it's apparently just widely misunderstood by people who create online tutorials about it.

Was it helpful?

Solution

To understand HTTP it's a good idea to go direct to RFC 2616, or at the very least a tutorial that cites the relevant sections.

In this case the relevant parts are the response header Last-Modified (section 14.29), the request header If-Modified-Since (section 14.25), and status 304 Not Modified (section 10.3.5). (You may want to read up on some of the other cache-related stuff there too though.)

Probably the best way to demonstrate what goes on is by looking at what the HTTP requests/responses would look like.

So here's the first request for an image (with a bunch of headers chopped out for clarity):

GET /res/image.png HTTP/1.1
Host: mydomain.com
User-Agent: I'm a browser
Accept: */*

That's basically what you get from the browser when the URL is http://mydomain.com/res/image.png.

The server parses that, figures out what the appropriate file is, and responds with something like:

HTTP/1.1 200 OK
Date: Tue, 06 May 2014 22:33:44 GMT
Last-Modified: Sat, 03 May 2014 20:00:25 GMT
Content-Length: 4636
Content-Type: image/png;charset=UTF-8

{image data}

When the browser does it's caching stuff, it makes note of the Last-Modified date, and when it later sends in a request it looks like this:

GET /res/image.png HTTP/1.1
Host: mydomain.com
If-Modified-Since: Sat, 03 May 2014 20:00:25 GMT
User-Agent: I'm a browser
Accept: */*

The server checks the If-Modified-Since against the file's modified date, sees there is no change, and responds:

HTTP/1.1 304 Not Modified
Date: Tue, 06 May 2014 22:34:56 GMT

And of course does not return the image data.

(If there HAD been a change, the response would be a 200 (with a new Last-Modified value) and the new data.)

OTHER TIPS

A denial-of-service can occur if enough there are lots of requests to CFContent-delivered web asset. If a visitor, bot or hacker is on a slow connection (or throttles their bandwidth), your ColdFusion threads will quickly fill up and be queued.

Any reason why the image(s) can't be served using the web server from a sub-directory with special "7 day expire" header rules added to it?

If the images are unique, you could generate them in an offline temp directory, set up directory mapping and then use unique or session-based filenames and use a scheduled task to clean up out-dated images.

The site's page load time could also benefit by lazy loading the images. This would load the webpage and only the visible images in "above the fold". This can be done using javascript. If you use jQuery, here's a decent plugin: http://www.appelsiini.net/projects/lazyload

Do you add the ColdFusion processing time to the generated HTML? We use GetTickCount() at the start & end and then post the generation time as a comment in the HTML (Execution: 163 milliseconds). If you see minimal time, then the bottleneck isn't ColdFusion.

Regarding webpage performance, I've had great success using Google PageSpeed. (I'm on IIS7, so I've had to use IISpeed http://www.iispeed.com/) On one of my sites, the perceived load time of a webpage with many product images went from from 13 sec to 7.2 (-5.8s / 44% faster).

https://www.youtube.com/watch?v=VwEGGB-4sMw

I've only tested IISpeed with ColdFusion. On a per-site basis, we use IISpeed to automatically perform the following optimizations on ColdFusion 9 generated pages:

  • Combine & Prioritize Critical CSS
  • Move CSS Above Scripts to Head
  • Combine, Minify & Defer JavaScript & Move to Head
  • Extend Cache (uses a hash to cache resources for 1 yr, unless changed)
  • Lazy Load Images, Sprite Images & Convert JPEG to Progressive
  • Optimize Images (Resizes images for smaller device screens & serves WebP to Chomr browsers)
  • Collapse Whitespace & Remove Comments

To test your website's page load speed, use http://www.webpagetest.org/

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top