Skip to content

Rendering Performance Improvements#566

Open
lessthanjacob wants to merge 6 commits intomainfrom
js/performance-improvements
Open

Rendering Performance Improvements#566
lessthanjacob wants to merge 6 commits intomainfrom
js/performance-improvements

Conversation

@lessthanjacob
Copy link
Contributor

@lessthanjacob lessthanjacob commented Jan 31, 2026

Background

#562 has surfaced a clear regression in recent versions of the library:

Metric v0.10.0 v1.2.1 (current) Change
Speed (i/s) 5.16 1.92 2.7x slower
Memory 26.7 MB 112 MB 4.2x more

This has been compounding for awhile, and there are two core contributing factors:

  • The "structure" of a given view is built each time that view is used, and we've been adding more processing in this path over time.
  • During the render process, we're allocating a lot extra Hash instances as a side-effect of managing and passing around local_options.

While there certainly a lot of opportunities here for rework here, this PR aims to improve performance while avoiding too significant of a refactor (with the understanding that we'd like to put more emphasis on moving to a V2 version of the library in the near future).

Changelog

  • Introduce caching mechanism for ViewCollection to avoid recalculating the structure of each view at render time.
    • Freeze fields and transformers arrays in the cache
  • Merge view into local_options once per object, instead of every time a field is extracted.
  • Only attempt to initialize a new options_without_default Hash if default or default_if keys are present.
  • Initialize one instance of extractor_default and reuse that for each Field.
  • Initialize one instance of AssociationExtractor and reuse that for each Association.
  • Extend Metrics/MethodLength to 20.

ViewCollection Caching

There were a few options considered in regards to caching fields/transformers in ViewCollection.

  1. Build the full cache upfront the first time any view is accessed.
  2. Lazily build a per-view cache as needed
  3. Use TracePoint to build the full cache when the a Blueprint class definition ends.

After some testing, the first Option 1 seemed to strike the best balance between consistency/safety and code simplicity, at the expense of a negligible overhead when the first render occurs.

Option 2 incurs less overhead, but requires a bit more complexity for consistent cache handling and thread-safety.

Option 3 puts the cache building in a spot where we wouldn't add any overhead to render calls, but is more complex and harder to reason with.

Introducing a battle-tested library like concurrent-ruby may also help here, but given the current use-case, a simple double-check mutex should be sufficient.

Benchmarks

Using the same benchmark in the linked issue:

Metric Before After Improvement
Speed 5.084 i/s 8.738 i/s +71.9%
Memory 76.6M 15.0M -80.4%
Allocations 929k ~100k -89.2%

Comparisons

Speed (Iterations/Second)

Rank Serializer Speed vs Top
1 Panko 21.4 i/s
2 as_json 19.4 i/s 1.11x slower
3 fast_jsonapi 13.2 i/s 1.62x slower
4 Alba 9.4 i/s 2.29x slower
5 Blueprinter 8.7 i/s 2.46x slower
6 Roar 2.8 i/s 7.78x slower
7 Grape Entity 2.5 i/s 8.45x slower
8 AMS 0.5 i/s 45.4x slower

Memory Usage

Rank Serializer Memory Objects vs Top
1 Blueprinter 15.0M 100k --
2 as_json 23.0M 270k 1.53x more
3 Panko 23.1M 270k 1.53x more
4 Alba 24.6M 140k 1.64x more
5 fast_jsonapi 25.2M 350k 1.68x more
6 Grape Entity 100.5M 1.06M 6.69x more
7 Roar 120.2M 801k 8.00x more
8 AMS 245.1M 2.99M 16.31x more

Signed-off-by: Jacob Sheehy <jacobjsheehy@gmail.com>
…s in Rendering

Signed-off-by: Jacob Sheehy <jacobjsheehy@gmail.com>
Signed-off-by: Jacob Sheehy <jacobjsheehy@gmail.com>
@lessthanjacob lessthanjacob requested review from a team and ritikesh as code owners January 31, 2026 03:40
@lessthanjacob lessthanjacob self-assigned this Jan 31, 2026
Signed-off-by: Jacob Sheehy <jacobjsheehy@gmail.com>
Signed-off-by: Jacob Sheehy <jacobjsheehy@gmail.com>
@lessthanjacob lessthanjacob force-pushed the js/performance-improvements branch from a5ef505 to 4f5168c Compare January 31, 2026 03:41
Signed-off-by: Jacob Sheehy <jacobjsheehy@gmail.com>
@lessthanjacob lessthanjacob force-pushed the js/performance-improvements branch from 7d52d4f to bbb86fa Compare January 31, 2026 03:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants