Performance
Every number here was measured with BenchmarkDotNet, not estimated - and in the browser, they only hold with ahead-of-time compilation. Read the first section before anything else.
Measure this browser
The operations from the table below, on a generated 1920 × 1080 image, best of three
after a warm-up. Hundreds of milliseconds per row means the interpreter; tens means AOT.
This site is published with AOT; under dotnet run it is interpreted, and the
run takes minutes rather than seconds - which is the point.
WebAssembly: compile ahead of time
By default Blazor WebAssembly runs .NET code on an IL interpreter. Tight pixel loops are its
worst case: every Vector4 operation RyuJIT turns into one SIMD instruction becomes
a separate interpreted step. Measured on a 1920 × 1080 image in Chrome, best of three after
a warm-up:
| Operation | Interpreter | AOT | Desktop .NET |
|---|---|---|---|
| Resize to 1280, Lanczos3 | 505 ms | 56 ms | 34 ms |
| Brightness | 406 ms | 27 ms | 21 ms |
| Four adjustments, fused | 1008 ms | 63 ms | 48 ms |
| Gaussian blur, radius 8 | 789 ms | 88 ms | 57 ms |
| Unsharp mask | 1267 ms | 119 ms | 86 ms |
The interpreter is 10 to 35× slower than desktop .NET; AOT is 1.2 to 2.4×. A Debug build measured the same as Release on the interpreter, so the build configuration is not the issue. Hosting models shows how to switch it on, and how this site does.
<PropertyGroup> <RunAOTCompilation>true</RunAOTCompilation></PropertyGroup>Resizing
Downscaling to 1280px on the long edge, and generating a 200px thumbnail (desktop .NET 10, AVX2):
| Operation | 1920×1080 | 3840×2160 | 6000×4000 |
|---|---|---|---|
| Box | 15.0 ms | 35.4 ms | 127 ms |
| Bilinear | 17.4 ms | 40.2 ms | 114 ms |
| Bicubic | 21.9 ms | 49.0 ms | 124 ms |
| Lanczos3 | 26.8 ms | 67.7 ms | 134 ms |
| Thumbnail to 200px (Auto) | 6.6 ms | 19.2 ms | 52.4 ms |
Allocations stay between 22 KB and 180 KB regardless of source size, because pixel buffers are pooled and the resampler works a row at a time. Thumbnailing a 24 megapixel photo originally took 368 ms; an exact integer box prepass to within 2× of the target, finished by the requested filter, brought it to 52 ms and cut its allocations tenfold - with quality guarded by tests that a ramp stays linear and a fine checkerboard averages to grey rather than aliasing.
Operations and pipelines
| Operation | 1920×1080 | 3840×2160 |
|---|---|---|
| Crop | 0.04 ms | 0.41 ms |
| Rotate 90 (lossless) | 2.0 ms | 20.2 ms |
| Brightness (one pass) | 18.0 ms | 70.7 ms |
| Four adjustments, fused | 47.4 ms | 188.9 ms |
| Four adjustments, unfused | 76.7 ms | 292.3 ms |
| Gaussian blur, radius 8 | 55.7 ms | 226.1 ms |
| Unsharp mask | 103.8 ms | 331.0 ms |
| Rotate 37° (resampled) | 222.0 ms | 888.6 ms |
- Fusing adjustments is worth 1.55×. Four adjustments in one pass beat four passes, because the cost is reading and writing pixels rather than the arithmetic.
- A right angle is 100× cheaper than an arbitrary one, because it moves pixels rather than resampling them. The optimiser routes multiples of 90° to the lossless path automatically.
- Blur is independent of radius: three box passes approximating a gaussian, so radius 40 costs the same as radius 4.
- Resize then crop fuses into one resample of only the surviving region - 47.9 ms against 117.7 ms on a 4000 × 3000 source, bit-identical.
Codecs
| Operation | 1024×768 | 3840×2160 | Allocated (4K) |
|---|---|---|---|
| PNG decode | 10.5 ms | 104 ms | 37 MB |
| JPEG decode | 10.7 ms | 117 ms | 14 MB |
| JPEG encode, quality 85 | 17.0 ms | 175 ms | 6.7 MB |
| PNG encode, level 1 | 25.1 ms | 252 ms | 98 MB |
| PNG encode, level 6 | 82.1 ms | 848 ms | 46 MB |
PNG compression level matters more than anything else here: level 6 is 3.4× slower than level 1 for a modest size gain. Level 1 suits a preview that is on screen for a moment; the default of 6 suits the file the user keeps.
Interactive editing
The editor works on a proxy no larger than 2048px on its longest edge and replays the edit at full resolution on export: for a 24 megapixel photo, a 25× reduction in the pixels each preview touches - the difference between a slider that tracks your finger and one that does not. History stores descriptions, not pixels: a test asserts that 100 undo steps on a 6000 × 4000 image stay under 1 MB, where pixel snapshots would need about 4.8 GB.
Guidance
- Cap dimensions before anything else. Every later operation costs proportionally less.
- Group adjustments together. Consecutive per-pixel operations fuse into one pass; a geometric operation between them prevents that.
- Prefer right angles.
Rotate(90)takes the lossless path;Rotate(90.5)resamples the whole image. - Keep batch concurrency low. Each worker holds a decoded image; four on 24 megapixel photos is roughly 400 MB of live pixels.
- Dispose buffers, and use object URLs, not data URLs.
ImagePipeline.Create().AutoOrient().MaxSize(1920, 1920).Sharpen(0.3f)