Skip to content

Commit dae37f6

Browse files
authored
Merge pull request #29 from dbrattli/perf/optimize-rendering
perf: optimize HTML rendering for 35-55% faster performance
2 parents c499549 + eb460a7 commit dae37f6

8 files changed

Lines changed: 345 additions & 99 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ examples/giraffe/obj
66
bin
77
obj
88
.vs
9+
BenchmarkDotNet.Artifacts/

Feliz.ViewEngine.sln

Lines changed: 83 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,83 @@
1-
2-
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio 15
4-
VisualStudioVersion = 15.0.26124.0
5-
MinimumVisualStudioVersion = 15.0.26124.0
6-
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Feliz.ViewEngine", "src\Feliz.ViewEngine.fsproj", "{B9BCF160-840B-449C-B6D8-07909A4161EB}"
7-
EndProject
8-
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Tests-Feliz.ViewEngine", "test\Tests.Feliz.ViewEngine.fsproj", "{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}"
9-
EndProject
10-
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Feliz.Bulma.ViewEngine", "Feliz.Bulma.ViewEngine\Feliz.Bulma.ViewEngine.fsproj", "{69A365F4-9078-4421-8ABA-0D6E5B6A030E}"
11-
EndProject
12-
Global
13-
GlobalSection(SolutionConfigurationPlatforms) = preSolution
14-
Debug|Any CPU = Debug|Any CPU
15-
Debug|x64 = Debug|x64
16-
Debug|x86 = Debug|x86
17-
Release|Any CPU = Release|Any CPU
18-
Release|x64 = Release|x64
19-
Release|x86 = Release|x86
20-
EndGlobalSection
21-
GlobalSection(SolutionProperties) = preSolution
22-
HideSolutionNode = FALSE
23-
EndGlobalSection
24-
GlobalSection(ProjectConfigurationPlatforms) = postSolution
25-
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
26-
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
27-
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Debug|x64.ActiveCfg = Debug|Any CPU
28-
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Debug|x64.Build.0 = Debug|Any CPU
29-
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Debug|x86.ActiveCfg = Debug|Any CPU
30-
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Debug|x86.Build.0 = Debug|Any CPU
31-
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
32-
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Release|Any CPU.Build.0 = Release|Any CPU
33-
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Release|x64.ActiveCfg = Release|Any CPU
34-
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Release|x64.Build.0 = Release|Any CPU
35-
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Release|x86.ActiveCfg = Release|Any CPU
36-
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Release|x86.Build.0 = Release|Any CPU
37-
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
38-
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Debug|Any CPU.Build.0 = Debug|Any CPU
39-
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Debug|x64.ActiveCfg = Debug|Any CPU
40-
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Debug|x64.Build.0 = Debug|Any CPU
41-
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Debug|x86.ActiveCfg = Debug|Any CPU
42-
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Debug|x86.Build.0 = Debug|Any CPU
43-
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Release|Any CPU.ActiveCfg = Release|Any CPU
44-
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Release|Any CPU.Build.0 = Release|Any CPU
45-
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Release|x64.ActiveCfg = Release|Any CPU
46-
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Release|x64.Build.0 = Release|Any CPU
47-
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Release|x86.ActiveCfg = Release|Any CPU
48-
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Release|x86.Build.0 = Release|Any CPU
49-
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
50-
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Debug|Any CPU.Build.0 = Debug|Any CPU
51-
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Debug|x64.ActiveCfg = Debug|Any CPU
52-
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Debug|x64.Build.0 = Debug|Any CPU
53-
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Debug|x86.ActiveCfg = Debug|Any CPU
54-
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Debug|x86.Build.0 = Debug|Any CPU
55-
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Release|Any CPU.ActiveCfg = Release|Any CPU
56-
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Release|Any CPU.Build.0 = Release|Any CPU
57-
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Release|x64.ActiveCfg = Release|Any CPU
58-
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Release|x64.Build.0 = Release|Any CPU
59-
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Release|x86.ActiveCfg = Release|Any CPU
60-
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Release|x86.Build.0 = Release|Any CPU
61-
EndGlobalSection
62-
EndGlobal
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio 15
4+
VisualStudioVersion = 15.0.26124.0
5+
MinimumVisualStudioVersion = 15.0.26124.0
6+
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Feliz.ViewEngine", "src\Feliz.ViewEngine.fsproj", "{B9BCF160-840B-449C-B6D8-07909A4161EB}"
7+
EndProject
8+
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Tests.Feliz.ViewEngine", "test\Tests.Feliz.ViewEngine.fsproj", "{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}"
9+
EndProject
10+
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Feliz.Bulma.ViewEngine", "Feliz.Bulma.ViewEngine\Feliz.Bulma.ViewEngine.fsproj", "{69A365F4-9078-4421-8ABA-0D6E5B6A030E}"
11+
EndProject
12+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{66320409-64EC-F7C5-3DEF-65E7510DAAD1}"
13+
EndProject
14+
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Benchmarks", "benchmarks\Benchmarks.fsproj", "{28B368AD-E461-423E-8341-454A31D95927}"
15+
EndProject
16+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
17+
EndProject
18+
Global
19+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
20+
Debug|Any CPU = Debug|Any CPU
21+
Debug|x64 = Debug|x64
22+
Debug|x86 = Debug|x86
23+
Release|Any CPU = Release|Any CPU
24+
Release|x64 = Release|x64
25+
Release|x86 = Release|x86
26+
EndGlobalSection
27+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
28+
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29+
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Debug|Any CPU.Build.0 = Debug|Any CPU
30+
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Debug|x64.ActiveCfg = Debug|Any CPU
31+
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Debug|x64.Build.0 = Debug|Any CPU
32+
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Debug|x86.ActiveCfg = Debug|Any CPU
33+
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Debug|x86.Build.0 = Debug|Any CPU
34+
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Release|Any CPU.ActiveCfg = Release|Any CPU
35+
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Release|Any CPU.Build.0 = Release|Any CPU
36+
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Release|x64.ActiveCfg = Release|Any CPU
37+
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Release|x64.Build.0 = Release|Any CPU
38+
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Release|x86.ActiveCfg = Release|Any CPU
39+
{B9BCF160-840B-449C-B6D8-07909A4161EB}.Release|x86.Build.0 = Release|Any CPU
40+
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
41+
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Debug|Any CPU.Build.0 = Debug|Any CPU
42+
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Debug|x64.ActiveCfg = Debug|Any CPU
43+
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Debug|x64.Build.0 = Debug|Any CPU
44+
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Debug|x86.ActiveCfg = Debug|Any CPU
45+
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Debug|x86.Build.0 = Debug|Any CPU
46+
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Release|Any CPU.ActiveCfg = Release|Any CPU
47+
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Release|Any CPU.Build.0 = Release|Any CPU
48+
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Release|x64.ActiveCfg = Release|Any CPU
49+
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Release|x64.Build.0 = Release|Any CPU
50+
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Release|x86.ActiveCfg = Release|Any CPU
51+
{477BDCE1-C00F-4A37-B52C-C2BC9AF05D57}.Release|x86.Build.0 = Release|Any CPU
52+
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
53+
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Debug|Any CPU.Build.0 = Debug|Any CPU
54+
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Debug|x64.ActiveCfg = Debug|Any CPU
55+
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Debug|x64.Build.0 = Debug|Any CPU
56+
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Debug|x86.ActiveCfg = Debug|Any CPU
57+
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Debug|x86.Build.0 = Debug|Any CPU
58+
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Release|Any CPU.ActiveCfg = Release|Any CPU
59+
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Release|Any CPU.Build.0 = Release|Any CPU
60+
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Release|x64.ActiveCfg = Release|Any CPU
61+
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Release|x64.Build.0 = Release|Any CPU
62+
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Release|x86.ActiveCfg = Release|Any CPU
63+
{69A365F4-9078-4421-8ABA-0D6E5B6A030E}.Release|x86.Build.0 = Release|Any CPU
64+
{28B368AD-E461-423E-8341-454A31D95927}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
65+
{28B368AD-E461-423E-8341-454A31D95927}.Debug|Any CPU.Build.0 = Debug|Any CPU
66+
{28B368AD-E461-423E-8341-454A31D95927}.Debug|x64.ActiveCfg = Debug|Any CPU
67+
{28B368AD-E461-423E-8341-454A31D95927}.Debug|x64.Build.0 = Debug|Any CPU
68+
{28B368AD-E461-423E-8341-454A31D95927}.Debug|x86.ActiveCfg = Debug|Any CPU
69+
{28B368AD-E461-423E-8341-454A31D95927}.Debug|x86.Build.0 = Debug|Any CPU
70+
{28B368AD-E461-423E-8341-454A31D95927}.Release|Any CPU.ActiveCfg = Release|Any CPU
71+
{28B368AD-E461-423E-8341-454A31D95927}.Release|Any CPU.Build.0 = Release|Any CPU
72+
{28B368AD-E461-423E-8341-454A31D95927}.Release|x64.ActiveCfg = Release|Any CPU
73+
{28B368AD-E461-423E-8341-454A31D95927}.Release|x64.Build.0 = Release|Any CPU
74+
{28B368AD-E461-423E-8341-454A31D95927}.Release|x86.ActiveCfg = Release|Any CPU
75+
{28B368AD-E461-423E-8341-454A31D95927}.Release|x86.Build.0 = Release|Any CPU
76+
EndGlobalSection
77+
GlobalSection(SolutionProperties) = preSolution
78+
HideSolutionNode = FALSE
79+
EndGlobalSection
80+
GlobalSection(NestedProjects) = preSolution
81+
{28B368AD-E461-423E-8341-454A31D95927} = {66320409-64EC-F7C5-3DEF-65E7510DAAD1}
82+
EndGlobalSection
83+
EndGlobal

