Skip to content

Cut ViewComponent's share of boot by a quarter: look sidecar files up once while eager loading - #2746

Open
czj wants to merge 2 commits into
ViewComponent:mainfrom
czj:perf/memoize-sidecar-files
Open

czj wants to merge 2 commits into
ViewComponent:mainfrom
czj:perf/memoize-sidecar-files

Conversation

@czj

@czj czj commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

What are you trying to accomplish?

Following #2742, which made each sidecar_files call cheaper: a component still looks its sidecar files up several times while the app eager loads. It does so when it compiles, when it loads its translations, and when its cache digest is precomputed. Each subclass also repeats the lookups for every superclass. Nothing on disk changes during that time, so this looks each component's files up once.

With performance/measure.rb, five interleaved runs each, the boot ratio drops from 1.553 to 1.136 (−26.9%): time spent in ViewComponent while booting the 300-component app goes from 1,341 ms to 965 ms (median) on my machine. Allocations drop from 1,117,727 to 1,100,810. Rendering scenarios don't move. On our app (302 components in deeper hierarchies) it saves about another 250 ms of compile.view_component on top of #2742.

What approach did you choose and why?

The cache only exists inside CompileCache.with_sidecar_files_cache, which wraps the eager compile and the cache digest precomputation in the engine. It lives in fiber storage and is dropped when the block ends, so no state outlives boot. A first version kept it in a mattr_accessor cleared with the compile cache, which Audition rightly flagged as not Ractor-safe. Scoping it to eager loading also removes any invalidation question in development, where components compile lazily and every lookup still reads the disk.

The public sidecar_files is untouched: it still reads the disk and returns a new array every time. The cached lookup, __vc_sidecar_files, is only used by the compiler, translation loading, and the cache digest resolver.

Anything you want to highlight for special attention from reviewers?

The new test checks that within the block, a template added after a lookup stays invisible to the cached lookup while sidecar_files sees it straight away, and that the cached lookup sees it once the block ends.

While measuring, I also profiled render_in, hoping for a follow-up. A component rendering a plain #call allocates 4 objects without instrumentation, and the only one that belongs to ViewComponent is the buffer with_buffer swaps in. With instrumentation on, ActiveSupport::Notifications adds 7. A component's t costs one object more than the view's. So I found nothing worth changing there.


🤖 Generated with Claude Code, but prompted, read, and verified by @czj, a caring human :)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@czj czj changed the title Cache sidecar file lookups until the compile cache is invalidated Cut ViewComponent's share of boot by a quarter: cache sidecar file lookups (700 → 538 ms) Oct 24, 2026
Module state isn't Ractor-safe, and scoping the cache to the eager load
removes any invalidation concern in development.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@czj czj changed the title Cut ViewComponent's share of boot by a quarter: cache sidecar file lookups (700 → 538 ms) Cut ViewComponent's share of boot by a quarter: look sidecar files up once while eager loading Oct 24, 2026

This branch has not been deployed

No deployments
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.

1 participant