Skip to content

Find a Text's clickable spans only when a touch needs them - #59583

Open
hryhoriiK97 wants to merge 2 commits into
react:mainfrom
hryhoriiK97:android-text-lazy-clickable-spans
Open

hryhoriiK97 wants to merge 2 commits into
react:mainfrom
hryhoriiK97:android-text-lazy-clickable-spans

Conversation

@hryhoriiK97

Copy link
Copy Markdown
Contributor

Summary:

PreparedLayoutTextView collects the ClickableSpans of its text every time its prepared layout changes, which is every time the text is updated: it walks the text with nextSpanTransition, calls getSpans for each segment and copies the results into a list. The list itself is never read. Touch handling only checks whether it is empty before hit testing a tap, and focus handling only checks it before clearing the selection.

With this change, touch handling checks whether the text has a ClickableSpan on its first touch after a layout change, with a single getSpans, and keeps the answer until the next layout change. Losing focus clears the selection when there is one, which is the state it resets, instead of looking at the spans. Texts that are never touched no longer pay for the scan.

This is the main thread work done for every update of a Text whose spannable has spans, so it adds up on screens where many texts change often (lists of messages, prices, scores, counters).

Changelog:

[ANDROID] [CHANGED] - Find the clickable spans of a Text only when it is touched, instead of on every text update

Test Plan:

  • New PreparedLayoutTextViewClickableSpanTest: tapping a ClickableSpan clicks it; after the text is replaced by one without clickable spans a tap no longer clicks; after the text is replaced by one with a clickable span a tap clicks it. ./gradlew :packages:react-native:ReactAndroid:testDebugUnitTest --tests 'com.facebook.react.views.text.*' passes (104 tests).
  • yarn format-check-kotlin reports no changes.
  • On a device. Two RNTester release APKs built from the same local checkout (with local benchmark screens, and PreparedLayoutTextView.kt identical to main at 9bc1a28), without and with this change, installed alternately on a moto g57 power (Android 16). Each install first opens a screen with a static text containing a nested <Text onPress> link and taps the link: onPress fired once on every install of both builds (10 installs). Then a Playground screen of 200 rich text rows (bold, colored, italic and a link per row) where every row changes every 100 ms runs for 30 s, and the main and JS thread CPU time is read from /proc:
before this PR change
Main thread, 30 s 4.88 / 5.01 s 4.61 / 4.64 s −6.5%
JS thread, 30 s 15.00 / 14.66 s 14.67 / 14.69 s no change

Three earlier rounds, with a build that differed only in the focus check (which this screen never runs), gave the same result: main thread 4.97 / 5.15 / 4.93 s before and 4.68 / 4.65 / 4.70 s after (−6.8%).

In a simpleperf profile of the same screen before this change, collecting the spans (nextSpanTransition, getSpans and the list copies in the preparedLayout setter, called from PreparedLayoutTextViewManager.updateExtraData) was about 5% of the main thread.

  • Not covered by a unit test: the focus change path, which now checks selection != null (the selection is private).

🤖 Generated with Claude Code

PreparedLayoutTextView collected the ClickableSpans of its text every time
the prepared layout changed, but the list was only checked for being empty,
by touch and focus handling. Touch handling now checks whether the text has
a ClickableSpan on first use and forgets the answer when the layout changes.
Losing focus clears the selection when there is one, which is the state it
resets, instead of looking at the spans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Oct 28, 2026
@facebook-github-tools facebook-github-tools Bot added p: Software Mansion Partner: Software Mansion Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team. labels Oct 28, 2026
@github-actions

Copy link
Copy Markdown

Visual regression

No visual differences at 4cd2d06bdab8.

ios: no capture

@meta-codesync

meta-codesync Bot commented Oct 29, 2026

Copy link
Copy Markdown

@andrewdacenko has imported this pull request. If you are a Meta employee, you can view this in D128363239.

@christophpurrer christophpurrer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review automatically exported from Phabricator review in Meta.

fun recycleView() {
BackgroundStyleApplicator.reset(this)
overflow = Overflow.VISIBLE
clickableSpans = emptyList()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The old code explicitly cleared clickableSpans = emptyList() here. Consider adding hasClickableSpansCache = null for defensive consistency — if a recycled view ever receives a touch before a new preparedLayout is set, the cache would still hold the stale value from the previous text.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andrewdacenko done in e5873ce: recycleView() now clears hasClickableSpansCache too, and I added a test for it. Thanks!

Setting `preparedLayout` to null already resets it, but clear it explicitly
too, like the other per-text state in recycleView().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

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

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. p: Software Mansion Partner: Software Mansion Partner Shared with Meta Applied via automation to indicate that an Issue or Pull Request has been shared with the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants