You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用JsonSerializer.DeserializeAsyncEnumerable处理大JSON性能缓慢如何解决?

问题

使用.NET 8、System.Text.Json 8、HttpClient发起请求,以HttpConnection.ChunkedEncodingReadStream作为响应输入流,通过JsonSerializer.DeserializeAsyncEnumerable异步反序列化枚举大型JSON对象时性能极差。

代码示例

await using var fileStream = File.OpenRead(filePath);
using var content = new MultipartFormDataContent();
using var streamContent = new StreamContent(fileStream);
streamContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data")
{
    Name = "file",
    FileName = Path.GetFileName(filePath)
};
content.Add(streamContent);

var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, "api/v1/streaming")
{
    Content = content
};

var response = await httpClient.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead,
    cancellationToken);

var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
var streamWrapper = new StreamWrapper(responseStream);

var bufferSize = 10 * 1024 * 1024;
var options = new JsonSerializerOptions
{
    PropertyNameCaseInsensitive = true,
    Converters = {new JsonElementToObjectConverter()},
    DefaultBufferSize = bufferSize
};

await foreach (var batchPart in JsonSerializer.DeserializeAsyncEnumerable<BatchPart>(streamWrapper, options,
                   cancellationToken))
{
}

排查结果

JsonSerializer尝试读取DefaultBufferSize大小的数据,但服务器/输入流无法提供足够数据填满缓冲区,随后尝试反序列化对象,数千次迭代下导致性能极差。读取日志显示每次仅读取少量字节却尝试填充大缓冲区:

Time            Iteration #N            Buffer to read          Read bytes
12:35:45.145            1               16777216                535
12:35:49.327            2               16777216                4090
12:35:49.33             3               16773126                10671
12:35:49.33             4               16762455                4088
12:35:49.331            5               16758367                10673
...
12:36:04.751            882             24051345                4088
12:36:04.789            883             24047257                10658
12:36:04.827            884             24036599                4088
12:36:04.871            885             24032511                10667
...

整体耗时约15分钟,但将HttpCompletionOption改为ResponseContentRead(不使用分块读取)仅需约2秒。问题根源在于反序列化时内部使用fillBuffer:false,导致每次读取都无法高效利用缓冲区。


解决方案

1. 调整DefaultBufferSize为合理小值

过大的缓冲区(如10MB)会导致每次读取都等待填充大缓冲区,改为匹配实际读取字节数的大小,比如4KB或8KB:

var bufferSize = 4 * 1024; // 4KB
var options = new JsonSerializerOptions
{
    PropertyNameCaseInsensitive = true,
    Converters = {new JsonElementToObjectConverter()},
    DefaultBufferSize = bufferSize
};

2. 使用BufferedStream包装响应流

给HttpConnection.ChunkedEncodingReadStream套一层BufferedStream,让小批量分块数据先缓冲到内存,再提供给JsonSerializer,减少频繁的小读取操作:

var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken);
// 使用64KB左右的缓冲大小平衡内存占用和读取效率
await using var bufferedStream = new BufferedStream(responseStream, 64 * 1024);
// 若StreamWrapper无特殊必要逻辑,直接使用bufferedStream即可
await foreach (var batchPart in JsonSerializer.DeserializeAsyncEnumerable<BatchPart>(bufferedStream, options, cancellationToken))
{
    // 处理数据
}

3. 移除不必要的StreamWrapper

如果StreamWrapper没有特殊业务逻辑,直接使用原始响应流或包装后的BufferedStream,避免额外的流包装开销。

4. 优化自定义转换器JsonElementToObjectConverter

检查该转换器的实现是否存在不必要的内存分配或重复序列化操作,若业务允许,改用强类型反序列化代替JsonElement转换,减少转换开销。

5. 手动控制Utf8JsonReader处理流

若上述方法仍不满足需求,可绕过DeserializeAsyncEnumerable,直接使用Utf8JsonReader手动控制读取逻辑:

await using var bufferedStream = new BufferedStream(responseStream, 64 * 1024);
var reader = new Utf8JsonReader(bufferedStream, options);

while (reader.Read())
{
    if (reader.TokenType == JsonTokenType.StartObject)
    {
        var batchPart = JsonSerializer.Deserialize<BatchPart>(ref reader, options);
        // 处理batchPart
    }
}

6. 协调服务器调整分块大小

若服务器每次仅返回几百字节的极小分块,也会导致客户端频繁读取。可协调服务器调整分块输出的批量大小,减少IO往返次数。


内容的提问来源于stack exchange,提问作者Oleh Hrechukh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 20:44:55