← Back to list

Blazor vs Oxpecker

I’ve recently released an Oxpecker version of the standard (out of the box) Blazor Web App template. Let’s try to compare those templates…

Vladimir Shchur · 2024-03-05 19:00 · 11 claps · 7.0 min read
#blazor #aspnetcore #fsharp #server-side-rendering #oxpeckers
Open on Medium ↗
Wiki topics: 🌐 · Web Development

Blazor vs Oxpecker

Weather app

Weather app

I’ve recently released an Oxpecker version of the standard (out of the box) Blazor Web App template. Let’s try to compare those templates and see pros and cons of each.

Structural differences

Create new project window

Create new project window

First, let’s create and explore the default Blazor template:

Blazor project structure

Blazor project structure

Blazor template includes the following features: Razor Pages, Bootstrap, launchSettings.json, appsettings.json, _Imports.razor, scoped css files and Interactive Server rendering mode.

Now, let’s have a look at a standard Blazor page Counter.razor:

@page "/counter"
@rendermode InteractiveServer

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;
    private void IncrementCount()
    {
        // click is executed on server (!)
        currentCount++;
    }
}

You can see a mix of special directives, custom components, Razor syntax with Blazor extensions (like @onclick ) and a block of C# code. This can also include some special attributes and lifecycle method overrides like in Error.razor file.

@code{
    [CascadingParameter] private HttpContext? HttpContext { get; set; }

    private string? RequestId { get; set; }
    private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);

    protected override void OnInitialized() =>
        RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;

}

Now, let’s look at how this template looks in Oxpecker:

Oxpecker app structure

Oxpecker app structure

You can see the following changes compared to the Blazor template: no launchSettings, no appSettings, no scoped CSS, no global imports, but much more simple structure. The former two features were dropped because I personally never found them beneficial, quite the opposite — they make local development environment and experience different than production, which creates issues for CI/CD process and overall production support. The latter two features are just not supported.

In place of Razor pages there is a simple route map inside Program.fs file, which should remind you of the old good MVC routing:

let endpoints = [
    GET [
        route "/" <| htmlView' home.html
        route "/counter" <| htmlView' counter.html
        route "/weather" <| htmlView' weather.html
        route "/weatherData" <| getWeatherData
        route "/error" <| htmlView' error.html
    ]
]

Every html view file is just a a regular F# code, for example counter.fs:

module WeatherApp.templates.counter

open Microsoft.AspNetCore.Http
open Oxpecker.ViewEngine
open Oxpecker.ViewEngine.Aria

let html (ctx: HttpContext) =
    ctx.Items["Title"] <- "Counter"

    div().attr("x-data", "{ currentCount: 0 }") {
        h1() { "Counter" }
        p(role="status").attr("x-text", "'CurrentCount: ' + currentCount")
        button(class'="btn btn-primary").attr("x-on:click", "currentCount++") { 
            "Click me" 
        }
    }

or weather.fs

let html (ctx: HttpContext) =
    ctx.Items["Title"] <- "Weather"

    Fragment(){
        h1() { "Weather" }
        p() { "This component demonstrates showing data." }
        p(hxGet="/weatherData", hxTrigger="load", hxSwap="outerHTML"){
            em() { "Loading..." }
        }
    }

There are some things to note here:

  1. Oxpecker.ViewEngine provides nice DSL for writing HTML
  2. Counter.fs file uses Alpine library to execute clicks and apply changes on client side
  3. Weather.fs file uses Htmx library to do the “heavy” call to the server outside of rendering main page and replace loading stub with the result
  4. HttpContext is passed explicitly rather than implicitly
  5. Title is passes explicitly as well and then used in layout.fs

One more difference can be found in NavMenu.razor and layout.navMenu function respectively. In C# built-in custom component is used, which sets active class to the link matching current URL:

<NavLink class="nav-link" href="counter">
    <span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
</NavLink>

while in F# it’s a normal function (also with explicit HttpContext parameter)

