I have uploaded an example project, so that I can run “more realistic” benchmarks. You can find the repo here. The benchmarks below are the result of rendering the form generated by the phoenix generators for a dummy User resource. This means the template is probably representative of “real world” templates.
You can run these benchmarks yourselfby running the following command in the repo above.
mix run benchee/main.exs
The results are:
Name ips average deviation median 99th %
vampyre 123.41 K 8.10 μs ±231.37% 7 μs 25 μs
phoenix 10.13 K 98.69 μs ±20.93% 95 μs 180.18 μs
vampyre 123.41 K
phoenix 10.13 K - 12.18x slower
This shows that in the case of forms, Vampyre templates are about 12x faster than the default phoenix templates. The absolute time differences are tiny, of course, because we’re talking about microseconds, but the improvement is quite impressive.
I still haven’t implemented all the HTML widgets implemented by
phoenix_html. In fact, I have implemented just enough to be able to render the output of the Phoenix generators, but most of the rest can be implemented on top of what I already have. This is not one of those cases where “the code is simple and fast because implementing the last 10% takes the last 90% of the code/work”, because the work my widgets do it at compile-time. Although my incomplete implementation of the HTML widgets is already more complex than the one in
phoenix_html (in terms of lines of code, and concepts I introduce throughout the code base) the complexity happens at compile time with the goal of making things fast at runtime.
Am I doing something dirty to get these benchmarks?
Yes. The implementation of
form_for that makes it fast inlines a function call by introducing a hygienic variable in the global scope of the template. This doesn’t change the semantics of the template too much because the variable is inaccessible to the user-written code. But it is an issue if you nest
form_for inside another
form_for. This is not valid HTML but there might be reasons why you’d want to do that. In case the above isn’t scary enough, I reproduce the code here:
def make_form_for(form_data, action, options, fun) do
]} = fun
hygienic_var = hygienize_var(arg)
substituted_body = substitute(body, arg, hygienic_var)
open_form = Tag.make_form_tag(action, options)
unquote(hygienic_var) = FormData.to_form(unquote(form_data), unquote(options))
close_form = Segment.static("</form>")
contents = [
I’m pattern matching on the AST of the function in the form to get the variable given as argument, an then, in the bpdy of the function, I substitute the old variable by a new one, and dump the body of the function into the global scope of the template. This avoids a function call and flattens the segments list, so that I can merge them together.
I think I can the issue with nested forms by doing even dirtier things with the process dictionary (I can safely assume that the template will be compiled by a single process) and using the variable’s
:counter in the metadata to make variables even more distinct from each other.
I still haven’t implemented all HTML widgets. Also, I haven’t even implemented all widgets correctly. Because I’m aggressively optimizing everything, there is some heavy-duty case analysis on the widget-generating macros/functions. For example,
tag(:input, name: "user[name]", id: "user_name") takes advantage of the fact that all attrbutes are static to render everything into a static binary (which can be merged with the previous or next binaries). However, if we change the above to
tag(:input, name: @name, id: "user_name"), the expression will be rendered into a static part, followed by a dynamic part, followed by another static part. If the user writes instead
tag(name, attrs), then this is a case I don’t even handle yet!
So I have yet to implement the “maximally dynamic” versions of my widgets. In such cases I might just fall back to Phoenix’s implementation, but there are still some performance optimizations to be had. For example, even if both the tag name and the attributes are purely dynamic, we know the rendered value starts with
"<" and ends with
">", which can be merged into the previous binaries.
Is this worth it?
To render normal templates, I don’t know. As I said above, the relative differences in performance are impressive (I wasn’t expecting such a difference), but rendering templates is already so fast that maybe it doesn’t really matter. And all the complexity can introduce bugs, security vulnerabilities, etc.
To use with something like Drab or LiveView, or something like that, where being able to siolate the static from the dynamic parts of the templates is critical (to minimize sending data over the network), this is definitely worth it.
Even if for that reason alone, I plan on continuing to work on Vampyre until it has feature-parity with
phoenix_html. Vampyre is already a drop-in replacement for
phoenix_html (by changing two lines of code in your project you magically get Vampyre templates for free), and if the user decides to use Vampyre templates for something like LiveView or Drab, then it makes sense to use Vampyre templates to render the “normal” views.
It will take some time, of course, because I’m not only implementing template renderers. I’m actually implementing an optimizing compiler for template renderers, and the default widgets need to play well with the compiler.This means that everything is about 5x harder to write than the naïve versions in
phoenix_html. It gets easier as I get further away, because I can implement some widgets on top of other widgets. For example, I’m implementing the
form_for widget on top of the
EDIT: I believe I’m doing everything right when defining the renderer functions for both Vampyre and Phoenix templates. I’ve copied the approach taken by Phoenix itself, so I guess I’m not being unfair to the Phoenix templates.