benchmarks/Benchmarks.fsproj

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project Sdk="Microsoft.NET.Sdk">
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net9.0</TargetFramework>
6+
</PropertyGroup>
7+
<ItemGroup>
8+
<ProjectReference Include="../src/Feliz.ViewEngine.fsproj" />
9+
</ItemGroup>
10+
<ItemGroup>
11+
<Compile Include="HtmlBenchmarks.fs" />
12+
<Compile Include="Program.fs" />
13+
</ItemGroup>
14+
<ItemGroup>
15+
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
16+
</ItemGroup>
17+
</Project>

benchmarks/HtmlBenchmarks.fs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
module Feliz.ViewEngine.Benchmarks.HtmlBenchmarks
2+
3+
open BenchmarkDotNet.Attributes
4+
open Feliz.ViewEngine
5+
6+
/// Benchmarks matching the tests from hamy.xyz blog posts
7+
[<MemoryDiagnoser>]
8+
[<RankColumn>]
9+
type HtmlRenderBenchmarks() =
10+
11+
/// Simple element - baseline
12+
[<Benchmark>]
13+
member _.SimpleElement() =
14+
Html.div [ prop.className "test" ]
15+
|> Render.htmlView
16+
17+
/// Element with text content
18+
[<Benchmark>]
19+
member _.ElementWithText() =
20+
Html.p [
21+
prop.className "paragraph"
22+
prop.text "Hello, World!"
23+
]
24+
|> Render.htmlView
25+
26+
/// Element with styles
27+
[<Benchmark>]
28+
member _.ElementWithStyles() =
29+
Html.div [
30+
prop.style [
31+
style.color "red"
32+
style.fontSize 16
33+
style.margin 10
34+
style.padding 5
35+
]
36+
prop.text "Styled"
37+
]
38+
|> Render.htmlView
39+
40+
/// Text that needs escaping
41+
[<Benchmark>]
42+
member _.TextEscaping() =
43+
Html.p [
44+
prop.text "Hello <world> & \"friends\" it's great"
45+
]
46+
|> Render.htmlView
47+
48+
/// Text that doesn't need escaping
49+
[<Benchmark>]
50+
member _.TextNoEscaping() =
51+
Html.p [
52+
prop.text "Hello world this is plain text without special characters"
53+
]
54+
|> Render.htmlView
55+
56+
57+
/// Long page benchmarks - similar to hamy.xyz flat/shallow benchmarks
58+
[<MemoryDiagnoser>]
59+
[<RankColumn>]
60+
type LongPageBenchmarks() =
61+
62+
let createItem i =
63+
Html.div [
64+
prop.className "item"
65+
prop.children [
66+
Html.span [ prop.text $"Item {i}" ]
67+
Html.p [ prop.text "Description text here" ]
68+
]
69+
]
70+
71+
[<Params(10, 100, 1000)>]
72+
member val ItemCount = 0 with get, set
73+
74+
[<Benchmark>]
75+
member this.LongFlatPage() =
76+
Html.div [
77+
prop.className "container"
78+
prop.children [
79+
for i in 1 .. this.ItemCount do
80+
createItem i
81+
]
82+
]
83+
|> Render.htmlView
84+
85+
86+
/// Deeply nested benchmarks - similar to hamy.xyz nested benchmarks
87+
[<MemoryDiagnoser>]
88+
[<RankColumn>]
89+
type DeeplyNestedBenchmarks() =
90+
91+
let rec createNestedDiv depth =
92+
if depth <= 0 then
93+
Html.span [ prop.text "Leaf" ]
94+
else
95+
Html.div [
96+
prop.className $"level-{depth}"
97+
prop.children [ createNestedDiv (depth - 1) ]
98+
]
99+
100+
[<Params(10, 100, 500)>]
101+
member val Depth = 0 with get, set
102+
103+
[<Benchmark>]
104+
member this.DeeplyNested() =
105+
createNestedDiv this.Depth
106+
|> Render.htmlView
107+
108+
109+
/// Table rendering benchmark - common real-world scenario
110+
[<MemoryDiagnoser>]
111+
[<RankColumn>]
112+
type TableBenchmarks() =
113+
114+
[<Params(10, 100, 500)>]
115+
member val RowCount = 0 with get, set
116+
117+
[<Benchmark>]
118+
member this.TableRendering() =
119+
Html.table [
120+
Html.thead [
121+
Html.tr [
122+
Html.th "ID"
123+
Html.th "Name"
124+
Html.th "Email"
125+
Html.th "Status"
126+
]
127+
]
128+
Html.tbody [
129+
for i in 1 .. this.RowCount do
130+
Html.tr [
131+
Html.td $"{i}"
132+
Html.td $"User {i}"
133+
Html.td $"user{i}@example.com"
134+
Html.td "Active"
135+
]
136+
]
137+
]
138+
|> Render.htmlView

