Note: this article is based on Velocity 1.7 because that is the version in use. I know version 2.0 exists and it probably has a very different performance profile compared to what I have shown here. This exercise was more of an outside of work curiosity and not a full assessment of Velocity's performance.
Directives are harder to code and aren't very well documented. On the other hand utility/tool classes are documented and are an integral part in how Velocity is designed to be extended. So in my mind, going to the trouble of writing custom directives has only one purpose - performance gains.
So I set up a very simple test harness that would call merge() the same template 100 times. In one of the cases I made use of a utility/tool class and the other case used a custom directive. The only functionality that both of these had was to take an input string and output it as is.
This was the code for the harness (with the run*() calls commented out). Note that I made sure to start timing only around the merge() method, excluding VelocityContext population, though in reality this made little difference.
The utility class and its corresponding template looked like this. It doesn't get any simpler...
The directive was more complicated, but it did essentially the same thing as the utility class. Because this was an inline directive it also output a new line after the input string so that I could get the same strings out of the merged template.
I had to modify both of the templates to change their foreach loops to increase the number of iterations since my initial count of 100 didn't show any significant figures i.e. it was taking something like 0-1 ms to run. These tests were done on my 2017 MacBook Pro. The minimum number of iterations that showed significant results was 10,000. I also then added 100,000 and 1,000,000 to see how it scaled.
The results were interesting...
To my surprise, the custom directive was always faster than a utility class! Between 15-20% faster in fact. My expectation was that it would be the other way around. The initial few merge() calls showed even higher variations - setup time for the custom directive seemed to be much lower than for a utility class.
So my results show that directives appear to be much faster to use when compared to a utility/tool class. However these results are hardly realistic since templates will not have so many invocations of the same directive/utility over and over. Still there's something interesting here and maybe this was the reason for writing this code in the first place by whoever wrote it!
If it were up to me I would go with the utility/tool class approach because of its ease of implementation and maintainability. Using a utility/tool also decouples your code from Velocity internals which is very much what the directive approach depends on. Without a compelling reason to optimise, writing clean, maintainable code should always take precedence.