Closures as callbacks: improving Blazor performance (part 3).

This post is about Blazor, the Microsoft web framework that allows you to write single-page apps in C# instead of JavaScript. They run either in the browser or on the server. Rendering is done to a virtual DOM. The changes to the virtual DOM are sent as a diff to the browser to make the changes to the real DOM.

In part 2 we found a solution to compare closures and optimize some diffs away, but is it enough for our bind-* constructs as well? Due to the way the razor compiler generates two-way binding, it creates a closure as well, so it should work. Inspecting our message size though, it looks like our fix fails to make a difference in this case: message sizes are still increasing with every added row. So there must be more to this than just comparing closures. When we turn on the debugger and place a breakpoint on our new code, we see something interesting:

We expect our delegates to point to a method on a closure within our component, but instead they point to a closure inside a CreateBinderCore method. What is going on? Lets try and find this CreateBinderCore method inside Blazor.

And there it is, our setter delegate that points to a closure is captured by the next lambda inside the CreateBinderCore method, creating another closure. In other words, we have nested closures and we need to make our algorithm recursive.

Recursive comparison of delegates

With our newfound knowledge about closures, we can write our final version of EventHandlerDelegateWithClosuresComparer that will recursively compare the fields of the closure:

Lines 66-82 will do the recursive calls necessary to inspect delegates to closures that capture delegates to closures themselves. This completes the fix for this issue. A quick look at our test component verifies that we now have equally sized diffs for every added row:

Bonus: Unit tests

Now that we have verified that our code works using the browser it is time to write some unit tests for it (and to make sure that the existing unit tests don’t fail). The unit tests are listed below:

We have four situations that we will test:

  • A test with a closure that captures the same value. This should result in an empty diff.
  • A test with a closure that captures a different value. This should result in a diff with an entry for the attribute.
  • A test with a nested closure that captures the same value. This should result in an empty diff.
  • A test with a nested closure that captures a different value. This should result in a diff with an entry for the attribute.

And indeed our tests succeed with the fix enabled. (they also failed without the fix, I’ve verified).

This concludes our three-part series about inspecting closures to improve Blazor performance. The code for this branch is here. I’ll submit this as a PR and fingers crossed it will be accepted. If it won’t, at least you know why your diffs are larger than necessary.