benchmarks/Program.fs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module Feliz.ViewEngine.Benchmarks.Program
2+
3+
open BenchmarkDotNet.Running
4+
open Feliz.ViewEngine.Benchmarks.HtmlBenchmarks
5+
6+
[<EntryPoint>]
7+
let main args =
8+
// Run all benchmarks
9+
BenchmarkSwitcher
10+
.FromAssembly(typeof<HtmlRenderBenchmarks>.Assembly)
11+
.Run(args)
12+
|> ignore
13+
0

justfile

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,16 @@
44
src_path := "src"
55
test_path := "test"
66
bulma_path := "Feliz.Bulma.ViewEngine"
7+
bench_path := "benchmarks"
78

89
# Default recipe - show available commands
910
default:
1011
@just --list
1112

1213
# Clean build artifacts
1314
clean:
14-
rm -rf {{src_path}}/obj {{test_path}}/obj {{bulma_path}}/obj
15-
rm -rf {{src_path}}/bin {{test_path}}/bin {{bulma_path}}/bin
15+
rm -rf {{src_path}}/obj {{test_path}}/obj {{bulma_path}}/obj {{bench_path}}/obj
16+
rm -rf {{src_path}}/bin {{test_path}}/bin {{bulma_path}}/bin {{bench_path}}/bin
1617

1718
# Restore all dependencies
1819
restore:
@@ -23,6 +24,10 @@ restore:
2324
build:
2425
dotnet build --configuration Release
2526

27+
# Build benchmarks
28+
build-bench:
29+
dotnet build {{bench_path}} -c Release
30+
2631
# Run all tests
2732
test:
2833
dotnet test {{test_path}}
@@ -40,3 +45,15 @@ pack-version version:
4045

4146
# Full setup and test
4247
setup: restore build test
48+
49+
# Run benchmarks (all benchmark classes)
50+
bench:
51+
dotnet run -c Release --project {{bench_path}} -- --filter '*'
52+
53+
# Run specific benchmark class (e.g., just bench-class LongPageBenchmarks)
54+
bench-class class:
55+
dotnet run -c Release --project {{bench_path}} -- --filter '*{{class}}*'
56+
57+
# Run quick benchmarks (fewer iterations, for development)
58+
bench-quick:
59+
dotnet run -c Release --project {{bench_path}} -- --filter '*' --job short

0 commit comments

Comments
 (0)