Google Charts is a free interactive JavaScript charting framework. The Google Chart Gallery showcases many of the available chart types and options. This project shows how to create data from C# functions in Blazor, then use JavaScript interop to pass arrays into JavaScript for display with Google Charts.
This page contains a div
where the chart will be displayed, controls for customizing settings, and a few functions in the code-behind to call a JavaScript function that updates the chart.
@page "/"
@inject IJSRuntime JsRuntime
<!-- this is where the Google Chart is displayed -->
<div id="chart_div" style="height: 400px;"></div>
<!-- buttons call C# functions -->
<button @onclick="PlotSin">Sin</button>
<button @onclick="PlotRandom">Random</button>
<button @onclick="PlotWalk">Walk</button>
<button @onclick="PlotRandomXY">RandomXY</button>
<!-- a slider bound to a C# field -->
<input type="range" @bind="PointCount" @bind:event="oninput">
@code{
private int PointCount = 123;
Random Rand = new Random();
private void PlotData(double[] xs, double[] ys)
{
// This function calls a JavaScript function to update the chart.
// Notice how multiple parameters are passed in.
JsRuntime.InvokeVoidAsync("createNewChart", new { xs, ys });
}
private void PlotSin()
{
double[] xs = Enumerable.Range(0, PointCount).Select(x => (double)x).ToArray();
double[] ys = xs.Select(x => Math.Sin(x / 10)).ToArray();
PlotData(xs, ys);
}
private void PlotRandom()
{
double[] xs = Enumerable.Range(0, PointCount).Select(x => (double)x).ToArray();
double[] ys = xs.Select(x => (Rand.NextDouble() - .5) * 1000).ToArray();
PlotData(xs, ys);
}
private void PlotWalk()
{
double[] xs = Enumerable.Range(0, PointCount).Select(x => (double)x).ToArray();
double[] ys = new double[PointCount];
for (int i = 1; i < ys.Length; i++)
ys[i] = ys[i - 1] + Rand.NextDouble() - .5;
PlotData(xs, ys);
}
private void PlotRandomXY()
{
double[] xs = Enumerable.Range(0, PointCount).Select(x => Rand.NextDouble()).ToArray();
double[] ys = Enumerable.Range(0, PointCount).Select(x => Rand.NextDouble()).ToArray();
PlotData(xs, ys);
}
}
These JavaScript blocks were added just before the closing </body>
tag
<script type='text/javascript' src='https://www.gstatic.com/charts/loader.js'></script>
<script>
google.charts.load('current', { packages: ['corechart', 'line'] });
// draw an empty chart when the page first loads
google.charts.setOnLoadCallback(initChart);
function initChart() {
var xs = [];
var ys = [];
window.createNewChart({xs, ys});
}
// draw a new chart given X/Y values
window.createNewChart = (params) => {
var xs = params.xs;
var ys = params.ys;
var data = new google.visualization.DataTable();
data.addColumn('number', 'X');
data.addColumn('number', 'Y');
for (var i = 0; i < ys.length; i++) {
data.addRow([xs[i], ys[i]]);
}
var options = {
hAxis: { title: 'Horizontal Axis Label' },
vAxis: { title: 'Vertical Axis Label' },
title: 'This is a Google Chart in Blazor',
legend: { position: 'none' },
};
var chart = new google.visualization.LineChart(document.getElementById('chart_div'));
chart.draw(data, options);
};
</script>
Plots look bad when the window is resized because Google Charts adopts a fixed size on each render. To give the appearance of fluid charts (that resize to fit their container as its size changes), add some JavaScript to re-render on every resize.
window.onresize = function () { initChart(); };
We can’t call our existing createNewChart()
method because that expects data (from C#/Blazor) passed-in as a parameter. To support this type of resizing, the call from C# must be modified to store data arrays at the window level so they can be later accessed when the chart is plotted again. This would take some re-structuring of this project, but it’s possible.
-
Google Charts is a straightforward way to display data in mouse-interactive graphs on the web.
-
JS interop allows Blazor to pass data to a JavaScript function that can plot it on a Google Chart.
-
Extra steps are required to automatically resize a Google Chart when its container size changes.
-
For interactive graphs in desktop applications, check out ScottPlot (an open-source plotting library for .NET that makes it easy to interactively display large datasets).
The project simulates the classic Mystify your Mind screensaver from Windows 3 using client-side Blazor. The graphics model logic is entirely C# and the web interface has user-facing options (razor components) to customize its behavior in real time. To render a frame JavaScript calls a C# function that returns JSON containing an array of line details (colors and coordinates) so a JavaScript render function can draw them on a HTML canvas. While .NET APIs for drawing on canvases exist, I find this method to be a bit more versatile because it does not require any external libraries, and it only makes a single JS interop call on each render so it is appreciably faster.
The point of this article is to discuss strategies for rendering graphics using client-side Blazor and WebAssembly, so I won’t detail how the polygons are tracked here. Source code for these classes can be viewed on GitHub (MystifyBlazor/Models) or downloaded with this project (blazor-mystify.zip).
-
Models/Color.cs
- Represents a color and has methods to help with hue-shifting
-
Models/Corner.cs
- Represents a corner of a polygon (position, direction, and velocity) and has logic to move forward in time and bounce off walls
-
Models/Polygon.cs
- A polygon is just a color and a list of corners that are connected by a single line.
-
Models/Shape.cs
- A Shape is a moving polygon, and this class contains a list of polygons (a historical record of a single polygon’s configuration as it changed over time).
-
Models/Field.cs
- Represents the rectangular render area and holds multiple shapes.
- This is the only class Blazor directly interacts with3
- Configuration options (like rainbow mode) are public fields that can be bound to inputs on Razor pages
- This class can return rendering instructions as JSON. This is just an array of lines, each with a color and an array of X/Y points.
This is the HTML that holds the HTML canvas. By using CSS to make the canvas background black I don’t have to concern myself with manually drawing a black background on every new frame.
Note that I’m using Bootstrap 5 (not the Bootstrap 4 that currently comes with new Blazor projects), so the classes may be slightly different.
@page "/"
<div class="bg-white shadow mt-3" id="myCanvasHolder">
<canvas id="myCanvas" style="background-color: black;"></canvas>
</div>
The only goals here are to start the JavaScript render loop and provide a method JavaScript can call to get line data in JSON format. Notice the method called by JavaScript accepts width
and height
arguments so the data model can properly adjust to canvases that change size as the browser window changes dimensions.
@inject IJSRuntime JsRuntime
@code
{
Models.Field MystifyField = new Models.Field();
protected override void OnInitialized()
{
JsRuntime.InvokeAsync<object>("initRenderJS", DotNetObjectReference.Create(this));
}
[JSInvokable]
public string UpdateModel(double width, double height)
{
MystifyField.Advance(width, height);
return MystifyField.GetJSON();
}
}
User-facing configuration is achieved by binding public fields of the graphics model directly to input elements.
<input type="range"
@bind="MystifyField.Speed"
@bind:event="oninput">
<input type="range" min="1"max="20"
@bind="MystifyField.ShapeCount"
@bind:event="oninput">
<input type="range" min="3" max="20"
@bind="MystifyField.CornerCount"
@bind:event="oninput" >
<input type="range" min="1" max="50"
@bind="MystifyField.HistoryCount"
@bind:event="oninput">
<button @onclick="MystifyField.RandomizeColors">
Random Colors
</button>
<input type="checkbox" @bind="MystifyField.Rainbow">
This file contains JavaScript to start the renderer (an infinite loop) and request line data as JSON from a C# function in each render.
Notice that the aspect ratio of the canvas is maintained by setting its with to be a fraction of its height. The canvas width and height are passed as arguments to the C# function which updates the model.
<script>
function renderJS() {
// resize the canvas to fit its parent (resizing clears the canvas too)
var holder = document.getElementById('myCanvasHolder');
var canvas = document.getElementById('myCanvas');
canvas.width = holder.clientWidth;
canvas.height = canvas.width * .6;
// tell C# about the latest dimensions, advance the model, and parse the new data
var dataString = window.theInstance.invokeMethod('UpdateModel', canvas.width, canvas.height);
var polys = JSON.parse(dataString);
// render each polygon
var lineCount = 0;
var ctx = document.getElementById('myCanvas').getContext('2d');
ctx.lineWidth = 2;
for (var i = 0; i < polys.length; i++) {
var poly = polys[i];
var color = poly.shift();
ctx.beginPath();
for (var j = 0; j < poly.length; j++) {
x = parseFloat(poly[j][0]);
y = parseFloat(poly[j][1]);
if (j == 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.strokeStyle = color;
ctx.closePath();
ctx.stroke();
lineCount += 1;
}
window.requestAnimationFrame(renderJS);
}
window.initRenderJS = (instance) => {
window.theInstance = instance;
window.requestAnimationFrame(renderJS);
};
</script>
The JSON that C# passes to the JavaScript renderer is just an array of lines, each with a color and an array of X/Y points. This is an example JSON packet to render a single frame:
["#00FFB4",[665.26,767.31], [1098.58,250.94], [1159.48,206.49], [717.52,194.45]],
["#00FFB4",[660.94,757.61], [1089.18,257.06], [1166.11,203.76], [720.90,201.92]],
["#00FFB4",[656.62,747.92], [1079.78,263.18], [1172.73,201.03], [724.29,209.40]],
["#00FFB4",[652.30,738.22], [1070.38,269.31], [1179.36,198.30], [727.67,216.88]],
["#00FFB4",[647.98,728.53], [1060.98,275.43], [1185.99,195.57], [731.05,224.36]],
["#0049FF",[1219.26,656.86], [5.34,599.35], [454.87,716.81], [276.93,416.92]],
["#0049FF",[1224.78,648.28], [0.00,602.83], [455.97,708.21], [286.48,408.14]],
["#0049FF",[1230.30,639.69], [6.55,606.32], [457.08,699.61], [296.03,399.35]],
["#0049FF",[1235.81,631.11], [13.10,609.80], [458.19,691.02], [305.58,390.57]],
["#0049FF",[1241.33,622.53], [19.65,613.28], [459.29,682.42], [315.13,381.79]]
There are wrappers to let you draw on a HTML canvas from .NET, allowing you to write your render function in C# instead of JavaScript. It is a bit of added complexity (requires a package) and is a bit slower (additional JavaScript interop calls), so I did not choose to use it for this project.
See my earlier article Draw Animated Graphics in the Browser with Blazor WebAssembly for an example of how to draw on a canvas from C# using the Blazor.Extensions.Canvas
package.
Vanilla JavaScript will probably always be faster at rendering graphics in the browser than Blazor. If your application requires fast graphics, you’ll have to write all your graphics model logic and render system in JavaScript. The great advantage of Blazor is that existing C# code that has already been developed (and extensively tested) can be used in the browser. At the time of writing, Speed is not Blazor’s strong suit.
-
This system starts to choke when the model represents more than a few lines. The bottleneck seems to be encoding all the data as JSON on the C# side.
-
I tried using StringBuilder everywhere and also using int
instead of float
, but it did not produce large improvements.
-
I also tired using the Blazor.Extensions.Canvas
package so I could make all the rendering calls from C# (eliminating the conversion to JSON step). Because this required so many individual interop calls (even with batching), it was about twice as slow as the JS renderer implemented in the code above.
This project implements the Boids flocking algorithm in C# and uses Blazor WebAssembly to render it in the browser. Drone birds (bird-oids, or boids) follow a simple set of rules to determine their angle and velocity. When many boids are placed in a field together, interesting group behaviors emerge. Details about the boid flocking algorithm are discussed in depth elsewhere. This article summarizes how to configure a Blazor application to model graphics with C# and render them with JavaScript.
-
Boids with default settings: app/
-
Boids with custom settings: app/?boids=100&predators=5
-
Pro tip: resize your window really small and watch what happens!
The code that manages the field of boids is entirely written in C#. It tracks the positions of boids and advances their positions and directions over time, but it does not manage rendering. Briefly,
- A
BoidField
has a width, height, and array of Boids
- Each
Boid
has a position, direction, and velocity
- Each
Boid
can advance itself by considering the positions of all the other boids:
- Rule 1: boids steer toward the center of mass of nearby boids
- Rule 2: boids adjust direction to match nearby boids
- Rule 3: boids adjust speed to match a match a target
- Rule 4: boids steer away from very close boids
- Rule 5: boids steer away from boids marked as predators
- Rule 6: boids are repelled by the edge of the window
At the time of writing, the most performant way to render high frame rates in the browser is to write your model and business logic entirely in JavaScript. The purpose of this project is not to make the most performant model possible, but rather to explore what can be done with client-side Blazor and WebAssembly. There is a strong advantage to being able to keep keeping your graphics model and business logic in C#, especially if it is already written and extensively tested.
In my Draw Animated Graphics in the Browser with Blazor WebAssembly article we used Blazor.Extensions.Canvas
to draw on a HTML Canvas from C# code. Although this worked, it was limited by the fact that interop calls can be slow. Even though they can be batched, they still represent a significant bottleneck when thousands of calls need to be made on every frame.
In this project I used C# to model the field of boids, converted the field to JSON (with each boid having a X/Y position and a rotation), and had JavaScript parse the boid array and render each boid using a rendering function inside JavaScript.
The full source code can be downloaded here (blazor-boids.zip) or navigated on GitHub C# Data Visualization: Blazor Boids. The C# code for Boid
and BoidField
can be found there. What I’ll focus on here is the key Blazor code required to manage the model and render it using JavaScript.
There are a lot of different things going on here. Most of them can be figured out with a little visual inspection. Here are the highlights:
-
A canvas is created inside a div, each with a unique ID so they can be referenced from JavaScript.
-
A IJSRuntime
is injected so JS functions can be called from C#. Specifically, the initRenderJS
is called when the component is first rendered.
-
A NavigationManager
is injected to let us easily work with query strings (Note that QueryHelpers
requires the Microsoft.AspNetCore.WebUtilities
NuGet package).
-
A boidField
graphics model is created as a private field and initialized OnAfterRender
using query strings to customize the initializer parameters.
-
The UpdateModel()
method is decorated with JSInvokable
so it can be called from JavaScript. It accepts a width and height (which may change if the browser size changes) and can resize the boidField
as needed. It advances the model then converts the positions and directions of all boids to JSON and returns it as a string.
@page "/"
@using System.Text;
@inject IJSRuntime JsRuntime
@inject NavigationManager NavManager
@using Microsoft.AspNetCore.WebUtilities
<div id="boidsHolder" style="position: fixed; width: 100%; height: 100%">
<canvas id="boidsCanvas"></canvas>
</div>
@code{
private Random rand = new Random();
private Models.Field boidField;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
// call the initialization JavaScript function
await JsRuntime.InvokeAsync<object>("initRenderJS", DotNetObjectReference.Create(this));
await base.OnInitializedAsync();
// use a query string to customize the number of boids
int boidCount = 75;
var uri = NavManager.ToAbsoluteUri(NavManager.Uri);
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("boids", out var boidCountString))
if (int.TryParse(boidCountString, out int parsedBoidCount))
boidCount = parsedBoidCount;
// use a query string to customize the number of predators
int predatorCount = 3;
if (QueryHelpers.ParseQuery(uri.Query).TryGetValue("predators", out var predatorCountString))
if (int.TryParse(predatorCountString, out int parsedPredatorCount))
predatorCount = parsedPredatorCount;
// create the model using the custom settings
boidField = new Models.Field(800, 600, boidCount, predatorCount);
}
[JSInvokable]
public string UpdateModel(double width, double height)
{
if (boidField is null)
return "";
boidField.Resize(width, height);
boidField.Advance(bounceOffWalls: true, wrapAroundEdges: false);
StringBuilder sb = new StringBuilder("[");
foreach (var boid in boidField.Boids)
{
double x = boid.X;
double y = boid.Y;
double r = boid.GetAngle() / 360;
sb.Append($"[{x:0.0},{y:0.0},{r:0.000}],");
}
sb.Append(boidField.PredatorCount.ToString());
sb.Append("]");
return sb.ToString();
}
}
Two JavaScript functions are added to the HTML:
renderJS()
- this function calls C# to update the model and request the latest data, then calls itself to create an infinite render loop. Each time it executes it:
- resizes the canvas to fit the div it’s inside of
- calls the C#
UpdateModel()
method (passing in the latest canvas size)
- parses the JSON the C# method returns to obtain an array of boid details
- clears the canvas by filling it with a blue color
- renders each boid (by translating and rotating the canvas, not the boid)
- requests itself be rendered again so rendering continues infinitely
initRenderJS
- this function is called from Blazor so the running instance can be referenced in the future. It also starts the infinite render loop.
<script>
function renderJS() {
// resize the canvas to fit its parent (resizing clears the canvas too)
var holder = document.getElementById('boidsHolder');
var canvas = document.getElementById('boidsCanvas');
canvas.width = holder.clientWidth;
canvas.height = holder.clientHeight;
// tell C# about the latest dimensions, advance the model, and parse the new data
var boidsString = window.theInstance.invokeMethod('UpdateModel', canvas.width, canvas.height);
var boids = JSON.parse(boidsString);
var predatorCount = boids.pop();
// render each boid
var ctx = document.getElementById('boidsCanvas').getContext('2d');
for (var i = 0; i < boids.length; i++) {
var predator = i < predatorCount;
var boid = boids[i];
var x = boid[0];
var y = boid[1];
var rotation = boid[2];
ctx.save();
ctx.translate(x, y);
ctx.rotate(rotation * 2 * Math.PI);
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(4, -2);
ctx.lineTo(0, 10);
ctx.lineTo(-4, -2);
ctx.lineTo(0, 0);
ctx.closePath();
ctx.fillStyle = predator ? '#FFFF00' : '#FFFFFF';
ctx.fill();
ctx.restore();
}
// call this same function to render the next frame
window.requestAnimationFrame(renderJS);
}
window.initRenderJS = (instance) => {
window.theInstance = instance;
window.requestAnimationFrame(renderJS);
};
</script>
- Boids in C# - a Windows application that runs much faster than this Blazor app. It has controls to customize flocking parameters. This was the source of the C# model I used for this Blazor app.
Obviously a native JavaScript Boids simulator will be much faster. Implementing this Blazor app in JavaScript would have meant translating the model from C# to JavaScript. For performance-critical rendering-intensive applications, this is the way to go at the time of writing.
Client-side Blazor allows C# graphics models to be rendered in the browser. This means .NET developers can create web apps with business logic written (and tested) in C# instead of being forced to write their business logic in a totally different language (JavaScript) just to get it to run in the browser. In this project we’ll create an interactive graphics model (a field of balls that bounce off the edge of the screen) entirely in C#, and draw the model on the screen using an API that allows us to interact with a HTML5 Canvas.
At the time of this writing Blazor WebAssembly can’t directly paint on the screen, so JavaScript is required somewhere to make this happen. The Blazor.Extensions.Canvas
package has a Canvas component and provides a C# API for all of its JavaScript methods, allowing you to draw on the canvas entirely from C#.
Rendering gets a little slower every time a JavaScript function is called from Blazor. If a large number of shapes are required (a lot of JavaScript calls), performance may be limited compared to a similar application written entirely in JavaScript. For high performance rendering of many objects, a rendering loop written entirely in JavaScript may be required.
This method is good for simple models with a limited number of shapes. It allows the business logic (and tests) to remain entirely in C#. The same graphics model code could be displayed in the browser (using HTML canvas) or on the desktop in WPF and WinForms apps (using System.Drawing or SkiaSharp).
I’m using graphics model to describe the state of the field of balls and logic required to move each ball around. Ideally this logic would be isolated in a separate library. At the time of writing a .NET Standard C# library seems like a good idea, so the same graphics model could be used in .NET Framework and .NET Core environments.
public class Ball
{
public double X { get; private set; }
public double Y { get; private set; }
public double XVel { get; private set; }
public double YVel { get; private set; }
public double Radius { get; private set; }
public string Color { get; private set; }
public Ball(double x, double y, double xVel, double yVel, double radius, string color)
{
(X, Y, XVel, YVel, R, Color) = (x, y, xVel, yVel, radius, color);
}
public void StepForward(double width, double height)
{
X += XVel;
Y += YVel;
if (X < 0 || X > width)
XVel *= -1;
if (Y < 0 || Y > height)
YVel *= -1;
if (X < 0)
X += 0 - X;
else if (X > width)
X -= X - width;
if (Y < 0)
Y += 0 - Y;
if (Y > height)
Y -= Y - height;
}
}
public class Field
{
public readonly List<Ball> Balls = new List<Ball>();
public double Width { get; private set; }
public double Height { get; private set; }
public void Resize(double width, double height) =>
(Width, Height) = (width, height);
public void StepForward()
{
foreach (Ball ball in Balls)
ball.StepForward(Width, Height);
}
private double RandomVelocity(Random rand, double min, double max)
{
double v = min + (max - min) * rand.NextDouble();
if (rand.NextDouble() > .5)
v *= -1;
return v;
}
private string RandomColor(Random rand) =>
string.Format("#{0:X6}", rand.Next(0xFFFFFF));
public void AddRandomBalls(int count = 10)
{
double minSpeed = .5;
double maxSpeed = 5;
double radius = 10;
Random rand = new Random();
for (int i = 0; i < count; i++)
{
Balls.Add(
new Ball(
x: Width * rand.NextDouble(),
y: Height * rand.NextDouble(),
xVel: RandomVelocity(rand, minSpeed, maxSpeed),
yVel: RandomVelocity(rand, minSpeed, maxSpeed),
radius: radius,
color: RandomColor(rand);
)
);
}
}
}
Use NuGet to install Blazor.Extensions.Canvas
Install-Package Blazor.Extensions.Canvas
This JavaScript sets-up the render loop which automatically calls RenderInBlazor
C# method on every frame. It also calls the ResizeInBlazor
C# function whenever the canvas is resized so the graphics model’s dimensions can be updated. You can place it just before the closing </body>
tag.
This code will automatically resize the canvas and graphics model to fit the window, but for a fixed-size canvas you can omit the addEventListener
and resizeCanvasToFitWindow
calls.
<script src='_content/Blazor.Extensions.Canvas/blazor.extensions.canvas.js'></script>
<script>
function renderJS(timeStamp) {
theInstance.invokeMethodAsync('RenderInBlazor', timeStamp);
window.requestAnimationFrame(renderJS);
}
function resizeCanvasToFitWindow() {
var holder = document.getElementById('canvasHolder');
var canvas = holder.querySelector('canvas');
if (canvas) {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
theInstance.invokeMethodAsync('ResizeInBlazor', canvas.width, canvas.height);
}
}
window.initRenderJS = (instance) => {
window.theInstance = instance;
window.addEventListener("resize", resizeCanvasToFitWindow);
resizeCanvasToFitWindow();
window.requestAnimationFrame(renderJS);
};
</script>
This step ties everything together:
- The graphics model: a private class stored at this level
- The canvas component: a protected field
- The canvas: a Razor component referencing the canvas component
- The init code: which tells JavaScipt to start the render loop
- The render method: C# function called from the JavaScript render loop when a frame is to be drawn
- The resize method: C# function called from JavaScript to update the model when the canvas size changes
- JavaScript runtime injection: This allows Blazor/JavaScript interoperability (JS interop)
For simplicity it’s demonstrated here using a code-behind, but a clearer strategy would be to move the render logic into its own class/file.
@page "/"
@using Blazor.Extensions
@using Blazor.Extensions.Canvas
@using Blazor.Extensions.Canvas.Canvas2D
@inject IJSRuntime JsRuntime;
<div id="canvasHolder" style="position: fixed; width: 100%; height: 100%">
<BECanvas Width="600" Height="400" @ref="CanvasRef"></BECanvas>
</div>
@code{
private Models.Field BallField = new Models.Field();
private Canvas2DContext ctx;
protected BECanvasComponent CanvasRef;
private DateTime LastRender;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
this.ctx = await CanvasRef.CreateCanvas2DAsync();
await JsRuntime.InvokeAsync<object>("initRenderJS", DotNetObjectReference.Create(this));
await base.OnInitializedAsync();
}
[JSInvokable]
public void ResizeInBlazor(double width, double height) => BallField.Resize(width, height);
[JSInvokable]
public async ValueTask RenderInBlazor(float timeStamp)
{
if (BallField.Balls.Count == 0)
BallField.AddRandomBalls(50);
BallField.StepForward();
double fps = 1.0 / (DateTime.Now - LastRender).TotalSeconds;
LastRender = DateTime.Now;
await this.ctx.BeginBatchAsync();
await this.ctx.ClearRectAsync(0, 0, BallField.Width, BallField.Height);
await this.ctx.SetFillStyleAsync("#003366");
await this.ctx.FillRectAsync(0, 0, BallField.Width, BallField.Height);
await this.ctx.SetFontAsync("26px Segoe UI");
await this.ctx.SetFillStyleAsync("#FFFFFF");
await this.ctx.FillTextAsync("Blazor WebAssembly + HTML Canvas", 10, 30);
await this.ctx.SetFontAsync("16px consolas");
await this.ctx.FillTextAsync($"FPS: {fps:0.000}", 10, 50);
await this.ctx.SetStrokeStyleAsync("#FFFFFF");
foreach (var ball in BallField.Balls)
{
await this.ctx.BeginPathAsync();
await this.ctx.ArcAsync(ball.X, ball.Y, ball.Radius, 0, 2 * Math.PI, false);
await this.ctx.SetFillStyleAsync(ball.Color);
await this.ctx.FillAsync();
await this.ctx.StrokeAsync();
}
await this.ctx.EndBatchAsync();
}
}
Notice how the JavaScript calls are wrapped in BeginBatchAsync
and EndBatchAsync
. This allows the calls between these two statements to be bundled into a single call. Since Blazor/JavaScript interop calls are the primary bottleneck in this system, limiting the number of individual JavaScript calls has a large influence on final performance.
💡 I observed a strong performance increase when upgrading from .NET Core 3.1 to .NET 5. If you can, Migrate your Blazor project to .NET 5 or newer
When Visual Studio 2019 creates a new Blazor app it defaults to .NET Standard 2.1. A few months ago .NET 5 was released, and upgrading Blazor apps to use .NET 5 has many strong performance improvements. I’m sure a Visual Studio update will soon make this easier, but for now to create a new .NET Blazor app running .NET 5 I use these steps:
- Create a new .NET core 3.1 Blazor App
- Edit the csproj file
- Change SDK from
Microsoft.NET.Sdk.Web
to Microsoft.NET.Sdk.BlazorWebAssembly
- Remove
RazorLangVersion
- Update
TargetFramework
from netstandard2.1
to net5.0
- Remove the
Microsoft.AspNetCore.Components.WebAssembly.Build
package reference
- update all NuGet packages to their latest versions
Extensive details can be found on Microsoft’s official Migrate from ASP.NET Core 3.1 to 5.0 documentation page, but I find this short list of steps easier to refer to.
Here is a new project running .NET 5.0.1 with Bootstrap 5.0 alpha:
To change the project name/namespace:
- Rename the
.sln
and .csproj
files
- Update the
.sln
file in a text editor to match the new filenames
- Update the namespace in
Program.cs
- Update the namespaces in
_Imports.razor