diff --git a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm index d86ce029407..77bc364889f 100644 --- a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm +++ b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm @@ -281,10 +281,6 @@ static BackgroundExecutor RCTGetBackgroundExecutor() CoreFeatures::useNativeState = true; } - if (reactNativeConfig && reactNativeConfig->getBool("react_fabric:enable_nstextstorage_caching")) { - CoreFeatures::cacheNSTextStorage = true; - } - if (reactNativeConfig && reactNativeConfig->getBool("react_fabric:cancel_image_downloads_on_recycle")) { CoreFeatures::cancelImageDownloadsOnRecycle = true; } 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 05eccce40f4..3ee10204917 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 @@ -97,9 +97,6 @@ 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 08d1fa767ee..e8139b5380f 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 @@ -421,8 +421,6 @@ void Binding::installFabricUIManager( "CalculateTransformedFramesEnabled", getFeatureFlagValue("calculateTransformedFramesEnabled")); - CoreFeatures::cacheLastTextMeasurement = - getFeatureFlagValue("enableTextMeasureCachePerShadowNode"); CoreFeatures::enablePropIteratorSetter = getFeatureFlagValue("enableCppPropsIteratorSetter"); CoreFeatures::useNativeState = getFeatureFlagValue("useNativeState"); 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 1f4cbc4a497..0b622be5798 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.cpp @@ -7,6 +7,7 @@ #include "ParagraphLayoutManager.h" #include +#include #include namespace facebook::react { @@ -15,40 +16,18 @@ 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, - textAttributedStringHashLayoutWise(attributedString), - paragraphAttributes); + if (CoreFeatures::cacheLastTextMeasurement) { + bool shouldMeasure = shoudMeasureString( + attributedString, paragraphAttributes, layoutConstraints); - if (!hostTextStorage_ || newHash != hash_) { - hostTextStorage_ = textLayoutManager_->getHostTextStorage( - attributedString, paragraphAttributes, layoutConstraints); - hash_ = newHash; + if (shouldMeasure) { + cachedTextMeasurement_ = textLayoutManager_->measure( + AttributedStringBox(attributedString), + paragraphAttributes, + layoutConstraints, + hostTextStorage_); + lastAvailableWidth_ = layoutConstraints.maximumSize.width; } - } - - if (cacheLastTextMeasurement) { - cachedTextMeasurement_ = textLayoutManager_->measure( - AttributedStringBox(attributedString), - paragraphAttributes, - layoutConstraints, - hostTextStorage_); - - availableWidth_ = layoutConstraints.maximumSize.width; return cachedTextMeasurement_; } else { @@ -56,10 +35,43 @@ TextMeasurement ParagraphLayoutManager::measure( AttributedStringBox(attributedString), paragraphAttributes, layoutConstraints, - hostTextStorage_); + nullptr); } } +bool ParagraphLayoutManager::shoudMeasureString( + AttributedString const &attributedString, + ParagraphAttributes const ¶graphAttributes, + LayoutConstraints layoutConstraints) const { + size_t newHash = folly::hash::hash_combine( + 0, + textAttributedStringHashLayoutWise(attributedString), + paragraphAttributes); + + if (newHash != paragraphInputHash_) { + // AttributedString or ParagraphAttributes have changed. + // Must create new host text storage and trigger measure. + hostTextStorage_ = textLayoutManager_->getHostTextStorage( + attributedString, paragraphAttributes, layoutConstraints); + paragraphInputHash_ = newHash; + return true; // Must measure again. + } + + bool hasMaximumSizeChanged = + layoutConstraints.maximumSize.width != lastAvailableWidth_; + Float threshold = 0.01; + bool doesMaximumSizeMatchLastMeasurement = + std::abs( + layoutConstraints.maximumSize.width - + cachedTextMeasurement_.size.width) < threshold; + if (hasMaximumSizeChanged && !doesMaximumSizeMatchLastMeasurement) { + hostTextStorage_ = textLayoutManager_->getHostTextStorage( + attributedString, paragraphAttributes, layoutConstraints); + return true; + } + return false; +} + LinesMeasurements ParagraphLayoutManager::measureLines( AttributedString const &attributedString, ParagraphAttributes const ¶graphAttributes, 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 d262db62b8c..9583aae501b 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.h +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.h @@ -52,20 +52,41 @@ class ParagraphLayoutManager { private: std::shared_ptr mutable textLayoutManager_{}; + + /* + * Stores opaque pointer to `NSTextStorage` on iOS. nullptr on Android. + * TODO: In the future, we may want to cache Android's text storage. + */ std::shared_ptr mutable hostTextStorage_{}; + /* + * Hash of AttributedString and ParagraphAttributes last used to + * measure. Result of that measure is stored in cachedTextMeasurement_. + * The available width defined for the measurement is stored in + * lastAvailableWidth_. + */ + size_t mutable paragraphInputHash_{}; + /* The width Yoga set as maximum width. - * Yoga sometimes calls measure twice with two - * different maximum width. One if available space. + * Yoga calls measure twice with two + * different maximum width. One of 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_{}; + Float mutable lastAvailableWidth_{}; TextMeasurement mutable cachedTextMeasurement_{}; - size_t mutable hash_{}; + /* + * Checks whether the inputs into text measurement meaningfully affect + * text measurement result. Returns true if inputs have changed and measure is + * needed. + */ + bool shoudMeasureString( + AttributedString const &attributedString, + ParagraphAttributes const ¶graphAttributes, + LayoutConstraints layoutConstraints) const; }; } // 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 5f0abf2f4fc..937e40aa0bf 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.cpp @@ -13,7 +13,6 @@ bool CoreFeatures::enablePropIteratorSetter = false; bool CoreFeatures::enableMapBuffer = false; bool CoreFeatures::blockPaintForUseLayoutEffect = false; bool CoreFeatures::useNativeState = false; -bool CoreFeatures::cacheNSTextStorage = false; bool CoreFeatures::cacheLastTextMeasurement = false; bool CoreFeatures::cancelImageDownloadsOnRecycle = false; diff --git a/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.h b/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.h index 36d31cd18ef..5c49928e226 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.h +++ b/packages/react-native/ReactCommon/react/renderer/core/CoreFeatures.h @@ -34,14 +34,10 @@ class CoreFeatures { // in simple data passing scenarios with JS static bool useNativeState; - // Creating NSTextStorage is relatively expensive operation and we were - // 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. + // On iOS, we also cache NSTextStorage. static bool cacheLastTextMeasurement; // Fabric was not cancelling image downloads when was removed diff --git a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp index 90d558985b9..f800ed581a6 100644 --- a/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp +++ b/packages/react-native/ReactCommon/react/renderer/scheduler/Scheduler.cpp @@ -131,6 +131,9 @@ Scheduler::Scheduler( CoreFeatures::blockPaintForUseLayoutEffect = reactNativeConfig_->getBool( "react_fabric:block_paint_for_use_layout_effect"); + CoreFeatures::cacheLastTextMeasurement = + reactNativeConfig_->getBool("react_fabric:enable_text_measure_cache"); + if (animationDelegate != nullptr) { animationDelegate->setComponentDescriptorRegistry( componentDescriptorRegistry_);