diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java index b63deffd042..40d777c5b23 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java @@ -75,6 +75,9 @@ public class ReactFeatureFlags { /** Feature Flag to enable the pending event queue in fabric before mounting views */ public static boolean enableFabricPendingEventQueue = false; + /** Feature Flag to enable caching mechanism of text measurement at shadow node level */ + public static boolean enableTextMeasureCachePerShadowNode = false; + /** * Feature flag that controls how turbo modules are exposed to JS * diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp index 51999c97a29..36a78a671da 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/Binding.cpp @@ -429,6 +429,9 @@ void Binding::installFabricUIManager( "CalculateTransformedFramesEnabled", getFeatureFlagValue("calculateTransformedFramesEnabled")); + CoreFeatures::cacheLastTextMeasurement = + getFeatureFlagValue("enableTextMeasureCachePerShadowNode"); + // Props setter pattern feature CoreFeatures::enablePropIteratorSetter = getFeatureFlagValue("enableCppPropsIteratorSetter"); diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.cpp index fb55485d037..1f4cbc4a497 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.cpp @@ -15,6 +15,19 @@ TextMeasurement ParagraphLayoutManager::measure( AttributedString const &attributedString, ParagraphAttributes const ¶graphAttributes, LayoutConstraints layoutConstraints) const { + bool cacheLastTextMeasurement = CoreFeatures::cacheLastTextMeasurement; + if (cacheLastTextMeasurement && + (layoutConstraints.maximumSize.width == availableWidth_ || + layoutConstraints.maximumSize.width == + cachedTextMeasurement_.size.width)) { + /* Yoga has requested measurement for this size before. Let's use cached + * value. `TextLayoutManager` might not have cached this because it could be + * using different width to generate cache key. This happens because Yoga + * switches between available width and exact width but since we already + * know exact width, it is wasteful to calculate it again. + */ + return cachedTextMeasurement_; + } if (CoreFeatures::cacheNSTextStorage) { size_t newHash = folly::hash::hash_combine( 0, @@ -28,11 +41,23 @@ TextMeasurement ParagraphLayoutManager::measure( } } - return textLayoutManager_->measure( - AttributedStringBox(attributedString), - paragraphAttributes, - layoutConstraints, - hostTextStorage_); + if (cacheLastTextMeasurement) { + cachedTextMeasurement_ = textLayoutManager_->measure( + AttributedStringBox(attributedString), + paragraphAttributes, + layoutConstraints, + hostTextStorage_); + + availableWidth_ = layoutConstraints.maximumSize.width; + + return cachedTextMeasurement_; + } else { + return textLayoutManager_->measure( + AttributedStringBox(attributedString), + paragraphAttributes, + layoutConstraints, + hostTextStorage_); + } } LinesMeasurements ParagraphLayoutManager::measureLines( diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.h b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.h index c5ed8b7f5b8..d262db62b8c 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.h +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.h @@ -54,6 +54,18 @@ class ParagraphLayoutManager { std::shared_ptr mutable textLayoutManager_{}; std::shared_ptr mutable hostTextStorage_{}; + /* The width Yoga set as maximum width. + * Yoga sometimes calls measure twice with two + * different maximum width. One if available space. + * The other one is exact space needed for the string. + * This happens when node is dirtied but its size is not affected. + * To deal with this inefficiency, we cache `TextMeasurement` for each + * `ParagraphShadowNode`. If Yoga tries to re-measure with available width + * or exact width, we provide it with the cached value. + */ + Float mutable availableWidth_{}; + TextMeasurement mutable cachedTextMeasurement_{}; + size_t mutable hash_{}; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.cpp b/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.cpp index 129bc7c2a68..bda8adb91a6 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.cpp @@ -15,6 +15,7 @@ bool CoreFeatures::enableMapBuffer = false; bool CoreFeatures::blockPaintForUseLayoutEffect = false; bool CoreFeatures::useNativeState = false; bool CoreFeatures::cacheNSTextStorage = false; +bool CoreFeatures::cacheLastTextMeasurement = false; } // namespace react } // namespace facebook diff --git a/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.h b/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.h index e9672d647e2..45defe2afd1 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.h +++ b/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.h @@ -39,6 +39,11 @@ class CoreFeatures { // creating it twice. Once when measuring text and once when rendering it. // This flag caches it inside ParagraphState. static bool cacheNSTextStorage; + + // Yoga might measure multiple times the same Text with the same constraints + // This flag enables a caching mechanism to avoid subsequents measurements + // of the same Text with the same constrainst. + static bool cacheLastTextMeasurement; }; } // namespace react diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp index a8b186a5eb8..e39f6712784 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -146,7 +147,10 @@ Size measureAndroidComponentMapBuffer( TextLayoutManager::TextLayoutManager( const ContextContainer::Shared &contextContainer) : contextContainer_(contextContainer), - measureCache_(kSimpleThreadSafeCacheSizeCap) {} + measureCache_( + CoreFeatures::cacheLastTextMeasurement + ? 8096 + : kSimpleThreadSafeCacheSizeCap) {} void *TextLayoutManager::getNativeTextLayoutManager() const { return self_;