Related Topics: ColdFusion on Ulitzer

CFDJ: Article

Caching Redundant Dynamic Content With Custom Tags

Caching Redundant Dynamic Content With Custom Tags

What do you do when you want to get even better performance out of your most efficient application code?

It can be difficult to keep total page execution time low when processing a complex page. Developers are tasked with finding ways to make apps perform better everyday. By statically storing redundant results of a dynamic page, you can get a CF site to run exponentially faster.

CFHTTP, CFCACHE, and custom tags allow you to create static versions of an entire page, but only custom tags give you the flexibility to cache the results of portions of code, unleashing some awesome possibilities.

A Common Challenge
If you're like me, you do all your SQL queries with <CFQUERY> when developing a new page. Any CF developer knows this is not the preferred way to do things. We know we should use stored procedures because they're precompiled, so we create stored procedures out of each working <CFQUERY>. Some developers time their code and rewrite it in an effort to shave off milliseconds from their total execution time. Maybe you've tried everything, and you want to go even faster.

This is a situation I've been in. I wanted to make sure that each piece of my project was working at maximum capacity. My site was template-based and it became clear that each time a page was built, ColdFusion Server was doing a whole bunch of unnecessary work.

The Web application had three classic features: a navigation bar that the site administrator could change by logging in; the ability to modify content on the main page of the site; and a rotating ad management system - changes that were all stored in the database (see Figure 1).

The problem was that each time a visitor requested the main page, my application would query for the elements of the navigation bar, query for the main page content, and query for the banner advertising. If my site gets a million requests per day, that's 3 million queries right there. And for what? If the only thing that's changing between page re-quests is the advertising, why query for the navigation bar and the main page content over and over again? The application is running 2 million redundant queries per day.

This is the point at which many CF developers would mention cached queries. However, what if you have a cluster of machines and need to run 400 hefty queries? ColdFusion can only cache 100 queries. If you're an ASP, that's not enough.

Creating Static Pages
One way you can maximize your resources is to create static pages based on dynamic ones. Then, you just serve off the static version of your page and ColdFusion Server doesn't need to do any work for each page request. You can use CFHTTP to grab the HTML being served from a dynamic page, and then use CFFILE to write it off as a regular .html file. CFCACHE does the same thing in a single step, as does the procedure of saving the output of a ColdFusion scheduled task, described in Chapter 5 of Allaire ColdFusion Web Applic-ation Server 4.5, "Administrating Cold- Fusion Server".

The main problem I had with the CFHTTP method was that by creating a static version of the entire page, the resulting static page could no longer rotate banners or count banner impressions because it was no longer a .cfm file. Even if you end the resulting file with ".cfm", CFHTTP will return the same HTML content that you get when you view the source code of a page in your browser - HTML with all CFML removed.

I needed to find a way to generate partially static main pages. I say "partially" because I needed to maintain my ability to manage dynamic banner ads in ColdFusion, but at the same time, reduce the load on my CF boxes. It didn't make any sense to query for the navigation bar and the main page content for each page request because that rarely changed - that was the part I wanted to be static. Also, I didn't want to deal with CFHTTP because it was a bit slow, and I would need to reconfigure my production firewall in order to get CFHTTP requests from my production servers that are directed at my load balancer to work right. The solution was in a special variable called thisTag.generatedContent, available in any closing custom tag.

In case you're not familiar with this custom tag feature, you must first understand that custom tags have execution modes. Most developers call only the START mode without even knowing it. That's because you're in START mode when you call a given custom tag like this:

In exampleTag.cfm, you can check what mode you're in by using a special variable called thisTag.-executionMode. You're in END mode only in closing custom tags, which are called when you pass a slash in the tag, like this:
This allows you to use a CFSWITCH statement or a CFIF conditional statement in your custom tags to do certain things depending on which mode you're in. You could use the following code construct in the exampleTag.cfm:
<CFIF thisTag.executionMode IS "START">
<!--- Execute your code for start mode here --->
<CFELSEIF thisTag.executionMode IS "END">
<!--- Execute code for end mode here --->
Using these modes in conjunction with thisTag.generatedContent, you can grab the results of everything that happens between an opening custom tag and the corresponding closing custom tag. In the next example, I'll create a navigation bar based on a query:

<cfoutput query="navBarInfo"> <a href="#anchor#">#navText#</a><br>
There would be a variable available in </cf_grabTheNavBar>'s END execution mode called thisTag.-generatedContent, which contains the result of everything that happened between the <cf_grabThe-NavBar> and </cf_grabTheNavBar>. In this case, thisTag.generated-Content contains the resulting HTML content for the simple navigation bar:
<a href="main.cfm">Main</a><br>
<a href="company.cfm">Company</a><br>
<a href="contact.cfm">Contact Us</a><br>
While you're in <cf_grabThe-NavBar>'s END execution mode, you can do some cool things like change what the generated content is. In the next example, I'll take the generated content and CFFILE WRITE it to a .txt file, then clear the variable so nothing appears on the Web page. This is what my custom tag named grabTheNavBar.cfm would look like:
<CFIF thisTag.executionMode IS "START">
<!--- I won't do anything here. --->
<CFELSEIF thisTag.executionMode IS "END">
<!--- Write the content to a file. --->
ACTION="C:\myCFcache\sam ple.txt"
OUTPUT="#thisTag.generatedContent #">
<!--- Hide from browser display. --->
<cfset thisTag.generatedContent = "">
In this example, I've created a text file out of thisTag.generatedContent using CFFILE. Later, I can CFINCLUDE that .txt file on the main page of my site without having to query for it every time the page is requested.

I used this technique to solve my problem. I set up my application to write off the results of the navigation bar and of the main page content when they changed. On my main page, I had two simple CFINCLUDEs that bring in the preprocessed results. This way, I no longer have to waste my resources querying to get the same information for each page build. I have to query for it only once and write it off when the site administrator makes changes. With this simple technique, I got my application to perform five times faster because the database access on my main page was reduced to banner advertisement activity only.

Some other benefits of using thisTag.generatedContent instead of CFHTTP are:

  • The data is prepared much faster.
  • Your server will not count the static page generation as a page view or banner impression.
  • You free up CFHTTP threads for other things.
  • CF server doesn't need to be able to access itself. This could be troublesome depending on firewall settings.
Maybe you realize that by using the method described above, I make my main page vulnerable to a form of cross-site scripting. This means that the user responsible for editing site content could write ColdFusion code in the navigation bar or main page content that would be executed. For example, say the user wrote the following sentence in his or her main page content:
"ColdFusion is great. My favorite tag is the <CFABORT> tag."
thisTag.generatedContent would write that string to a .txt file via CFFILE, and then it would be included via CFINCLUDE when users browse the site. CFINCLUDE not only includes a file, but executes the included code as well. The <CFABORT> would be evaluated by ColdFusion Server and page processing would subsequently halt.

Using CFINCLUDE can mean big problems if you CFINCLUDE any random user's saved file. Somebody could write very nasty code that could do just about anything to your file system or database. Therefore, only use CFINCLUDE when you trust the author, or you want to be able to create executable CF code from within your application.

If you're concerned about unexpected ColdFusion code, one alternative that seems to work quite nicely is CFFILE READ instead of CFINCLUDE.

This way, the contents of your .txt file are stored in a ColdFusion variable that you simply CFOUTPUT.
When ColdFusion code is stored inside the text content of a Cold-Fusion variable, it's not evaluated. Therefore, the <CFABORT> will never be executed by ColdFusion Server. Instead, it flows into the HTML source and is sent to the browser.

There are lots of ways to optimize your code. It may be worth your while to figure out which parts of your code don't need to be dynamic for every page build. You may not have the need to keep some parts of a page dynamic, so you use the CFHTTP method. However, in ColdFusion 4.x, custom tags provide a faster way to render the complete page, or just certain parts of it. Let's see how Macromedia can make this technique less complex with the upcoming release of ColdFusion Server 5.

More Stories By Jon Block

Jon Block is the
lead ColdFusion
developer at Members Connect, Inc., in
Boston. He is one of
the cofounders of the firm's proprietary content

management system.

Comments (0)

Share your thoughts on this story.

Add your comment
You must be signed in to add a comment. Sign-in | Register

In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.