An easy way to drastically improve JsonSerializer performance (System.Text.Json)

A .NET 6 project that I’ve been contributing to uses the new(ish) System.Text.Json JsonSerializer to serialise/deserialise data prior to storing to, and after retrieving from, a distributed cache.

I started to notice some performance degradation as I increased our use of caching in this application (and therefore more JSON serialising/deserialising going on), so set to work on investigating why this was the case.

To note – we had been retrieving the JsonSerializerOptions object from a static method rather than using a static variable, an approach even used by some tutorials, e.g:

    private static JsonSerializerOptions GetJsonSerializerOptions()
    {
        return new JsonSerializerOptions
        {
            PropertyNamingPolicy = null,
            WriteIndented = true,
            AllowTrailingCommas = true,
            DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        };
    } 

And one of our usages of this in the cache context (we were also deserialising in the same way when getting from the cache):

 private static Task SetAsync<T>(this IDistributedCache cache, string key, T value, DistributedCacheEntryOptions options)
    {
        var bytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(value, GetJsonSerializerOptions()));
        return cache.SetAsync(key, bytes, options);
    }

Testing – Huge Performance Difference

I’ve written a couple of benchmark tests comparing the use of a “GetJsonSerializerOptions” method to return an JsonSerializerOptions instance on de/serialise vs using a static variable. The code and data used for both tests is identical, aside from the approach with the options instance – it loops through and both serialises and deserialises an object 10,000 times, and logs the time taken and memory allocated:

The test – 10k serialises + deserialises of a dummy “CustomData” object:

Static Method: 45,850.30ms mean time, 1397.81MB Memory Allocated

Static variable: 62.42ms mean time, 32.65MB Memory Allocated

Yep – over this 10k loop, the static method approach allocated over 1.3GB more memory, and took roughly 734.5x more time, vs using a static variable…! I then double checked the same approach with dotTrace – bear in mind there is a margin for error and deviation…


~44.7seconds for the Static Method, 80ms for the Static Variable.

At the bottom of the post is a gist to show that both tests are identical (10k loops of serialising and deserialising the same data, with the only difference being the JsonSerializerOptions).

Why?

This behaviour is due to the way the JsonSerializer fundamentally works. When serialising/deserialising an object, the JsonSerializer has to perform a “warm-up phase“, which involves generating and caching metadata related to how to de/serialise the type. When the JsonSerializerOptions are re-used, it can simply use the same metadata that it previously cached for the next time an object of that type is processed – however, with a method returning a new JsonSerializerOptions instance every time, it cannot use the cached de/serialising metadata for a type that has already been processed, and has to generate this anew every single time it serialises or deserialises any data. This is an expensive process, and results in much slower execution + a huge amount more memory being allocated. If part of your system relies on JsonSerializer before storing or after retrieving data – this could result in slow performance, and potentially memory leaks and timeouts.

Solution

The change is very simple. Replace (and change any references to) any method/anywhere inline where you might be creating a new JsonSerializerOptions object repeatedly, with a static variable, such as:

private static JsonSerializerOptions _serializerOptions = new JsonSerializerOptions
    {
        PropertyNamingPolicy = null,
        WriteIndented = true,
        AllowTrailingCommas = true,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
    };

Gist

Leave a Reply

Your email address will not be published. Required fields are marked *