Skip to content

Add API.eager_load! to compile an API and load its I18n translations - #3019

Merged
ericproulx merged 1 commit into
masterfrom
perf/eager-load-i18n
Nov 8, 2026
Merged

ericproulx merged 1 commit into
masterfrom
perf/eager-load-i18n

Conversation

@ericproulx

@ericproulx ericproulx commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

What

API.eager_load! compiles an API and loads I18n's translations, so an app can do both at boot:

Twitter::API.eager_load!

compile! and request handling are unchanged. There is no global setting.

Why

I18n loads lazily, so the first error a process answers pays for the load. Measured in a fresh process after API.compile!: first 400 3.65 ms lazy vs 0.87 ms loaded. Steady state is unchanged (0.095 vs 0.084 ms).

Why a method, not a setting

An earlier version of this PR added Grape.config.eager_load_i18n and loaded the translations in Instance#initialize. As @dblock pointed out, compile! also runs lazily on the first request, so with the setting on, the load moved onto that request, even a successful one, rather than to boot. An explicit eager_load! only runs when the app calls it.

Rails already loads I18n at boot when config.eager_load is on (ActiveSupport's I18n railtie registers config.eager_load_namespaces << I18n). Calling an API's eager_load! from Rails would need explicit integration, left to a separate change.

How the backend is loaded

  • Simple backend (I18n's default, and LazyLoadable): translations(do_init: true), the same load its first lookup would run. eager_load! is avoided on purpose:
    • it sets @eager_loaded, so every later I18n.reload! loads at once instead of on the next lookup;
    • a LazyLoadable backend set to lazy_load: true raises UnsupportedMethod on it.
  • Any other backend (Chain, KeyValue, …): eager_load! if it answers it. Their translations takes no do_init:. A backend that does not inherit I18n::Backend::Base may have no eager_load!, and is left to its first lookup.
  • Why not load only Grape's en.yml: I18n.backend.load_translations(path) reads just that file, but it does not mark the backend as loaded. The first lookup still reads the whole load_path, Grape's file included, and overwrites any key an app stored over Grape's.

Notes

  • Not a setup step: like compile!, eager_load! is in NON_OVERRIDABLE and delegated to the base instance, so mounting the API does not replay it.
  • Adding locale files later: a file appended to I18n.load_path with << after eager_load! is read only after I18n.reload!. Assigning the path (I18n.load_path += [path]) drops the loaded translations, and the next lookup reads them again. The README's I18n section says so, and the compile! section points to it.
  • grape-on-rack: the benchmark rackup calls Acme::API.eager_load! instead of compile!.
  • No UPGRADING entry: the method is additive.

Tests

  • eager_load! compiles the API that serves requests, and loads the translations.
  • A first request that succeeds, compiling the API lazily, reads no translations.
  • Mounting an API after eager_load! does not run it again.
  • A later I18n.reload! leaves the translations to the next lookup.
  • A LazyLoadable backend with lazy_load: true loads its current locale; a Chain backend is loaded through eager_load!; a backend with no eager_load! still answers.
  • Mutation-checked: no translation load, no compile, eager_load! recorded as a setup step, the load moved into Instance#initialize, eager_load! for every backend, do_init: for every backend, and no respond_to? guard each fail a spec.
  • Full RSpec suite passes locally (3193 examples, 0 failures); RuboCop clean.
  • The grape-on-rack rackup boots with I18n loaded.
  • CI green, including the Rails gemfiles.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Danger Report

No issues found.

View run

@github-actions

Copy link
Copy Markdown

Danger Report

No issues found.

View run

@ericproulx
ericproulx force-pushed the perf/eager-load-i18n branch 4 times, most recently from 9240107 to 1e2f663 Compare September 29, 2026 07:27
@ericproulx ericproulx changed the title Load I18n translations when an API is compiled Add a setting to load I18n translations when an API is compiled Sep 29, 2026
@dblock

dblock commented Sep 29, 2026

Copy link
Copy Markdown
Member

I think we should we provide eager_load! to match Rails and automatically call it when Rails.eager_load! is.

@ericproulx

Copy link
Copy Markdown
Contributor Author

I think we should we provide eager_load! to match Rails and automatically call it when Rails.eager_load! is.

Rails is already doing calling it through i18n. This is only when it's not a Rails app. I like the eager_load! load though.

@dblock

dblock commented Sep 30, 2026

Copy link
Copy Markdown
Member

I like the eager_load! load though.

Rename?

@ericproulx
ericproulx force-pushed the perf/eager-load-i18n branch 5 times, most recently from e3f5d35 to 291702f Compare October 17, 2026 20:42
@dblock

dblock commented Oct 19, 2026

Copy link
Copy Markdown
Member

Could we expose this through an explicit Twitter::API.eager_load! rather than introducing Grape.config.eager_load_i18n and coupling translation loading to Instance#initialize?

The first-error improvement makes sense, but compile! also runs lazily on the first request. With the setting enabled, this can move translation loading onto the first request—even a successful one—rather than necessarily moving it to boot.

An explicit eager_load! could load translations (guarding for backend support) and compile the API, leaving compile! and normal request handling unchanged. Applications outside Rails could call it at boot after configuring their locale paths, without another global setting.

Rails already eager-loads I18n when application eager loading is enabled. Automatically invoking Grape API eager loading from Rails would need explicit integration; naming the method eager_load! alone would not do that. That integration could be addressed separately.

— AI posting on behalf of @dblock

@ericproulx
ericproulx force-pushed the perf/eager-load-i18n branch from 291702f to 5ce1cc2 Compare October 19, 2026 12:47
@ericproulx
ericproulx force-pushed the perf/eager-load-i18n branch 2 times, most recently from 4bc4928 to dd8e887 Compare November 7, 2026 13:26
@ericproulx ericproulx changed the title Add a setting to load I18n translations when an API is compiled Add API.eager_load! to compile an API and load its I18n translations Nov 7, 2026
@ericproulx

Copy link
Copy Markdown
Contributor Author

Done: the setting is gone, and Twitter::API.eager_load! now compiles the API and loads the translations. compile! and requests are unchanged, and it's kept out of the recorded setup steps, so mounting doesn't replay it. One more change: it uses translations(do_init: true) on Simple backends rather than eager_load!. eager_load! makes every later I18n.reload! load at once, and it raises on LazyLoadable(lazy_load: true). Rails integration can come in a separate change.

@ericproulx
ericproulx force-pushed the perf/eager-load-i18n branch from dd8e887 to 5af5ffc Compare November 7, 2026 16:11
@ericproulx
ericproulx force-pushed the perf/eager-load-i18n branch 2 times, most recently from a022324 to 7d99559 Compare November 8, 2026 20:15
I18n reads its locale files on the first lookup, so in an app that
precompiles its API the first error a process answered still paid for the
load: 3.65 ms against 0.87 ms once the translations were in memory.
API.eager_load! compiles the API and loads the translations, so an app
calling it at boot pays for both there.

It is an explicit call rather than a setting read whenever an API is
compiled, because compile! also runs lazily on the first request: tied to
it, the load would land on that request, even a successful one. compile!
and request handling are unchanged. Rails already loads I18n at boot when
config.eager_load is on; calling an API's eager_load! from Rails is left
to a separate change.

A Simple backend is loaded with translations(do_init: true), the load its
first lookup would run, rather than eager_load!: that one also flags the
backend so every later I18n.reload! loads at once, and raises on a
LazyLoadable backend set to lazy_load. Loading only Grape's own locale
file would not help, as the first lookup still reads the whole load path,
Grape's file included. Any other backend is eager-loaded when it answers
eager_load!, and left to its first lookup otherwise.

Like compile!, eager_load! is kept off the setup steps an API records, so
mounting the API does not run it again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ericproulx
ericproulx force-pushed the perf/eager-load-i18n branch from 7d99559 to 2eb5160 Compare November 8, 2026 20:17
@ericproulx
ericproulx merged commit 56a868b into master Nov 8, 2026
51 checks passed
@ericproulx
ericproulx deleted the perf/eager-load-i18n branch November 8, 2026 20:37
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.

2 participants