Blazing Fast Rails View Rendering — From JBuilder to Fast JSON API

Calin Ciobanu
Nugit Engineering
Published in
6 min readJun 8, 2018

Motivation

Rails is a very productive framework and makes getting new projects of the ground easy. As projects mature, however, we start paying more attention to performance which also translates into infrastructure costs. In this article I will walk through the process of view rendering optimization of our Rails API.

Selecting the best tool for the job

Overview

One would expect that from the total request time the view rendering would be insignificant when compared with db queries and business logic/third party API calls. However I wanted to put this to the test and see if we can get a bigger bang for the buck.

Our current Rails app is running on version 4.2 and was using JBuilder as the view rendering engine. After a lot of googling and some initial benchmarks I decided to pit the following 3 solutions against each other:
1. JBuilder (legacy)
2. Pure Hash + oj
3. Fast JSON API (netflix open source)

the following were also considered: jsonapi-rails, json-serializer

Benchmarks

There are some benchmarks out there (see relevant links below) that only measure the view rendering time in isolation but for our API I was interested to learn whether there will be a relevant increase in throughput on real life endpoints with real life data so that it’s worth refactoring using a different tool.

Setup:
- Real endpoints with real data
- Apache benchmark
- Single threaded
- Rails 4.2.10
- Ruby 2.4.2
- JBuilder 2.7.0
- OJ 3.5.1
- Fast JSON API 1.1.1
- Puma 3.11.2

Methodology:
1. For each configuration I ran 5 batches of 50 to 200 requests (depending on the endpoint type)
2. I averaged the measured throughput
3. I tested the following configurations:
- index endpoint with pagination (items per page: 10, 20, 50, 100, 200, 500)
- show endpoint

INDEX Endpoint

The above graph shows the mean req/s tested on the index endpoint with different number of items per page and with 5 distinct implementations. For each implementation I considered whether it was possible to keep the same meta-data and template of the response (for backwards compatibility). Let:
- template : response uses the exact same template as legacy and is backwards compatible
- meta-data : extra information such pagination, filters etc
- data : make sure all information is included in the response

  1. (blue) Req/s Pure Hash
    I am building the response as a hash, mapping relevant fields and information then I parse it to json using oj.
    Backwards compatibility: template, meta-data, data
  2. (red) Req/s Pure Fast JSON API
    Just for the heck of it — see what is the potential performance going with the “pure” Fast JSON API way. Parsed to json directly through Fast JSON API.
    Backwards compatibility: data
  3. (yellow) Req/s Mix Hash + Fast JSON API
    Fast JSON API
    is used to serialize the objects to hash which is run through a hash template and then parsed into json with oj.
    Backwards compatibility: template, meta-data, data
  4. (green) Req/s Mix Fast JSON API + Hash
    In the middle solution, using the Fast JSON API way of passing meta-data to the request — template is different thus not backwards compatible. Parse to json directly from Fast JSON API.
    Backwards compatibility: meta-data, data
  5. (purple) Req/s Jbuilder
    Baseline legacy implementation.
    Backwards compatibility: template, meta-data, data

In order to better see the gains, I plotted below the % gains against legacy Jbuilder implementation:

For x < 100, we observe some slight anomalies likely due to data, however the trend is obvious and we can see that gains up to 45% can be achieved in throughput by tweaking the view rendering implementation.

As a side note this type of benchmark is also useful in determining the optimum number of items to be returned per page in order to get the highest throughput gains.

Green contains all data but is not backwards compatible due to default Fast JSON API layout while red does not include all data. Yellow and blue both maintain the same layout and data.

SHOW Endpoint

Notations are using the same conventions for the various implementations as above. Note Req/s Mix FS + H is not relevant for this type of response and has been removed.

Looking at the plot we can see there are slight gains in throughput but not that significant as for the index endpoint. This is aligned with the intuition that for short responses there is less effort involved in the view rendering hence the gains are small.

Code Sample

For the purpose of this exercise let MyEntity be the name of the model we want to serialize.

Notes:

json_layout is a helper method that embeds the hash data passed as parameter into the layout template (hash).

index_view is a helper method that maps model objects to hash.

json_meta is a helper method that creates a hash of meta data from the model objects.

Notice how I use the Fast JSON API serializer either to output json or hash for backwards compatibility.

Data Sample

For the purpose of reproducing the results please see below the anonymized json representation of the entity in the response:

{
"uid1": "some-cool-stuff",
"uid2": "some-more-cool-stuff",
"type": "some",
"currency": "USD",
"extra_data": {
"key": "value"
},
"types": [
"ONE"
],
"zone": "A",
"enabled": true,
"state": "unknown",
"configuration": {
"local": {
"Setting 1": {
"alias": "Setting 1"
},
"Setting 2": {
"alias": "Setting 2"
},
"Setting 3": {
"alias": "Setting 3"
},
"Setting 4": {
"alias": "Setting 4"
},
"Setting 5": {
"alias": "Setting 5"
},
"Setting 6": {
"alias": "Setting 6"
},
"Setting 7": {
"alias": "Setting 7"
},
"Setting 8": {
"alias": "Setting 8"
},
"Setting 9": {
"alias": "Setting 9"
},
"Setting 10": {
"alias": "Setting 10"
}
},
"global": {
"GSetting 1": {
"name": "new_name",
"place": "unknown",
"verified": false
},
"GSetting 2": {
"name": "new_name",
"place": "unknown",
"verified": false
},
"GSetting 3": {
"name": "new_name",
"place": "unknown",
"verified": false
},
"GSetting 4": {
"name": "new_name",
"place": "unknown",
"verified": false
},
"GSetting 5": {
"name": "new_name",
"place": "unknown",
"verified": false
}
}
},
"items": [
"some-cool-stuff"
],
"first_date": "2017-05-19",
"last_date": "2017-06-07"
}

Conclusions / TL;DR

Although JBuilder has a very convenient and clean API it’s performance is not stellar. When compared against raw hashmaps and Fast JSON API it falls short being up to 20% respectively 45% slower in real life usage (full request time with db queries).

--

--

Calin Ciobanu
Nugit Engineering

Random collection of interesting bits about software & (maybe) hardware.