question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Evaluate computation expression-based HTML for better rendering performance

See original GitHub issue

TL;DR: F# 6 makes it possible to use computation expressions for HTML and have much better performance than the current list-based syntax. It could look like this:

div {
    text "Welcome to "
    a {
        attr.href "https://fsbolero.io"
        attr.id "link"
        text "Bolero"
    }
    text "!"
}

Background: Blazor rendering

Blazor uses a diffing render engine, like React; but unlike React, it doesn’t diff between two tree representations of the DOM. Instead, a DOM fragment is represented by a linear sequence of instructions. For example, the following Razor file:

<div>Welcome to <a href="https://fsbolero.io" id="link">Bolero</a>!</div>

is compiled into approximately the following C#:

void Render(RenderTreeBuilder builder)
{
    builder.OpenElement(0, "div");
    builder.AddContent(1, "Welcome to ");
    builder.OpenElement(2, "a");
    builder.AddAttribute(3, "href", "https://fsbolero.io");
    builder.AddAttribute(4, "id", "link");
    builder.AddContent(5, "Bolero");
    builder.CloseElement();
    builder.AddContent(6, "!");
    builder.CloseElement();
}

Notice the sequence numbers in most of these calls. They are used to efficiently diff things like conditionals and loops. They are generated at compile time, and they are the main reason why generating such code as a library, rather than a compile-time tool such as Razor, is quite non-trivial. Naively keeping an incrementing runtime counter could cause inconsistent numbering between consecutive renders.

Bolero’s current solution

In Bolero, the HTML functions build a tree representation of the DOM as a discriminated union. Then, in a second pass, this tree representation is transformed into a series of calls similar to the above. The sequence numbers are generated dynamically, and the functions cond and forEach ensure that they remain consistent.

Unfortunately, this has a performance cost, as we allocate the DOM tree union before every render.

The opportunity

Instead of using lists of attributes and elements to build a DOM tree, another possibility would be to use a computation expression to directly build up the sequence of builder calls. The Node and Attr types would be delegates that take a RenderTreeBuilder. If this is done naively, it’s not really more efficient than the union way; it just allocates a bunch of lambdas instead of a bunch of union values.

However, F# 6’s [<InlineIfLambda>] changes this. By making very liberal use of this attribute in the computation expression builder, it is possible to entirely flatten the generated nested lambdas, and end up with completely linear IL, just like C#!

Here is a very basic proof of concept. With it, the following code:

div {
    text "Welcome to "
    a {
        attr.href "https://fsbolero.io"
        attr.id "link"
        text "Bolero"
    }
    text "!"
}

is decompiled by ILSpy into the following C#:

// Program.d@107
internal static int Invoke(RenderTreeBuilder tb, int i)
{
	tb.OpenElement(i, div.name);
	int num = i + 1;
	tb.AddContent(num, "Welcome to ");
	int num2 = num + 1;
	tb.OpenElement(num2, a.name);
	int num3 = num2 + 1;
	tb.AddAttribute(num3, "href", "https://fsbolero.io");
	int num4 = num3 + 1;
	tb.AddAttribute(num4, "id", "link");
	int num5 = num4 + 1;
	tb.AddContent(num5, "Bolero");
	int num6 = num5 + 1;
	tb.CloseElement();
	int num7 = num6;
	tb.AddContent(num7, "!");
	int result = num7 + 1;
	tb.CloseElement();
	return result;
}

It still uses a dynamically generated sequence number; that is inevitable without compile-time code generation. But it is completely linear and doesn’t allocate anything more than the equivalent Razor, which is a huge improvement!

Issue Analytics

  • State:closed
  • Created 2 years ago
  • Reactions:39
  • Comments:6 (3 by maintainers)

github_iconTop GitHub Comments

1reaction
Tarmilcommented, Jan 26, 2022

Assuming the switch, do you expect issues with the templating TP?

That’s a good question. IIRC TPs are expanded pretty late in the compiler pipeline, so I’m not sure that the inlining will go so well. We need to investigate it.

0reactions
Tarmilcommented, May 15, 2022

Released in v0.20.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Evaluate computation expression-based HTML for better ...
TL;DR: F# 6 makes it possible to use computation expressions for HTML and have much better performance than the current list-based syntax.
Read more >
Rendering on the Web
When deciding on an approach to rendering, measure and understand what your bottlenecks are. Consider whether static rendering or server-side ...
Read more >
The Basics of Page Speed
Learn the basics of how page speed works so you can better understand what to optimize for the overall user experience in this...
Read more >
Analyze runtime performance - Chrome Developers
Learn how to evaluate runtime performance in Chrome DevTools.
Read more >
Expression language reference
The performance of evaluating expressions during renders can be up to 5x faster than the Legacy ExtendScript engine. On Windows, After Effects ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found