Skip to main content

Benchmarks

These benchmarks show typical render times, compare FSharp.ViewEngine with other F# view engines, and provide the commands needed to reproduce the results.

On this page

Overview

These benchmarks show typical render times, compare FSharp.ViewEngine with other F# view engines, and provide the commands needed to reproduce the results.

Typical Render Times

The representative page is a complete HTML document with metadata, navigation, content, lists, a form, a table, and a footer.

  • Build and render: 1.585 μs for the representative page.
  • Render only: 833.5 ns when the representative page is already built.
  • Large response: 0.23 ms to build and render 1,000 repeated articles.

These are renderer operations, not HTTP requests per second. Routing, application work, I/O, concurrency, and response transport are not included.

How It Compares

For the representative build-and-render workload, the other engines take 1.35× to 2.35× as long as FSharp.ViewEngine. All four remain fast in absolute terms.

Typical dynamic page: build and render

Mean duration per operation. Shorter bars are faster; values are normalized against the slowest result.

Mean duration · Lower is better
FSharp.ViewEngine1.585 μs
baseline
Oxpecker.ViewEngine2.147 μs
1.35× as long
Giraffe.ViewEngine2.649 μs
1.67× as long
Feliz.ViewEngine3.723 μs
2.35× as long

How the Benchmarks Were Run

The current results were measured on August 6, 2026 with BenchmarkDotNet 0.15.8, .NET SDK 10.0.201, .NET runtime 10.0.5, macOS 26.4.1, and an Apple M5 Max Arm64 processor.

  • Process isolation — every benchmark executes in a generated benchmark process rather than inside the runner.
  • Repeated measurement — MediumRun uses two launches, ten warmups, and fifteen measured iterations.
  • Bounded iteration time — a 100 ms target avoids multi-gigabyte allocation pressure in the fastest render-only cases.
  • Render-only setup — documents are constructed once before warmup and measurement, keeping construction work out of the render loop.
  • Reported values — tables show the arithmetic mean and managed allocation per operation; lower is better.

The comparison suite uses the latest stable releases checked for this measurement: Oxpecker.ViewEngine 2.0.1, Giraffe.ViewEngine 1.4.0, and Feliz.ViewEngine 1.0.3. The runner prints resolved package versions with every execution. Results are representative measurements, not CI regression thresholds.

How to Run the Benchmarks

Run commands from the solution directory. The FAKE targets forward trailing BenchmarkDotNet options, including filters. BenchmarkSmoke executes selected cases once in isolated processes to validate the suite without producing stable measurements.

cd sln

# Run the complete measurement suite.
./fake.sh Benchmark

# List or target benchmark cases.
./fake.sh Benchmark --list flat
./fake.sh Benchmark --filter '*Benchmarks.RenderOnly.*'

# Validate every case, or a filtered subset, once.
./fake.sh BenchmarkSmoke
./fake.sh BenchmarkSmoke --filter '*AttributeEncodingBenchmarks*'

Appendix: Detailed Results

The following tables preserve the raw mean and managed-allocation results behind the analysis above.

Comparison: Build and Render

MethodMeanAllocated
FSharp.ViewEngine1.585 μs11.39 KB
Oxpecker.ViewEngine2.147 μs12.88 KB
Giraffe.ViewEngine2.649 μs23.94 KB
Feliz.ViewEngine3.723 μs25.87 KB

Comparison: Render Only

MethodMeanAllocated
FSharp.ViewEngine833.5 ns2.93 KB
Oxpecker.ViewEngine911.4 ns2.93 KB
Giraffe.ViewEngine989.6 ns12.77 KB
Feliz.ViewEngine1,872.9 ns14.2 KB

Comparison: Build Only

MethodMeanAllocated
FSharp.ViewEngine670.1 ns8.46 KB
Oxpecker.ViewEngine1,181.0 ns9.95 KB
Giraffe.ViewEngine1,654.9 ns11.17 KB
Feliz.ViewEngine1,782.9 ns11.66 KB

Attribute Encoding

ValueMeanAllocated
Plain36.17 ns280 B
Encoded81.92 ns496 B

Inline and Overflow Storage

ShapeCountMeanAllocated
Attributes026.43 ns200 B
Attributes133.16 ns216 B
Attributes241.23 ns240 B
Attributes8108.42 ns744 B
Children018.47 ns160 B
Children135.08 ns320 B
Children252.22 ns488 B
Children8187.57 ns1,648 B

Equivalent Collection Inputs

CollectionMeanAllocated
Array451.7 ns3.45 KB
List437.7 ns3.45 KB
Sequence482.8 ns3.53 KB

Document Workloads

WorkloadBuild + renderAllocationRender onlyAllocation
Small fragment72.92 ns680 B51.05 ns296 B
Representative page1,538.00 ns11,664 B813.40 ns3,000 B
Deeply nested2,288.68 ns12,096 B1,069.54 ns3,256 B
Large response228,746.00 ns1,252,539 B77,196.10 ns283,768 B

Profiling Findings

  • Build-only CPU samples are dominated by TagBuilder.Run and generated computation-expression methods; sampled allocations are DOM nodes and overflow collections rather than closure objects.
  • Render-only allocations are almost entirely the required returned System.String.
  • Optimized ARM64 JIT output retains indirect child-render calls, but profiling does not identify dispatch as a dominant cost.
  • General sequence input adds about 80 bytes and modest runtime overhead, which does not justify collection-specific overloads.
  • Measurements continue to support inline storage for zero, one, and two attributes or children.
  • The renderer retains at most one thread-static string builder no larger than 256K characters, bounding retained memory without regressing the representative large response.