How does this work?
Fun.Blazor provides a series of delegates for Blazor to handle. For example, when you write:
let demo =
div {
class' "cool"
}
This code essentially becomes:
let demo =
NodeRenderFragment(fun comp builder index -> // delegate
builder.OpenElement(index, "div")
bulder.AddAttribute(index + 1, "class", "cool")
builer.CloseElement()
index + 2
)
type NodeRenderFragment = delegate of root: IComponent * builder: RenderTreeBuilder * sequence: int -> int
In essence, you have created a delegate that will be passed to a component, which will manage the rendering or building of the DOM tree. This approach is similar to what the Razor engine would generate in the C# world.
Components can be created using adaptiview
, html.inject
, etc. These components are normal Blazor components that inherit from ComponentBase
.
Considerations before using Fun.Blazor:
There are a few things to keep in mind:
The F# compiler has performance issues with intellisense for some large computation expressions (CEs). It is better to keep single CE blocks, or use sequences like array
with childContent
for better intellisense. issue tracked in fsharp repo.
There are some tests in here, in summary, below are some recommend ways for better build time performance (but it can reduce runtime performance because we cannot inline and need to allocate memory on head for creating array or list)
The best result is list-with-local-vars for multiple child items
let demo1 = div {
class' "font-bold"
"demo1"
}
let demo2 = div {
class' "font-bold"
"demo2"
}
let comp = div {
style { color "red" }
childContent [| // 👌✅
demo1
demo2
|]
}
But you can also write like below even it will not build as fast as the above:
let comp = div {
style { color "red" }
childContent [| // 👌✅
div {
class' "font-bold"
"demo1"
}
div {
class' "font-bold"
"demo2"
}
|]
}
nested-one is kind of ok
let comp = div {
class' "font-bold"
div { // 👌✅
class' "font-bold"
"demo1"
}
}
but still prefer childContent:
let comp = div {
class' "font-bold"
childContet (div { // 👌✅✅
class' "font-bold"
"demo1"
})
}
nested-one-one is not ok (bad for build perf)
let comp = div {
class' "font-bold"
div {
class' "font-bold"
div { // ⛔🙅
class' "font-bold"
"demo1"
}
}
}
Write like below:
let comp = div {
class' "font-bold"
div {
class' "font-bold"
childContent [| // 👌✅
div {
class' "font-bold"
"demo1"
}
|]
}
}
inline local vars is not ok (bad for build perf)
let comp = div {
class' "font-bold"
let temp = div { // ⛔🙅
class' "font-bold"
"demo1"
}
temp
}
Hot-reload
The default templates provide limited hot-reload support. Too many files can slow down the hot-reload process, so for best results, add // hot-reload
at the top of files you want to enable hot-reload for. For more information, see the Hot-reload in Fun.Blazor blog post or document.
Attribute, items position in CE
When using a ref
or renderMode
attribute etc., you should place it like below, because blazor treat them very special and can only add them after other attributes:
div {
attributes ...
ref (fun x -> ()) // ✅
childContent [| ... |]
}
Or:
div {
attributes ...
ref (fun x -> ()) // ✅
div { 1 }
}
Benchmarks
BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3007/23H2/2023Update/SunValley3)
12th Gen Intel Core i7-12700H, 1 CPU, 20 logical and 14 physical cores
.NET SDK 8.0.100
[Host] : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2 DEBUG
DefaultJob : .NET 8.0.1 (8.0.123.58001), X64 RyuJIT AVX2
Method |
Mean |
Error |
StdDev |
Ratio |
RatioSD |
Gen0 |
Allocated |
Alloc Ratio |
RenderWithRazorCSharp |
237.0 ns |
4.62 ns |
7.46 ns |
1.00 |
0.00 |
0.0296 |
376 B |
1.00 |
RenderWithFunBlazorInlineCE |
372.5 ns |
7.26 ns |
9.94 ns |
1.58 |
0.07 |
0.0443 |
560 B |
1.49 |
RenderWithFunBlazorArray |
518.8 ns |
10.21 ns |
14.64 ns |
2.20 |
0.07 |
0.1154 |
1448 B |
3.85 |
RenderWithBolero |
538.5 ns |
10.59 ns |
19.89 ns |
2.27 |
0.10 |
0.1173 |
1480 B |
3.94 |