Skip to content

[0.87] Pick the I18nUtil RTL-detection fix (wrong direction + main-thread locale scan on every Android cold start) #1438

Open

Description

@salemwch

Target Branch

0.87

Link to commit or PR to be picked

react/react-native@e46189e

Description

Requesting a pick of the I18nUtil RTL-detection fix into 0.87, as the three commits that make up its final form on main:

  1. react/react-native@e46189e - fix(android): use the active locale to detect RTL direction (fix(android): use the active locale to detect RTL direction react/react-native#57635)
  2. react/react-native@eaa9d7f - Use ConfigurationCompat for device locale lookup (Use ConfigurationCompat for device locale lookup react/react-native#58723)
  3. react/react-native@ff19940 - Remove unsupported API 23 Robolectric test (Remove unsupported API 23 Robolectric test react/react-native#58731), which removes the test Use ConfigurationCompat for device locale lookup react/react-native#58723 added

The result is identical to I18nUtil.kt on main. The change is a single private function plus the test bookkeeping.

The bug. react/react-native#53417 (in 0.82 through 0.87.1) changed isDevicePreferredLanguageRTL to Locale.getAvailableLocales()[0]. react/react-native#57635 describes the correctness side: that entry is always af, so the device language never makes isRTL true. 0.87-stable still has it, and 0.87.1 is latest on npm.

A cost that has not been reported: it adds a main-thread scan to every Android cold start. ReactSurfaceImpl's constructor calls I18nUtil.isRTL() inside ReactActivity.onCreate. With the defaults (forceRTL false, allowRTL true), that reaches Locale.getAvailableLocales(), which builds every ICU locale on the device.

Measured on a production release build (RN 0.87.1, Hermes V1, R8, profile-compiled) on an OPPO CPH1937 (Android 11, arm64):

  • Profile. simpleperf on a profileable build: ReactHostImpl.createSurface -> ReactSurfaceImpl.<init> -> I18nUtil.isRTL -> Locale.getAvailableLocales -> ICU.localesFromStrings / localeFromIcuLocaleId on the main thread.
  • atrace, ActivityOnCreate median. 82.7 ms on RN 0.81 (which used Locale.getDefault()) vs 119.9 ms on RN 0.87.1 (n=10 / 12). Every other startup phase is within +-5 ms.
  • Bare-process probe (app_process) on the same phone. getAvailableLocales() returns 755 locales with af first; the first call costs 15.9-17.4 ms, later calls ~0 ms. Inside the app during startup the measured cost is larger (JIT compiling the ICU parsers runs concurrently in every trace).
  • Control. When isRTL short-circuits (allowRTL(false) persisted, or forceRTL(true)), the ICU work disappears from the traces (24/24 before, 0/12 after) and cold start drops: paired medians 641 -> 617 ms and 655 -> 625 ms (n=15 each).

The app-side workaround is to always persist allowRTL / forceRTL explicitly so isRTL never reaches the device-locale branch. But that is not the default, so apps on 0.87 that never call those APIs (most LTR apps) pay it on every cold start, and fresh installs always pay it once.

The fix is small, already on main and in 0.88, and swaps an O(all locales) lookup for a direct read of the configuration's first locale - so low risk for a patch release.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions