loading

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:

OperationInterpreterAOTDesktop .NET
Resize to 1280, Lanczos3505 ms56 ms34 ms
Brightness406 ms27 ms21 ms
Four adjustments, fused1008 ms63 ms48 ms
Gaussian blur, radius 8789 ms88 ms57 ms
Unsharp mask1267 ms119 ms86 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.

XML
<PropertyGroup>  <RunAOTCompilation>true</RunAOTCompilation></PropertyGroup>

Resizing

Downscaling to 1280px on the long edge, and generating a 200px thumbnail (desktop .NET 10, AVX2):

Operation1920×10803840×21606000×4000
Box15.0 ms35.4 ms127 ms
Bilinear17.4 ms40.2 ms114 ms
Bicubic21.9 ms49.0 ms124 ms
Lanczos326.8 ms67.7 ms134 ms
Thumbnail to 200px (Auto)6.6 ms19.2 ms52.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

Operation1920×10803840×2160
Crop0.04 ms0.41 ms
Rotate 90 (lossless)2.0 ms20.2 ms
Brightness (one pass)18.0 ms70.7 ms
Four adjustments, fused47.4 ms188.9 ms
Four adjustments, unfused76.7 ms292.3 ms
Gaussian blur, radius 855.7 ms226.1 ms
Unsharp mask103.8 ms331.0 ms
Rotate 37° (resampled)222.0 ms888.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

Operation1024×7683840×2160Allocated (4K)
PNG decode10.5 ms104 ms37 MB
JPEG decode10.7 ms117 ms14 MB
JPEG encode, quality 8517.0 ms175 ms6.7 MB
PNG encode, level 125.1 ms252 ms98 MB
PNG encode, level 682.1 ms848 ms46 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.
C#
ImagePipeline.Create().AutoOrient().MaxSize(1920, 1920).Sharpen(0.3f)
An unhandled error has occurred. Reload 🗙