Select Page

Exploring Turbo Drive Back-Button Caching Behavior In Lucee CFML

Cyberdime
Published: February 11, 2023

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="https://www.bennadel.com/blog/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!

Check out the license.

Source: www.bennadel.com