Skip to content

Commit 72412fd

Browse files
authored
fix: avoid reallocating views on RCTDevLoadingView showMessage calls (#2762)
This PR is equivalent to the upstream PR facebook#54608. Actually, the upstream PR is a _little_ different, as I had to handle the new `self->_dismissButton` that was addeed in facebook#54445 as well. When I came to handle `self->_dismissButton`, I took the opportunity to optimise the `NSConstraintLayout` bit while I was there, so be ready for both of those changes in `[email protected]`! ## Summary: As this PR regards the dev loading view, this issue of course affects only **debug** builds. The issue is that, each time a Metro progress update triggers the `showMessage:withColor:withBackgroundColor:` method on `RCTDevLoadingView`, we end up allocating a new `UIWindow` (iOS) or `NSWindow` (macOS), rather than reusing the existing window stored on `self->_window` from any previous progress updates. These unnecessary allocations are particularly expensive on macOS, as I show below. ### Demo of the issue on macOS On `react-native-macos`, the impact of this issue is dramatic (so much so that **the Microsoft Office team disable `RCTDevLoadingView`** in their Mac app). On the first connection to Metro (as first-time connections can produce nearly 100 progress updates), we end up allocating nearly 100 `NSWindow`s. Allocating `NSWindow`s is so expensive that it blocks the UI thread and **adds 30 seconds to app launch** (see 00:40 -> 01:15 of the below video clip). What's more, as we can see in the view debugger, these `NSWindow`s **never get released**, so they remain in the view graph and just leak memory (see 01:15 -> 01:30 of the below video clip). (This clip is from 1:15:00 of a [livestream](https://www.youtube.com/live/amRWVbfbknM?si=KmDBXjrQXdxnmf1E&t=4500) where I dug into this problem – sorry for the poor quality clips, that's the best the servers allow me to download) https://github.com/user-attachments/assets/65bb7c9b-dc18-4e54-8369-d0c611c59439 ## Solution Each of these views (the window, the container, and the label) need only be allocated once. Thereafter, they can be modified. This PR adds conditionals to lazily-allocate them, and reorders them in order of dependency (e.g. the label is the most nested, so needs to be handled first). ## Changelog: [MACOS] [FIXED] - Avoid reallocating views on RCTDevLoadingView progress updates ## Test Plan: I am unable to run `RNTester` on the latest commit of the `main` branch; I've tried [five different versions](https://x.com/birch_js/status/1991129150728642679?s=20) of Ruby, but the `bundle install` always fails. If someone could please guide me how to get RNTester, Ruby, and Bundler happy, I'd be happy to use that app to test it on `main`. So my testing so far has been based on patching an existing `[email protected]` app live on stream. This version of React Native macOS has the exact same bug as current `main`, so it should be representative.
1 parent b01a6d0 commit 72412fd

File tree

1 file changed

+27
-14
lines changed

1 file changed

+27
-14
lines changed

packages/react-native/React/CoreModules/RCTDevLoadingView.mm

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -128,33 +128,21 @@ - (void)showMessage:(NSString *)message color:(RCTUIColor *)color backgroundColo
128128
self->_window = [[UIWindow alloc] initWithWindowScene:mainWindow.windowScene];
129129
self->_window.windowLevel = UIWindowLevelStatusBar + 1;
130130
self->_window.rootViewController = [UIViewController new];
131-
#else // [macOS
132-
self->_window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 375, 20)
133-
styleMask:NSWindowStyleMaskBorderless | NSWindowStyleMaskFullSizeContentView
134-
backing:NSBackingStoreBuffered
135-
defer:YES];
136-
[self->_window setIdentifier:sRCTDevLoadingViewWindowIdentifier];
137-
#endif // macOS]
138131

139-
self->_container = [[RCTUIView alloc] init]; // [macOS]
132+
self->_container = [[UIView alloc] init];
140133
self->_container.backgroundColor = backgroundColor;
141134
self->_container.translatesAutoresizingMaskIntoConstraints = NO;
142135

143-
self->_label = [[RCTUILabel alloc] init]; // [macOS]
136+
self->_label = [[UILabel alloc] init];
144137
self->_label.translatesAutoresizingMaskIntoConstraints = NO;
145138
self->_label.font = [UIFont monospacedDigitSystemFontOfSize:12.0 weight:UIFontWeightRegular];
146139
self->_label.textAlignment = NSTextAlignmentCenter;
147140
self->_label.textColor = color;
148141
self->_label.text = message;
149142

150-
#if !TARGET_OS_OSX // [macOS]
151143
[self->_window.rootViewController.view addSubview:self->_container];
152-
#else // [macOS
153-
[self->_window.contentView addSubview:self->_container];
154-
#endif // macOS]
155144
[self->_container addSubview:self->_label];
156145

157-
#if !TARGET_OS_OSX // [macOS]
158146
CGFloat topSafeAreaHeight = mainWindow.safeAreaInsets.top;
159147
CGFloat height = topSafeAreaHeight + 25;
160148
self->_window.frame = CGRectMake(0, 0, mainWindow.frame.size.width, height);
@@ -175,6 +163,31 @@ - (void)showMessage:(NSString *)message color:(RCTUIColor *)color backgroundColo
175163
[self->_label.bottomAnchor constraintEqualToAnchor:self->_container.bottomAnchor constant:-5],
176164
]];
177165
#else // [macOS
166+
if (!self->_label) {
167+
self->_label = [[RCTUILabel alloc] init]; // [macOS]
168+
self->_label.translatesAutoresizingMaskIntoConstraints = NO;
169+
self->_label.font = [UIFont monospacedDigitSystemFontOfSize:12.0 weight:UIFontWeightRegular];
170+
self->_label.textAlignment = NSTextAlignmentCenter;
171+
}
172+
self->_label.textColor = color;
173+
self->_label.text = message;
174+
175+
if (!self->_container) {
176+
self->_container = [[RCTUIView alloc] init]; // [macOS]
177+
self->_container.translatesAutoresizingMaskIntoConstraints = NO;
178+
[self->_container addSubview:self->_label];
179+
}
180+
self->_container.backgroundColor = backgroundColor;
181+
182+
if (!self->_window) {
183+
self->_window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 375, 20)
184+
styleMask:NSWindowStyleMaskBorderless | NSWindowStyleMaskFullSizeContentView
185+
backing:NSBackingStoreBuffered
186+
defer:YES];
187+
[self->_window setIdentifier:sRCTDevLoadingViewWindowIdentifier];
188+
[self->_window.contentView addSubview:self->_container];
189+
}
190+
178191
// Container constraints
179192
[NSLayoutConstraint activateConstraints:@[
180193
[self->_container.topAnchor constraintEqualToAnchor:self->_window.contentView.topAnchor],

0 commit comments

Comments
 (0)