let navLink (attrs: {| Href: string; Class: string; Ctx: HttpContext|}) =
    let finalClass =
        if attrs.Href = attrs.Ctx.Request.Path then
            attrs.Class + " active"
        else
            attrs.Class
    a(href = attrs.Href, class' = finalClass)
navLink {| Class="nav-link"; Href="/counter"; Ctx=ctx |} {
    span(class'="bi bi-plus-square-fill-nav-menu", ariaHidden=true)
    " Counter"
}

This is probably the main benefit of choosing the Oxpecker framework and it’s DSL over Razor or any other non language native templating technology — everything is extremely flexible and composable and you don’t have to study documentation to find an extension point to embed your custom component or middleware.

The last difference I need to mention is scoped CSS. While Blazor does support it, it also requires to account for non-standard pseudo elements. With Oxpecker you need to either be creative and careful when adding styles to app.css (and using BEM technology or similar) or use modern CSS solutions like tailwind or css-scope-inline.

Behavioral differences

When you navigate between Home and Counter pages you have a feeling of a single page application, since page is not reloaded, and it’s the same for both frameworks.

However if you explore the developer tools (Network tab) and requests happening at this transition, you’ll see that the full page is rendered and returned to the client for both frameworks with additional 2 requests for Blazor:

Network tab result

Network tab result

Those requests are made for purpose - since counter button clicks are not calculated on client side, every click results in a WebSocket message to server and then re-rendering after server calculates the result. This can sound strange, but this is how Blazor server worked since it’s inception. And this of course means that the button (and the whole site) stops working once client or server gets disconnected. So, the first additional request asks for the available transport options, and once the server confirms the availability of WebSockets, the WebSocket connection is initiated with the second additional request.

Button clicks in Oxpecker template are all handled on client side (Apline.js handles it) and page change doesn’t lead to a reload due to the Htmx framework (and it’s magic hx-boost attribute). Just as Blazor it extracts body from the response and substitutes current page body (and title) which gives an experience of SPA framework.

One more difference is that Blazor server returns responses in chunked encoding and doesn’t return Content-Length header. The main idea is that it moves the burden of dynamic data buffering from server to client and has both pros (reducing server load) and cons (partial content problems). Still, Blazor template actively leverages it on Weather.razor page by using special StreamRendering attribute while Oxpecker template simply does Ajax call on page load.

Performance

Any comparison post would be not complete without performance test, especially in .NET space. I’ve used Autocannon testing tool (since it’s very easy to install) and measured the speed of server responding at the root of the website.

Blazor:

Running 10s test @ http://localhost:5164/
10 connections

┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬───────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev   │ Max   │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼───────┤
│ Latency │ 0 ms │ 0 ms │ 0 ms  │ 1 ms │ 0.03 ms │ 0.26 ms │ 19 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴───────┘
┌───────────┬────────┬────────┬─────────┬─────────┬───────────┬──────────┬────────┐
│ Stat      │ 1%     │ 2.5%   │ 50%     │ 97.5%   │ Avg       │ Stdev    │ Min    │
├───────────┼────────┼────────┼─────────┼─────────┼───────────┼──────────┼────────┤
│ Req/Sec   │ 11,959 │ 11,959 │ 16,103  │ 16,479  │ 15,712.73 │ 1,250.71 │ 11,958 │
├───────────┼────────┼────────┼─────────┼─────────┼───────────┼──────────┼────────┤
│ Bytes/Sec │ 38 MB  │ 38 MB  │ 51.2 MB │ 52.4 MB │ 49.9 MB   │ 3.97 MB  │ 38 MB  │
└───────────┴────────┴────────┴─────────┴─────────┴───────────┴──────────┴────────┘

Req/Bytes counts sampled once per second.
# of samples: 11

173k requests in 11.03s, 549 MB read

Oxpecker:

Running 10s test @ http://localhost:5000/
10 connections

┌─────────┬──────┬──────┬───────┬──────┬─────────┬─────────┬───────┐
│ Stat    │ 2.5% │ 50%  │ 97.5% │ 99%  │ Avg     │ Stdev   │ Max   │
├─────────┼──────┼──────┼───────┼──────┼─────────┼─────────┼───────┤
│ Latency │ 0 ms │ 0 ms │ 0 ms  │ 0 ms │ 0.01 ms │ 0.13 ms │ 14 ms │
└─────────┴──────┴──────┴───────┴──────┴─────────┴─────────┴───────┘
┌───────────┬─────────┬─────────┬─────────┬────────┬──────────┬──────────┬─────────┐
│ Stat      │ 1%      │ 2.5%    │ 50%     │ 97.5%  │ Avg      │ Stdev    │ Min     │
├───────────┼─────────┼─────────┼─────────┼────────┼──────────┼──────────┼─────────┤
│ Req/Sec   │ 16,831  │ 16,831  │ 19,903  │ 21,375 │ 19,345.6 │ 1,463.83 │ 16,822  │
├───────────┼─────────┼─────────┼─────────┼────────┼──────────┼──────────┼─────────┤
│ Bytes/Sec │ 29.1 MB │ 29.1 MB │ 34.4 MB │ 37 MB  │ 33.4 MB  │ 2.53 MB  │ 29.1 MB │
└───────────┴─────────┴─────────┴─────────┴────────┴──────────┴──────────┴─────────┘

Req/Bytes counts sampled once per second.
# of samples: 10

193k requests in 10.03s, 334 MB read

As you can see, Oxpecker is 18.5% faster on my machine which is not a huge difference (since they are both running on the same ASP.NET Core framework), but also Blazor shows 83% overhead in traffic size due to the additional “server state” passed along, so you should take it into account if you are paying for traffic size.

<script src="_framework/blazor.web.js"></script></body></html><!--Blazor-Server-Component-State:CfDJ8C/SBSRVJlJCuhXYhC8y1K7cpUtYvlnXKv4XYWeMNGfu3OLe6AR+y5Kt5YSzX4j6DAkzyB+lzojP2gSpeQFMxZyAXGlQRTS+s2G9MYu/Lv2a0IHra9YI2fbihsAzD8/OFHj799Ge5avT1WqMjEmeCpSao62VdRUGLCKdneQvN3kcnuEl6/xkyEXSmbruZ3tjB1XO9ECZ9RA+4z4JiRl2mJjTywMelUGbv6Ubi+MClALmsUlGnNjCeTaoEDj8J0RTyyalDG6pFbcqNuKLsHWNdGW5DTWnG4bQFzcIYCdP6hXI5ke+Lj7XtQjazPOi8l/jMewnkUc8TCkXMCCvuY8CYV48/QkWTy57WQQL8aG+qBV1jAObaf18BvYwI5NgUNiFZoyYWhnp7Jc3PrJMx9Ie1cB8lIirOE6c/PReI9VYjq7sBnuZDPJc1IDKWg+iSMxU+JkdbNGsFkU6Vbnu39m4wiQRf4NTcgn4ZqQxyqq6hC0vyR9pP3nCsfkQ9Pa8xUrY88zS/hbogmbaFdFWeloBovLa3dZt3zU0owHmMe+rzHOZ-->

Also keep in mind that there is incoming and outgoing traffic on every page interaction like button click through the WebSocket channel.

Summary

Let’s summarize the ideas above

  1. Both Blazor and Oxpecker templates provide the same user experience (when connection is good). Both of them provide server side rendering so the website is opened quickly (opposite to SPA frameworks).
  2. Several features are dropped in Oxpecker template, which make it cleaner and more universal, but less feature rich. Some of them are intentional (launchSettings, appSettings), some not (scoped CSS, global imports).
  3. Blazor framework requires you to invest into learning it’s internals, namely rendering modes, tag helpers, lifecycle events, custom CSS extensions, Razor DSL, Razor Pages routing, custom attributes as well as general ASP.NET Core pipeline. Oxpecker template is based on pure ASP.NET Core and modern js frameworks - HTMX and Alpine.js, which (ironically) allow you to write from none to very little javascript code.
  4. Blazor template is a mixed version of Blazor Server and Razor Pages technologies with their benefits and downsides. Oxpecker template comes with a lightweight combination of composable tools providing a more functional way or writing server rendered applications.
  5. F# code is not scary and can be used as a healthy option for your server-rendered .NET applications benefiting in both flexibility and noticeable performance increase.

Last thing to note is that that was just a template comparison, there are many more features to use in the aforementioned frameworks and libraries and many more things to consider when writing real-world big applications. Still, it’s good to know what you are starting with, so hopefully you find that information useful, thanks for reading!


메타데이터
post_id
067cbcda9f99
slug
blazor-vs-oxpecker-067cbcda9f99
url
https://medium.com/@lanayx/blazor-vs-oxpecker-067cbcda9f99
canonical_url
https://medium.com/@lanayx/blazor-vs-oxpecker-067cbcda9f99
author_url
https://medium.com/@lanayx
status
ok
fetched_at
2026-07-11 12:05:32