Repository navigation
Convert JS strings passed to iOS native modules without UTF-8 - #59422
Open
hryhoriiK97 wants to merge 1 commit into
Open
hryhoriiK97 wants to merge 1 commit into
hryhoriiK97 wants to merge 1 commit into
Conversation
convertJSIStringToNSString read every JS string argument (including object keys and string values inside objects and arrays) as UTF-8 and decoded it again into an NSString. Copy the string's own ASCII or UTF-16 characters with jsi::String::getStringData instead, and give JSCRuntime a native getStringData that passes the JSString's UTF-16 characters directly. Strings with a lone surrogate or a leading byte order mark, or that arrive in several chunks, still go through UTF-8, so every string converts to the same NSString as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary:
This is the other direction of #59325.
On iOS, every JS string that reaches a native module goes through
convertJSIStringToNSString(ReactCommon/react/nativemodule/core/platform/ios/ReactCommon/RCTTurboModule.mm): string arguments, every key of an object argument, and string values inside objects and arrays. It read the string withjsi::String::utf8, so the runtime first converted its own characters to UTF-8, and then-[NSString initWithBytes:length:encoding:NSUTF8StringEncoding]decoded that UTF-8 again. For text with accented letters or emoji, both conversions run over the whole string.With this change.
jsi::String::getStringData. Hermes implements it natively and passes its ASCII or UTF-16 characters directly; they are copied once, with-initWithBytes:length:encoding:NSASCIIStringEncodingor-initWithCharacters:length:.NSStringwith a lone surrogate has none:UTF8StringreturnsNULL, so for example afetchbody with one would be dropped. Finding one is a single pass over the characters, and it costs nothing measurable. A string that arrives in several chunks also goes through UTF-8; no runtime in the repository splits strings.JSCRuntimegets a nativegetStringDatathat passes theJSStringRef's UTF-16 characters (JSStringGetCharactersPtr). Without it, JSC would use JSI's defaultgetStringData, which converts to UTF-8 and then to UTF-16, and the new path would be up to 2.6× slower on JSC than before.Every string converts to the same
NSStringas before, including a\0(kept since #57906), lone surrogates and a leading byte order mark.A runtime that doesn't implement
getStringDataitself uses JSI's default, which converts to UTF-8 and then to UTF-16, so on such a runtime this conversion is slower than before, mostly for ASCII text. That applies to runtimes outside this repository, such as@react-native-community/javascriptcore; the same few lines as inJSCRuntimewould fix it there.Changelog:
[IOS] [CHANGED] - Strings passed from JS to native modules convert faster
Test Plan:
clang-format --dry-run --Werror(the repository's pinned version) on both files passes.xcodebuildin Release and run on a physical iPhone 15 Pro (iOS 26.6.1), with Hermes:getStringand compared with what comes back, with and without this change: an empty string,"a\x00b", emoji, accented letters, Japanese, and mixed text come back unchanged in both;"x\uD800y"(a lone surrogate) comes back with the surrogate replaced, in both.JSCRuntime.cppandjsi.cppconverts ASCII, empty,\0, lone-surrogate, emoji, accented, Japanese and concatenated strings, and strings with a byte order mark at the start or inside, with the old and the new function, and every result is the same. Old → new, best of 9 rounds: ASCII −34% at 16 characters to −58% at 10 KB; accented text −61% to −97% (10 KB: 24.8 → 0.72 µs).NativeSampleTurboModule.getStringwith JSON-like text in ASCII or with accented letters, andgetObjectwith 20 keys whose values are 24-character strings, timed withperformance.now()(20,000 calls up to 128 characters, 5,000 at 1 KB and for the objects, 1,000 at 10 KB, 200 at 100 KB, 20 at 1 MB). The same Release binary, this PR's code plus a launch argument that switches back to the old conversion, alternating, 4 launches each after a warm-up launch. The presses were driven by Argent on the physical iPhone.getStringalso returns the string, which since Convert NSStrings returned to JS to UTF-8 only once #59325 is a single copy, so the times below include that return. Mean time per call:For every row, the ranges of the two arms don't overlap. For 16-character strings, the change was −8% with accented letters and within noise for ASCII. An earlier run of the same comparison (5 and 6 launches, up to 10 KB) gave the same results within 3 points.
fetchbody. In the same launches, the JS-thread time ofXMLHttpRequest.send(body)with a 1 MB JSON body, which converts the body to anNSStringforRCTNetworkingbefore anything is sent (median of 25 sends per launch, posted to a closed local port so the network isn't timed):🤖 Generated with Claude Code