EvaluatedApplications.Blazier
1.1.0
dotnet add package EvaluatedApplications.Blazier --version 1.1.0
NuGet\Install-Package EvaluatedApplications.Blazier -Version 1.1.0
<PackageReference Include="EvaluatedApplications.Blazier" Version="1.1.0" />
<PackageVersion Include="EvaluatedApplications.Blazier" Version="1.1.0" />
<PackageReference Include="EvaluatedApplications.Blazier" />
paket add EvaluatedApplications.Blazier --version 1.1.0
#r "nuget: EvaluatedApplications.Blazier, 1.1.0"
#:package EvaluatedApplications.Blazier@1.1.0
#addin nuget:?package=EvaluatedApplications.Blazier&version=1.1.0
#tool nuget:?package=EvaluatedApplications.Blazier&version=1.1.0
EvaluatedApplications.Blazier
Lazy, scaffold-first Blazor WebAssembly for static hosts such as GitHub Pages. Visitors get real HTML in well under a second, and the .NET runtime downloads only when they use something that needs it. Long C# jobs run in a Web Worker behind an ordinary interface.
Features
- Static shells at publish time. Mark a component
[BlazierPage("tools/prism")]and publish: the build renders it withHtmlRendererand writestools/prism/index.htmlfrom your ownindex.html. A visitor who never interacts downloads only that page (6.8 KB in the sample, against 2.8 MB for the same app booted the usual way). - Per-page head tags.
TitleandDescriptionon[BlazierPage]replace the template's<title>and meta description in each shell. Set the MSBuild propertyBlazierSiteUrland every shell also gets a canonical link and Open Graph tags (og:type,og:url,og:title,og:description). - On-intent loader.
blazier.js(classic script, no dependencies) boots Blazor on click, visibility, idle or immediately, keeps the static shell visible until the component has rendered, and fixes the readiness race (the firstBlazor.rootComponents.addused to throw "Dynamic root components have not been enabled"): islands mount on a ready handshake from the runtime, with a retry fallback. - Start on one control. By default a
Clickpage starts on any press, key, focus or touch inside it. Give it aClickTargetselector (for example"[data-blazier-go]") and only a click on that control starts the runtime, so a phone visitor who scrolls or taps text never downloads it. - Islands. Several named components on one static page, each booting the one shared runtime on its own trigger;
<Island>marks them in Razor;data-blazier-islandworks in plain HTML too. - Build-time shells for I/O pages. Inject
IBuildTimeand skip fetching whenIsPrerenderis true, or wrap the live part in<NotAtBuildTime>with a skeleton. A page that still tries I/O fails the publish with BLZ102 instead of shipping an error message as static HTML. - Lazy assemblies with a guard.
LazyIsland("chart", "Charts.PrimeChart, Charts", "Charts")mounts a component whose assembly downloads only on demand. After every build, a guard walks the IL reachable from startup and fails with BLZ001 (naming the path) if startup code reaches a lazy-assembly type, which would otherwise kill the runtime at boot with a type-load error. - Typed worker bridge. Mark an interface
[WorkerService]; a source generator writes the proxy and the worker-side dispatcher. Calls are awaitable,IProgress<T>streams progress,IAsyncEnumerable<T>streams results with backpressure, aCancellationTokencancels cooperatively, and a job that never yields is stopped by terminating and restarting the worker after a grace period. Both known .NET 10 worker traps are handled inside the package. - Same protocol in-process.
BlazierWorker.StartInProcessAsyncruns your services on a dedicated thread with the same messages, for unit tests and non-browser hosts.
Requires .NET 10 and Microsoft.AspNetCore.Components.WebAssembly 10.0.11 (keep it equal to the installed
browser-wasm runtime pack). The MSBuild steps run a small tool on Microsoft.AspNetCore.App 10.
Quick start
dotnet add package EvaluatedApplications.Blazier
In the project file, turn on scaffolding (and list lazy assemblies, if any):
<PropertyGroup>
<BlazierScaffold>true</BlazierScaffold>
<BlazierSiteUrl>https://example.org</BlazierSiteUrl>
</PropertyGroup>
<ItemGroup>
<BlazierLazyAssembly Include="Charts" />
</ItemGroup>
In wwwroot/index.html, load the loader instead of starting Blazor yourself (keep the import map line):
<div id="app" data-blazier-island="app" data-blazier-start="immediate">Loading...</div>
<script src="_content/EvaluatedApplications.Blazier/blazier.js" defer></script>
Program.cs:
using Blazier;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.UseBlazier(b => b
.Island<Greeting>("greeting")
.LazyIsland("chart", "Charts.PrimeChart, Charts", "Charts"));
await builder.Build().RunAsync();
// Usually a .razor file. Published as hello/index.html; the runtime boots when its button is pressed.
[BlazierPage("hello", Island = "greeting", Start = StartOn.Click, ClickTarget = "[data-blazier-go]",
Title = "Hello", Description = "A page that greets you before any .NET code runs.")]
public sealed class Greeting : ComponentBase
{
[Inject] private IBuildTime Build { get; set; } = default!;
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddContent(0, Build.IsPrerender ? "Hello (static) " : "Hello (live) ");
builder.OpenElement(1, "button");
builder.AddAttribute(2, "type", "button");
builder.AddAttribute(3, "data-blazier-go", true);
builder.AddContent(4, "Start");
builder.CloseElement();
}
}
A worker service:
using Blazier.Workers;
[WorkerService]
public interface IPrimeJobs
{
Task<int> CountAsync(int limit, IProgress<double> progress, CancellationToken cancellationToken);
}
public sealed class PrimeJobs : IPrimeJobs
{
public async Task<int> CountAsync(int limit, IProgress<double> progress, CancellationToken cancellationToken)
{
var slice = new CooperativeSlice(); // yields every 12 ms so a cancel message can be read
int count = 0;
for (int n = 2; n <= limit; n++)
{
bool prime = true;
for (int d = 2; d * d <= n; d++) if (n % d == 0) { prime = false; break; }
if (prime) count++;
if (slice.IsDue) { progress.Report((double)n / limit); await slice.YieldIfDueAsync(cancellationToken); }
}
return count;
}
}
public static class PrimeDemo
{
// Call from a component, for example a button handler.
public static async Task<int> RunAsync(Action<double> onProgress, CancellationToken cancellationToken)
{
await using var worker = await BlazierWorker.StartAsync();
var jobs = worker.Get<IPrimeJobs>();
return await jobs.CountAsync(5_000_000, new Progress<double>(onProgress), cancellationToken);
}
}
How it fits together
| Piece | Where |
|---|---|
UseBlazier, BlazierBuilder.Island/App/LazyIsland |
registers islands with the ready handshake |
[BlazierPage] (Title, Description, Start, ClickTarget), StartOn, <Island> |
what the publish step writes, and what boots when |
IBuildTime, BuildTime, <NotAtBuildTime>, IBlazierPrerenderSetup |
build-time rendering |
blazier.js (Blazier.start/mount/configure, blazier:* events) |
the loader |
[WorkerService], BlazierWorker, WorkerOptions, CooperativeSlice |
the worker bridge |
MSBuild: BlazierScaffold, BlazierSiteUrl, BlazierLazyAssembly, BlazierLazyGuard |
publish and build steps |
Loader options go on the script tag (data-blazor-script, data-framework-base to serve _framework from another
path) or through Blazier.configure({...}). Each island element carries data-blazier-state
(idle, booting, mounting, live, failed) for styling. In plain HTML, a click target is the attribute
data-blazier-click-target="<selector>" next to data-blazier-start="click".
Limits worth knowing
- The first interaction boots the runtime but is not replayed: the visitor presses again once the island is live.
- Navigating between two static pages is a full page load, so in-memory state (a loaded model, say) does not carry
over. Mount the routed app (
App<TApp>()) to keep one runtime and one state across tool-to-tool moves. - The lazy guard is conservative: it follows startup code, routable pages and component render code, not lambdas run later (DI factories, event handlers).
- A worker runtime costs about 32 MiB of memory and about 0.2 s to start from cached files (desktop measurement); on phones, memory is the budget.
- Worker arguments and results travel as JSON (
WorkerJson.Options); for trimmed apps add a source-generatedJsonSerializerContextfrom a[ModuleInitializer]so both the page and the worker see it.
License
Proprietary: this package ships the compiled library only; see LICENSE for the full terms. Installing and using
unmodified copies for internal evaluation and non-commercial development or testing is licensed; commercial,
production, or revenue-generating use requires a separate commercial licence from the owner. No feature described here
is gated behind a tier or a licence key. Copyright (c) 2026 Dongyang Stephen Chen, trading as Evaluated Applications.
All rights reserved.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.AspNetCore.Components.WebAssembly (>= 10.0.11)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.