Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

ColdFusion Components (CFC) vs. ColdFusion Closures Performance Exploration In Lucee CFML 5.3.4.80

By Ben Nadel on
Tags: ColdFusion

In the early days of ColdFusion, instantiating ColdFusion Components - CFCs - was pretty expensive. This was due, in part, to the highly dynamic nature of ColdFusion Components which has to be facilitated on top of Java, a significantly less dynamic language. With Lucee CFML, this cost was been lowered. However, due to my history with ColdFusion, I still have a [?mostly irrational?] fear of creating a lot of CFCs. As such, I thought it would be a fun experiment to compare the cost of CFC instantiation to the cost of Closure creation as a means to create small, simple APIs in Lucee CFML 5.3.4.80.

CAUTION: Most speed-tests in isolation are fairly worthless and do not effectively capture what it would be like to run code in a production environment under production load with production resources. As such, please take this exploration with a grain of salt. This was just something fun to do on a Sunday.

If you think about what a ColdFusion Component is, from an experiential standpoint, it's just a collection of Methods and Properties that work together to provide a cohesive set of behaviors. Given this perspective, you don't have to squint that hard to see that a ColdFusion Closure can provide the same experience; only, instead of using private properties, it uses lexically-bound properties.

In fact, closures are used in JavaScript to create component-like behavior all the time. To this day, I use this type of closure-based approach to define most of my AngularJS (pre-Angular 2+) Controllers and Services.

To see what I mean, let's look at really simple ColdFusion component that provides a few public methods that mutate some internal state. I'm calling this MockProcessBuilder.cfc:

component
	output = false
	hint = "I am a mock API being used to compare CFC overhead to Closure overhead."
	{

	/**
	* Using the ColdFusion Component (CFC) approach, our "member variables" would be
	* stored as properties on the PRIVATE SCOPE of the component. Then, our "builder
	* pattern" will simply update the private properties and return a reference to the
	* CFC INSTANCE in order to provide a "fluent", chainable API.
	*/
	public void function init( required string command ) {

		variables.command = arguments.command;
		variables.commandArguments = [];
		variables.workingDirectory = "";

	}

	// ---
	// PUBLIC MEHTODS.
	// ---

	public any function run() {

		return({
			command: variables.command,
			commandArguments: variables.commandArguments,
			workingDirectory: variables.workingDirectory
		});

	}

	public any function withArguments( required array commandArguments ) {

		variables.commandArguments = arguments.commandArguments;
		return( this );

	}

	public any function withWorkingDirectory( required string workingDirectory ) {

		variables.workingDirectory = arguments.workingDirectory;
		return( this );

	}

}

As you can see, this ColdFusion Component (CFC) uses a "builder pattern" to provide a fluent API that can be used to mutate the private variables within the component. Then, the .run() method simply echoes the state of the component.

Using ColdFusion Closures, we can implement the same type of behavior using a Factory Function that returns a collection of closures with lexically-bound variable access:

component
	output = false
	hint = "I am a mock API factory being used to compare CFC overhead to Closure overhead."
	{

	/**
	* Using the ColdFusion closures approach, our "member variables" would be stored as
	* LOCAL PROPERTIES in the lexical scope / defining context. Then, our "builder
	* pattern" will simply update the lexically-bound properties and return a reference
	* to the LEXICALLY-BOUND API OBJECT in order to provide a "fluent", chainable API.
	*/
	public any function getMockerProcessBuilder( required string command ) {

		// "Private variables" are just lexically-bound properties.
		var command = arguments.command;
		var commandArguments = [];
		var workingDirectory = "";

		// Since we don't have a CFC instance to work with, we are going to create a
		// "public API" that provides access to the lexically-bound variables.
		var api = {
			run: () => {

				return({
					command: command,
					commandArguments: commandArguments,
					workingDirectory: workingDirectory
				});

			},
			withArguments: ( required array newCommandArguments ) => {

				commandArguments = newCommandArguments;
				return( api );

			},
			withWorkingDirectory: ( required string newWorkingDirectory ) => {

				workingDirectory = newWorkingDirectory;
				return( api );

			}
		};

		return( api );

	}

}

As you can see, the ColdFusion behavior from the previous example has been re-created using an api object and a set of function-local variable declaration. To create the fluent API, these closures can't return the this scope - that's still reserved by the ClosureFactory.cfc; as such, they return the api reference, which exposes the set of closures as the "public API" on our mock-object.

To see that these two approaches create the same experience, let's try running both with the same inputs:

<cfscript>

	// Simple test with ColdFusion Component (CFC).
	results = new MockProcessBuilder( "echo" )
		.withArguments([ "a", "b", "c" ])
		.withWorkingDirectory( "/foo/bar/baz" )
		.run()
	;

	dump( label = "Using a ColdFusion Component", var = results );
	echo( "<br />" );

	// Simple test with ColdFusion Closures.
	results = new ClosureFactory()
		.getMockerProcessBuilder( "echo" )
		.withArguments([ "a", "b", "c" ])
		.withWorkingDirectory( "/foo/bar/baz" )
		.run()
	;

	dump( label = "Using a ColdFusion Closure", var = results );

</cfscript>

As you can see, we're using both the ColdFusion Component and the ColdFusion Closures; and, we're providing both implementations with the same arguments. And, when we run this code in Lucee CFML, we get the following output:

The same results for ColdFusion Components and ColdFusion Closures in Lucee CFML.

As you can see, we were able to provide the same fluent, chainable API with ColdFusion Components and ColFusion Closures. And, we achieved the same outcome.

And now that we see that these two approaches are providing the same results, let's take a look at the relative performance. To test this, I'm going to define a set time-window; and then, see how many iterations I can run using each approach in the time-window. In this case, we'll use 2-seconds:

<cfscript>

	duration = 2000;
	echo( "Using a window of #numberFormat( duration )# ms <br /><br />" );

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

	// SPEED TEST: ColdFusion components.

	endedAt = ( getTickCount() + duration );
	iterationCount = 0;

	while ( getTickCount() <= endedAt ) {

		iterationCount++;

		results = new MockProcessBuilder( "echo_#iterationCount#" )
			.withArguments([ "a", "b", "c", iterationCount ])
			.withWorkingDirectory( "/foo/bar/baz/#iterationCount#" )
			.run()
		;

	}

	echo( "Using CFC : #numberFormat( iterationCount )# <br />" );

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

	// SPEED TEST: ColdFusion Closures.

	endedAt = ( getTickCount() + duration );
	iterationCount = 0;
	// NOTE: Since we're using closures, the point would be to have the CFC cached.
	closureFactory = new ClosureFactory();

	while ( getTickCount() <= endedAt ) {

		iterationCount++;

		results = closureFactory
			.getMockerProcessBuilder( "echo_#iterationCount#" )
			.withArguments([ "a", "b", "c", iterationCount ])
			.withWorkingDirectory( "/foo/bar/baz/#iterationCount#" )
			.run()
		;

	}

	echo( "Using Closures : #numberFormat( iterationCount )# <br />" );

</cfscript>

As you can see, there's nothing special going on here - I'm basically just taking the previous examples and sticking them inside a while-loop. And, when we run this ColdFusion page a few times in Lucee CFML, we get the following output - remember, a higher number means more iterations means faster execution (I'm adding line-breaks between each run of the experiment):

Using a window of 2,000 ms

Using CFC : 100,997
Using Closures : 138,666

Using CFC : 100,164
Using Closures : 143,370

Using CFC : 95,875
Using Closures : 142,634

Using CFC : 116,681
Using Closures : 124,934

Using CFC : 114,210
Using Closures : 130,369

Using CFC : 112,733
Using Closures : 137,561

Using CFC : 89,299
Using Closures : 137,106

So, two things I take away from these results:

  1. Instantiating ColdFusion Components in Lucee CFML is very fast - 100,000 instances in 2-seconds, that's much better than I thought it would be. This goes to show how outdated my mental model is for component creation in ColdFusion.

  2. Creating ColdFusion Closures is even faster. As we can see in these numbers, using ColdFusion Closures to implement the same API leads to a consistently faster outcome - sometimes as much as 35% faster!

Now, to be extremely clear, I am not in any way saying that we should be using Closures instead of Components for all use-cases. While we did create the same API and generate the same results, the API surface area was small and the behaviors very simple. As such, the "developer experience" (DX) of the implementation was relatively interchangeable. However, there's no saying that such a DX would scale well with the size of the API of the complexity of the behaviors.

That said, what this does make clear to me is the fact that we can use ColdFusion Closures to create small, cohesive behaviors to great effect. What this means is that not every combination of properties and methods necessitates the full "Component" treatment.

If nothing else, this was a fun look at the powerful, flexible nature of the ColdFusion language. And, a reminder of how exciting it is to have Closures at our disposal - Closures that are insanely fast to define and consume in Lucee CFML.



Reader Comments

Wow. Good to know about the closure approach performance in CFML.

I disagree with the one statement where you have mentioned that component approach is much cleaner. I felt that closure approach is more easy to read and easy to understand for developer once we starts to adapt.

Modern java-script framework has same approach, so it gives cfml developers more advantage to be good at both.

Thanks for the article.

Reply to this Comment

@Vikrant,

I'm glad you found the post interesting :D I'm reflecting on the "cleanliness" comment; and, I think what I mean when I said "cleaner" was that the contents of the CFC were all centered around one cohesive set of features.

That said, I definitely love closures; and, regarding JavaScript frameworks, I use the closure-based approach all the time. In fact, I often define a whole "class" (in the loose sense of the word) using this approach:

function MyClass( messageIn ) {

	var message = messageIn;

	// Return public API.
	return({
		getMessage: getMessage
	});

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

	function getMessage() {

		return( transform( message ) );

	}

	// ---
	// PRIVATE METHODS.
	// ---

	function transform( value ) {

		return( value.toUpperCase() );

	}

}

Which, to me, is very much like the ColdFusion component, but using closures.

Reply to this Comment

In reference to the chainable API, which I think is very useful and cool, why do you use the variables scope in the init function rather than this? Is this just your preference or is there some advantage I'm not aware of? Asking for a friend.

Reply to this Comment

@Chris,

The variables scope is the "private" scope of the ColdFusion component. So, anything that I put in the variables scope is inaccessible to the outside world and can only be accessed by component methods. Conversely, the this scope is the "public" scope and can be accessed by anyone with a reference to said component.

At the end of the day, it doesn't really matter so much (not in a practical sense). But, I like the idea of keeping things private that I don't want people to muck with.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
NEW: Some basic markdown formatting is now supported: bold, italic, blockquotes, lists, fenced code-blocks. Read more about markdown syntax »
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.