From 4b06dca85031c7a669324f7533519e393bdbc4d7 Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Fri, 3 Jul 2026 13:50:27 +0200 Subject: [PATCH 1/4] perf(ios): cache fontScale to avoid dispatch_sync on every measurement ENRMFontScaleForMeasurement previously called dispatch_sync to the main thread on every Yoga measurement to read RCTFontSizeMultiplier(). In a scrolling feed with many markdown cells, this added one synchronous main-thread hop per cell per layout pass. Cache the value in a std::atomic after the first read and update it via UIContentSizeCategoryDidChangeNotification. Subsequent measurements read the atomic directly with no thread synchronization. Co-authored-by: Cursor --- .../ios/internals/ShadowMeasurementUtils.h | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/react-native-enriched-markdown/ios/internals/ShadowMeasurementUtils.h b/packages/react-native-enriched-markdown/ios/internals/ShadowMeasurementUtils.h index 5f1e59734..470de0a01 100644 --- a/packages/react-native-enriched-markdown/ios/internals/ShadowMeasurementUtils.h +++ b/packages/react-native-enriched-markdown/ios/internals/ShadowMeasurementUtils.h @@ -4,6 +4,7 @@ #import #import #include +#include #include #include #include @@ -21,16 +22,32 @@ static inline CGFloat ENRMFontScaleForMeasurement(bool allowFontScaling) return 1.0; } - __block CGFloat fontScale = 1.0; - void (^readFontScale)(void) = ^{ fontScale = RCTFontSizeMultiplier(); }; + static std::atomic cachedScale{0.0}; + static std::once_flag flag; - if ([NSThread isMainThread]) { - readFontScale(); - } else { - dispatch_sync(dispatch_get_main_queue(), readFontScale); - } + std::call_once(flag, [] { + __block CGFloat scale = 1.0; + void (^readScale)(void) = ^{ scale = RCTFontSizeMultiplier(); }; - return fontScale; + if ([NSThread isMainThread]) { + readScale(); + } else { + dispatch_sync(dispatch_get_main_queue(), readScale); + } + cachedScale.store(scale, std::memory_order_relaxed); + + dispatch_async(dispatch_get_main_queue(), ^{ + [[NSNotificationCenter defaultCenter] + addObserverForName:UIContentSizeCategoryDidChangeNotification + object:nil + queue:[NSOperationQueue mainQueue] + usingBlock:^(NSNotification *) { + cachedScale.store(RCTFontSizeMultiplier(), std::memory_order_relaxed); + }]; + }); + }); + + return cachedScale.load(std::memory_order_relaxed); } static inline Size ENRMClampMeasuredSize(CGSize size, const LayoutConstraints &layoutConstraints) From 82e8c6fe0557086e58b507d37b4482ddb9362577 Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Fri, 3 Jul 2026 14:00:45 +0200 Subject: [PATCH 2/4] fix(macos): guard UIContentSizeCategoryDidChangeNotification with !TARGET_OS_OSX Co-authored-by: Cursor --- .../ios/internals/ShadowMeasurementUtils.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-native-enriched-markdown/ios/internals/ShadowMeasurementUtils.h b/packages/react-native-enriched-markdown/ios/internals/ShadowMeasurementUtils.h index 470de0a01..f3794ea35 100644 --- a/packages/react-native-enriched-markdown/ios/internals/ShadowMeasurementUtils.h +++ b/packages/react-native-enriched-markdown/ios/internals/ShadowMeasurementUtils.h @@ -36,6 +36,7 @@ static inline CGFloat ENRMFontScaleForMeasurement(bool allowFontScaling) } cachedScale.store(scale, std::memory_order_relaxed); +#if !TARGET_OS_OSX dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] addObserverForName:UIContentSizeCategoryDidChangeNotification @@ -45,6 +46,7 @@ static inline CGFloat ENRMFontScaleForMeasurement(bool allowFontScaling) cachedScale.store(RCTFontSizeMultiplier(), std::memory_order_relaxed); }]; }); +#endif }); return cachedScale.load(std::memory_order_relaxed); From c8a53654c1c57ac03efa306b005e95c5d6a197fd Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Fri, 3 Jul 2026 14:15:05 +0200 Subject: [PATCH 3/4] fix: use external linkage for ENRMFontScaleForMeasurement static inline gives each translation unit its own cachedScale and once_flag, causing duplicate dispatch_sync calls and notification observers. Plain inline (C++17) shares a single definition and static locals across all TUs. Co-authored-by: Cursor --- .../ios/internals/ShadowMeasurementUtils.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-enriched-markdown/ios/internals/ShadowMeasurementUtils.h b/packages/react-native-enriched-markdown/ios/internals/ShadowMeasurementUtils.h index f3794ea35..49bb8270e 100644 --- a/packages/react-native-enriched-markdown/ios/internals/ShadowMeasurementUtils.h +++ b/packages/react-native-enriched-markdown/ios/internals/ShadowMeasurementUtils.h @@ -16,7 +16,7 @@ namespace facebook::react { // synchronously joins the layout queue from main. A synchronous // layout flush from main would deadlock. -static inline CGFloat ENRMFontScaleForMeasurement(bool allowFontScaling) +inline CGFloat ENRMFontScaleForMeasurement(bool allowFontScaling) { if (!allowFontScaling) { return 1.0; From 2b7a4e9d17531e91b201048b1a405cd32d60f6f3 Mon Sep 17 00:00:00 2001 From: Gregory Moskaliuk Date: Fri, 3 Jul 2026 14:16:41 +0200 Subject: [PATCH 4/4] fix: add missing includes for mutex and TargetConditionals std::once_flag/std::call_once require , and the #if !TARGET_OS_OSX guard requires to be explicitly included rather than relying on transitive includes. Co-authored-by: Cursor --- .../ios/internals/ShadowMeasurementUtils.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/react-native-enriched-markdown/ios/internals/ShadowMeasurementUtils.h b/packages/react-native-enriched-markdown/ios/internals/ShadowMeasurementUtils.h index 49bb8270e..1d4e1d9bf 100644 --- a/packages/react-native-enriched-markdown/ios/internals/ShadowMeasurementUtils.h +++ b/packages/react-native-enriched-markdown/ios/internals/ShadowMeasurementUtils.h @@ -3,9 +3,11 @@ #include "MeasurementCache.h" #import #import +#include #include #include #include +#include #include #include