Repository navigation
Description
Bug Description
- I have run
gradle cleanand confirmed this bug does not occur with JSC
Hermes version: (whatever's in RN 0.70.6)
React Native version (if any): 0.70.6 (the current latest)
OS version (if any): Android 13
Platform (most likely one of arm64-v8a, armeabi-v7a, x86, x86_64): arm64-v8a
Steps To Reproduce
- Clone https://github.com/gnprice/hermes-localecompare-repro .
- Run it with
npx react-native run-android. - Observe the log lines showing calls to
String.prototype.localeComparetaking multiple seconds each. For example:
LOG sorting...
LOG slow localeCompare: 8836ms for 'vihmdxim' vs 'vlqkdtzn'
LOG slow localeCompare: 2078ms for 'ezkvpvbg' vs 'ivkpfinh'
LOG slow localeCompare: 6568ms for 'ctanfzkj' vs 'ctjhzdyi'
LOG slow localeCompare: 7639ms for 'hgyzpilj' vs 'hhpnlwqf'
LOG slow localeCompare: 7337ms for 'jzninmrt' vs 'orbuzdcq'
LOG slow localeCompare: 6872ms for 'rnarhxnr' vs 'rnauqlwl'
LOG slow localeCompare: 3648ms for 'nfdeptde' vs 'nfgpjekx'
LOG ...done, took 56938ms
Those extremely slow calls are rare relative to the total number of calls. But because they're so extremely slow, they account for most of the total time — about 75% of the whole sort in this example.
code example
The repro repo above just consists of a fresh React Native app on the latest version (from npx react-native init), with the following code added at the top of the render function:
const randInt = (end, start = 0) =>
Math.floor(Math.random() * (end - start) + start);
const alphabet = 'abcdefghijklmnopqrstuvwxyz';
const names = Array.from({length: 30000}, () =>
Array.from({length: 8}, () => alphabet[randInt(alphabet.length)]).join(''),
);
console.log('sorting...');
const s = Date.now();
names.sort(localeCompare);
console.log(`...done, took ${Date.now() - s}ms`);
function localeCompare(a, b) {
const s = Date.now();
const result = a.localeCompare(b);
const e = Date.now();
if (e - s > 100) {
console.log(`slow localeCompare: ${e - s}ms for '${a}' vs '${b}'`);
}
return result;
}That is, it makes up a long list of boring short alphabetic strings, and sorts them. The comparator just calls String.prototype.localeCompare, and times the call.
The Expected Behavior
Calling localeCompare on a string should be fast — certainly far less than a second, for any non-gigantic strings.
Other notes
I do not reproduce the issue when running in an emulated Android device, on x86_64. So it may be architecture-dependent.
Activity
tmikov commentedon Dec 17, 2022
The following can be executed locally without the need of a RN app:
The results are shocking:
gnprice commentedon Jan 11, 2023
Yeah. Thanks for that additional demo.
So it seems there are two different symptoms here:
In general,
String.prototype.localeCompareis quite slow — well over 10x slower than in V8 or JSC.This is the one demonstrated in the above comment.
A small fraction of the time, and apparently only on some architectures (yes on arm64, no on x86_64),
localeCompareis extremely slow. This is quite infrequent (like 1 in ~10000 calls); but when it does happen, a single call takes thousands of milliseconds.This is the one demonstrated in my report at the top.
Because the time for a single call is so very long, this issue makes it risky to use
localeCompareat all, at least in any React Native app or anywhere else where the JS code is responsible for a UI — because using it means that occasionally the UI will go unresponsive for multiple seconds.The extremely slow calls are also slow enough that when sorting a long list, the small number of extremely slow calls take more time than all the rest — even though the others are themselves slow compared to V8 or JSC.
tmikov commentedon Jan 11, 2023
It is interesting that this is reproducible both on MacOS and Android, since on Android localeCompare forwards to a Java implementation, while on MacOS it uses CoreFoundation. It appears that both are quite slow.
neildhar commentedon Jan 11, 2023
I did some quick profiling on macOS, which suggests there is likely some low hanging fruit for optimisation. Specifically, with Intl disabled, I see most of the time being spent in
CFStringCompareWithOptionsAndLocale. However, when Intl is enabled:CollatorSo it seems like we could achieve a dramatic speedup if we:
Collatorwhen Intl is enabledmajugurci commentedon Dec 27, 2023
I have also bumped into this situation in react native using hermes.
I had a list of objects which needed to be sort in locale.
Using this code:
it would take few seconds to finish sorting for 1000 items.
On production where there are 10000 items it would take almost a minute.
When changed to this:
it would be over really fast. I haven't messured times but it is super fast compared to localeCompare.
Also I noticed if I leave the locale in localeCompare it is working just as fast as collator:
So i guess there are some optimizations for default locale, probably the collator is reused when using default locale.
Anyway I wrote this just if it can help to track the issue and give more insight.
jamonholmgren commentedon Dec 29, 2025
I wonder if this would be a good candidate for a new Hermes extension, moving to the
Intl.Collatorapproach that @majugurci mentioned.EDIT: in our internal tests, the
Intl.Collatorapproach is faster but still not fast enough. We ended up going with a crude (and limited) but very fast string compare instead -- something like this:AI091 commentedon Nov 13, 2026
The rare multi-second calls reproduce on arm64 Android, and they are garbage-collector waits caused by the same per-call collator creation that makes
localeCompareslow in general. PR: #2320.What happens. On Android, every
localeComparecall creates a newcom.facebook.hermes.intl.Collatorthrough JNI: locale resolution, an ICU collator clone, and several finalizable objects. That is about 2 KB of Java garbage per call, or about 950 MB for the 30,000-string sort above. When ART's concurrent collector cannot keep up, the next allocation on the JavaScript thread blocks until the collection finishes, and that onelocaleComparecall takes seconds.Repro. tmikov's sort above (30,000 strings, 474,595 comparisons, every call timed), run inside the Android Intl test app with ART's collector counters around the sort. On Firebase Test Lab's arm64 image (
MediumPhone.arm, API 34), Hermesstatic_h:The slowest calls match ART's log lines on the JavaScript thread to the millisecond:
On the x86_64 emulator, the same sort has no blocking collections and its slowest call is 5 ms. It is still slow overall (6.7-8.5 s, about 1 GB of Java garbage). That fits "arm64 yes, x86_64 no" above. Our reading, not tested: whether a device blocks depends on how fast its collector runs compared with the allocation rate, not on the instruction set.
Passing a locale is much worse.
a.localeCompare(b, 'en')goes through the best-fit locale matcher on every call. On the emulator, the same sort took 617.7 s and allocated 103.7 GB of Java objects (41,196 collections). That matches the "a few seconds for 1000 items" report above.In a production app. In a React Native 0.88 app on Android,
Collator.<init>was the largest single JavaScript cost we profiled, and most of the app's Java garbage was collators waiting to be finalized. On a physical Galaxy A03s (133 MB Java heap), the UI thread waited up to 22.8 s at a time for the collector. When the app stopped callinglocaleCompare, those waits went away on the A03s and on a Galaxy A54.With the fix (reuse the collator while locales, options and the default locale stay the same), the same arm64 run: sort 0.36-0.41 s, slowest call 9 ms, no blocking collections, 29 MB of Java allocation. With
'en': 0.39-0.44 s. Full numbers are in the PR.Not yet measured: the patched engine on a physical phone. We had no device quota left. The phone evidence above comes from an app that stopped calling
localeCompare, not from the patched engine.Workaround until a release has the fix: create one collator and reuse it.
new Intl.Collator().comparesorted the same strings in 0.23 s on the arm64 image without the fix, with no blocking collections: