Skip to main content
Ben Nadel at the jQuery Conference 2009 (Cambridge, MA) with: Vlad Filippov
Ben Nadel at the jQuery Conference 2009 (Cambridge, MA) with: Vlad Filippov ( @_vladislav_ )

Exploring Turbo Drive Back-Button Caching Behavior In Lucee CFML

By on

When you enable Hotwire Turbo Drive in your ColdFusion application, link clicks and form submissions are intercepted and then subsequently executed via the fetch() API. Hotwire continues to maintain expected browser behaviors by pushing the relevant URLs onto the browser's History API. Then, if the user presses the Back Button, Turbo Drive pulls the previous rendering out of its cache and restores the previous <body> content and scroll offset. In this post, I want to take a closer look at when the page cache is populated; and, what events get triggered when a cached page is re-rendered.

View this code in my ColdFusion + Hotwire Demos project on GitHub.

The magic of Hotwire is that you can take a standard multi-page ColdFusion application (ie, the type of application that we've been building for decades) and progressively enhance it to look and feel more like a single-page application (SPA). Which means, after a give page loads, the content of said page may continue to change prior to a top-level navigation event.

And, once you perform a top-level navigation, what version of the previous page is getting cached? Is it the initial load of the previous page? Or, is it the version of the page that was just unloaded? To explore this, I've created a simple ColdFusion application in which we can use a Stimulus Controller to alter the runtime styles of arbitrary DOM (Document Object Model) nodes.

This Stimulus Controller - magic - uses a <button> element to locate other elements via a query-selector; and then, appends styles to the located elements:

<button
	data-controller="magic"
	data-action="magic#changeDom"
	data-magic-selector-param="p"
	data-magic-styles-param='{
		"border": "3px dashed red",
		"background-color": "yellow"
	}'>
	Change Runtime Styles
</button>

When this button is clicked, the magic.changeDom(event) method is called. The changeDom() method uses the given selectorand styles parameters to update the runtime state of the page. Here's the code for this Stimulus Controller (slightly truncated):

// Import core modules.
import { Application } from "@hotwired/stimulus";
import { Controller } from "@hotwired/stimulus";

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

class MagicController extends Controller {

	// ---
	// LIFE-CYCLE METHODS.
	// ---

	/**
	* I run once after the component instance has been bound to the host DOM element. At
	* this point, all of the classes, targets, and values have already been bound.
	*/
	connect() {

		console.info( "Controller connected." );

	}


	/**
	* I get called once after the component instance has been unbound from the host DOM
	* element. At this point, all of the targets have already been disconnected as well.
	*/
	disconnect() {

		console.info( "Controller disconnected." );

	}

	// ---
	// PUBLIC METHODS.
	// ---

	/**
	* I alter the runtime styles of the DOM for the purposes of testing the cache.
	*/
	changeDom( event ) {

		document.querySelectorAll( event.params.selector ).forEach(
			( node ) => {

				Object.assign( node.style, event.params.styles );

			}
		);

	}

}

// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //

window.Stimulus = Application.start();
// When not using the Ruby On Rails asset pipeline / build system, Stimulus doesn't know
// how to map controller classes to data-controller attributes. As such, we have to
// explicitly register the Controllers on Stimulus startup.
Stimulus.register( "magic", MagicController );

As you can see, the changeDom() method queries the runtime state of the DOM for the given nodes and then appends the given styles object to said nodes. This causes the state of the DOM to change beyond the initial rendering by the ColdFusion application. What we want to test is whether or not these runtime style changes are saved in the Turbo Drive page cache?

To test this, I've created two pages in my ColdFusion application: one page to test the styles and one page to provide a means of activating the browser's back button.

Here's the main ColdFusion page with the aforementioned Stimulus Controller - note that it includes a deferred-loading Turbo Frame that will update the content of the page after the initial rendering:

<cfscript>

	// Slowdown the load on the main page so that we can see any flashes of cached content
	// as the page navigation is occurring.
	sleep( 500 );

</cfscript>
<cfmodule template="./tags/page.cfm">
	<cfoutput>

		<h1>
			ColdFusion + Hotwire Back-Button Cache Demo
		</h1>

		<p>
			<a href="index.htm"><strong>Home</strong></a> -
			<a href="about.htm">About</a>
		</p>

		<hr />

		<h2>
			Welcome to Our Site!
		</h2>

		<p>
			Copy, copy, copy....
		</p>

		<h2>
			Lazy Loaded Content
		</h2>

		<turbo-frame id="home-frame" src="frame.htm">
			<p>
				Frame content is eagerly-loading....
			</p>
		</turbo-frame>

		<!---
			This Controller will dynamically change the runtime styles of all the P tags.
			This will help illustrate how and when the page cache is populated (and then
			later restored or previewed).
		--->
		<button
			data-controller="magic"
			data-action="magic##changeDom"
			data-magic-selector-param="p"
			data-magic-styles-param='{
				"border": "3px dashed red",
				"background-color": "yellow"
			}'>
			Change Runtime Styles
		</button>

	</cfoutput>
</cfmodule>

And, here's the deferred-loading Turbo Frame:

<turbo-frame id="home-frame">
	<p>
		This is lazy-loaded frame content. Copy, copy, copy ....
	</p>
</turbo-frame>

The content of the "About Page" isn't relevant - it exists only to give us a page from whence to navigate back. As such, I won't bother showing the content. What we want to see is what happens when we load the initial page, update with the DOM, navigate away from the page, and then hit the back button:

Hitting the back button results in a re-rendering of previously modified page, demonstrating that runtime changes are saved to the Turbo Drive page cache.

As you can see, not only were the runtime changes to the DOM persisted in the cache, the lazy-loaded Turbo Frame did not reload upon re-rendering (if it had, the <p> tag contained within it would have reverted back to its original styles).

What we can tell from this experiment is:

  1. The Turbo Drive page cache is populated upon unloading of a given page. This allows all the runtime changes to be persisted in the cache.

  2. Turbo Frames are not re-fetched when rendered from the page cache.

Another interesting feature (not shown in the screenshot) is that if we look at the Chrome Dev Console when we hit the back button we see:

Controller connected.

This is the logging from our MagicController attached to the <button>. So, when a page is re-rendered from the page cache, it maintains the previous state of the page but it also re-attaches all of the Controllers! Very interesting!

In some ways, this is a really nice experience when compared to hitting the back button in a Single-Page Application (SPA). Re-rendering cached data and restoring the scroll offset inside a series of nested components is a shockingly hard thing to do. And here, with Turbo Drive, it's basically the default behavior - noice!

Page Cache For Application Visits

In this post, we looked at how the page cache interacts with the browser's back button. But, the page cache can also be used to provide a preview during an application visit (ie, clicking a link in order to move forward in the space-time continuum). This forward-facing cache can cause a flicker of the previously rendered content (while the new page is being fetched):

Visting a link renders a preview of the cached page while the latest content is being fetched. This causes the runtime changes to be shown briefly.

In my ColdFusion demo, my page sleeps for 500ms in order to exaggerate the network latency; but, when we navigate from the About page back to the Home page, the runtime changes that we applied to the DOM are clearly visible before the fresh content is rendered.

Pages can opt-out of caching by using the turbo-cache-control meta tag or by adding the data-turbo-cache attribute to a given element (for finer-grained control). But, that goes beyond the scope of this post.

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

Reader Comments

15,688 Comments

Just a heads-up (and a note to self), as of the Hotwire v7.3.0 release, the [data-turbo-cache=false] attribute to prevent caching of targeted elements is being deprecated in favor of a new attribute, [data-turbo-temporary]. This is meant to mirror the [data-turbo-permanent] attribute more semantically.

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