Scraping search results at SerpApi often involves JSON parsing. SerpApi is also generating large JSON responses, where JSON serialization takes place. It is worthwhile to investigate if we can speed up JSON processing.

A quick search of the Ruby ecosystem shows Oj as the best fit for Rails. It has support for being a drop-in replacement with super easy configuration. We benchmarked Oj vs. the standard json library that ships with Ruby.

Since json performance may differ across Ruby versions, we ran the same benchmark code under Ruby 2.7.2, Ruby 3.1.2, and Ruby 3.1.2 +YJIT.

We compared four methods that are mainly used: JSON.parse, obj.to_json, JSON.dump, JSON.pretty_generate. We ran each method for 10K iterations and compared the time taken.

Here is the benchmark code:

def run(result)
  json = File.read('test.json')
  result[:parse] << Benchmark.ms {
    10000.times do
      JSON.parse(json)
    end
  }
  puts "parse #{result[:parse].last}"

  hash = JSON.parse(json)
  result[:to_json] << Benchmark.ms {
    10000.times do
      hash.to_json
    end
  }
  puts "to_json #{result[:to_json].last}"

  hash = JSON.parse(json)
  result[:dump] << Benchmark.ms {
    10000.times do
      JSON.dump(hash)
    end
  }
  puts "dump #{result[:dump].last}"

  hash = JSON.parse(json)
  result[:pretty_generate] << Benchmark.ms {
    10000.times do
      JSON.pretty_generate(hash)
    end
  }
  puts "pretty_generate #{result[:pretty_generate].last}"
end

json_result = {
  parse: [],
  to_json: [],
  dump: [],
  pretty_generate: []
}
10.times { run(json_result) }

Oj.optimize_rails()

oj_result = {
  parse: [],
  to_json: [],
  dump: [],
  pretty_generate: []
}
10.times { run(oj_result) }

[json_result, oj_result].each do |result|
  result.each do |k, v|
    result[k] = v.sum / v.size
  end
end

pp json_result
pp oj_result

We ran the tests on DigitalOcean CPU-Optimized, 2 vCPUs, 4 GB.

Here are the results:

json vs Oj under ruby 2 7 2 (lower is better)
json vs Oj under ruby 3 1 2 (lower is better)
json vs Oj under ruby 3 1 2 +YJIT (lower is better)

And here is the raw data:

Ruby 2.7.2

 jsonOj
JSON.parse7994.6613934043.134571
obj.to_json64983.644823405.177611
JSON.dump2713.5363241633.386881
JSON.pretty_generate2964.2740161938.549645

Ruby 3.1.2

 jsonOj
JSON.parse6005.8065094317.333413
obj.to_json72626.326462760.772417
JSON.dump2901.9542731708.129035
JSON.pretty_generate3214.6539182136.254046

Ruby 3.1.2 +YJIT

 jsonOj
JSON.parse5963.4523774363.962555
obj.to_json69703.346562790.86789
JSON.dump2873.2923261712.871591
JSON.pretty_generate3158.8970772179.701062

We have also checked if the Oj and json outputs are identical.

Oj outperforms json on all tested ruby versions.

On Ruby 2.7.2, Oj has 1.97x JSON.parse, 19x obj.to_json, 1.69x JSON.dump, 1.5x JSON.pretty_generate. We can easily improve JSON performance by at least 50% with a few lines of code!

Ruby 3.1.2 has improved JSON.parse, but other methods have become slower. Ruby 3.1.2 with YJIT enabled does not have a significant impact on performance.

obj.to_json is unexpectedly slow by default. According to the documentation, Oj has optimized the behavior. We have yet to check if the output will differ on model objects, but we can certainly use it on pure Hash without a problem.

That's all for the JSON benchmark. Thank you for reading! See you at the following benchmark post.