Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Salvatore D'Agostino
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Salvatore D'Agostino

Using Stale-While-Revalidate Cache-Control Headers In HTMX And ColdFusion

By
Published in ,

In my ColdFusion applications, unless I'm generating dynamic images that need to be cached, I almost never use the Cache-Control HTTP headers. Historically, all of my "caching needs" for perceived performance have been handled in my client-side Angular controllers. But, the HTMX JavaScript framework is all about leaning on the web platform to drive application mechanics. As such, I wanted to see how I might use the stale-while-revalidate response directive to boost performance in an HTMX and ColdFusion application.

The Cache-Control HTTP header contains a list of directives that tell the browser if-and-how to cache the content being returned by the server. One of the available directives is stale-while-revalidate. This directive tells the browser that it can show stale content to the user for some period of time; but that it must make a request in the background to re-fetch the given URL and "freshen" the cache.

This means that if a user hits a stale URL, they'll see the old content rendered immediately on the current request. But, there's a good chance that the next time they hit the same URL, it will have been updated in the background to contain the latest content.

The value-add of this directive is closely linked to how critical it is to see up-to-the-moment accurate results. Or, if it's OK to see some staleness in the user interface.

To explore this HTTP header, I'm going to serve my ColdFusion page with a Cache-Control header that contain three directives:

  • private - this tells the browser that the returned content has privileged / personalized information and should be stored in a private cache. I believe this also ties into the Cookie header, and caches based on the cookie-driven session management.

  • max-age=1 - this tells the browser that it can cache the response and present it to the user as "fresh" content for up to 1-second. After the 1-second time window, the content should be considered "stale".

  • stale-while-revalidate=120 - this tells the browser that it can continue to show the user "stale" cached content for up to 120-seconds. But, that it must make a request in the background to re-fresh the content associated with the given URL.

My ColdFusion page will be artificially slowed-down for 2-second so that we can clearly see which rendering is based on cached content and which rendering is blocking-and-waiting for the server. I then have three links which just link back to the same page with a URL parameter:

<cfscript>

	param name="url.page" type="string" default="one";

	// Artificially slow down the response so we can see the effects of caching.
	sleep( 2000 );

	// Caching directives:
	// 
	// PRIVATE - only cache the response in the user's local computer (ie, not in any
	// shared caches in the network hops). And, include the Cookie header as a point of
	// differentiation in caching heuristics.
	// 
	// MAX-AGE - the response is only "fresh" for 1-second.
	// 
	// STALE-WHILE-REVALIDATE - the browser can continue to use a "stale" cached response
	// for 120-seconds, but needs to send a background request to revalidate / re-fresh
	// the cache in the meantime.
	header
		name = "Cache-Control"
		value = "private, max-age=1, stale-while-revalidate=120"
	;
	// Tell the browser to cache "HTMX" requests separately from non-HTMX requests. This
	// is helpful if we want to start showing a partial / different layout for HTMX
	// requests vs. native browser requests.
	header
		name = "Vary"
		value = "HX-Request"
	;

</cfscript>
<cfoutput>

	<h1>
		Page <mark>#encodeForHtml( ucase( url.page ) )#</mark>
	</h1>

	<p
		hx-boost="true"
		hx-sync="this:replace">

		<a href="index.cfm?page=one">Page One</a> -
		<a href="index.cfm?page=two">Page Two</a> -
		<a href="index.cfm?page=three">Page Three</a>
	</p>
</cfoutput>

I then loaded this ColdFusion application and clicked-through the three links in order to pre-warm the cache with the HX-Request=true request header. This filled the browser cache with "stale" content. I then started recording the screen and clicked back through the three links:

Screen capture of network activity showing two HTTP requests triggered for each click - one served immediately from the disk cache and one served eventually from the server.

As you can see, every time I click on a link, the page is updated immediately. This is because the browser is willing to show me stale content being served locally from the cache. But, if we look at the network activity, we can see that every click of a link actually triggers two different network line-items:

  • The first request is the "stale" content served from the "disk cache". From the HTMX perspective, this is just a normal AJAX request; but, it happens to complete in 1-2 milliseconds. And, HTMX will swap-in the new (stale) content the same way it would for any other AJAX request.

  • The second request is the HTTP request that the browser makes in the background thanks to the stale-while-revalidate directive. This freshens the cache in the background so that the next time I view the same URL, I'll have updated information being served locally.

Obviously, this is not a perfect solution since the user is seeing stale content on each subsequent click. But, this might be acceptable in a bunch of places within a dynamic application. Plus, I think there's some other stuff we can do to improve this—but that's a topic for another post.

Double-Rendering In a Single-Page Application

In my AngularJS and ColdFusion applications, I basically did the same thing. However, instead of using the stale-while-revalidate directive, I managed the cache in the Angular logic; and, would often double-render the Angular view. Essentially, when a user navigated to a view in my Single-Page Application (SPA), I would:

  1. Immediately render that view using in-memory cached content.

  2. Make an AJAX request in the background to get the up-to-date information.

  3. Re-render the view (if it was still the active view) using the fresh content.

The stale-while-revaldiate takes care of steps 1 and 2 automatically; but, doesn't perform step 3. Maybe there's something we can do about that in HTMX. I need to let that marinate in my brain and do some more experimentation.

As an aside, the Unpoly framework uses a strategy similar to what I was doing in Angular. It will cache "fresh" content for 15-seconds. But then, will make a second request to revalidate the content on the server. This leads to a two-phased rendering strategy (just like I did).

Want to use code from this post? Check out the license.

Reader Comments

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel