From b86e8ef95fc6d7e35b2e20529a52bb74780dcf1a Mon Sep 17 00:00:00 2001 From: Tim Yung Date: Mon, 26 Aug 2024 11:16:54 -0700 Subject: [PATCH] Appearance: Lazily Initialize `NativeAppearance` (#46123) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46123 Optimizes initialization performance by changing `Appearance` to lazily import `NativeAppearance` and setup its listeners. Changelog: [General][Changed] - Improve `Appearance` performance overhead by lazily initializing the NativeModule Reviewed By: rickhanlonii Differential Revision: D61578726 fbshipit-source-id: 13fe6de7b5c3b52b8f4c54dd567bbe146379cf0f --- .../Libraries/Utilities/Appearance.js | 92 +++++++++++++------ 1 file changed, 62 insertions(+), 30 deletions(-) diff --git a/packages/react-native/Libraries/Utilities/Appearance.js b/packages/react-native/Libraries/Utilities/Appearance.js index eff658e9017..3cd23a65fe2 100644 --- a/packages/react-native/Libraries/Utilities/Appearance.js +++ b/packages/react-native/Libraries/Utilities/Appearance.js @@ -8,37 +8,64 @@ * @flow strict-local */ +import type {EventSubscription} from '../vendor/emitter/EventEmitter'; +import type {AppearancePreferences, ColorSchemeName} from './NativeAppearance'; +import typeof INativeAppearance from './NativeAppearance'; + import NativeEventEmitter from '../EventEmitter/NativeEventEmitter'; -import EventEmitter, { - type EventSubscription, -} from '../vendor/emitter/EventEmitter'; +import EventEmitter from '../vendor/emitter/EventEmitter'; import {isAsyncDebugging} from './DebugEnvironment'; -import NativeAppearance, { - type AppearancePreferences, - type ColorSchemeName, -} from './NativeAppearance'; import invariant from 'invariant'; -const eventEmitter = new EventEmitter<{ - change: [{colorScheme: ?ColorSchemeName}], -}>(); - -type NativeAppearanceEventDefinitions = { - appearanceChanged: [AppearancePreferences], +type Appearance = { + colorScheme: ?ColorSchemeName, }; -// Cache the color scheme to reduce the cost of reading it between changes. -// NOTE: If `NativeAppearance` is null, this will always be null. -let appearance: ?{colorScheme: ?ColorSchemeName} = null; +let lazyState: ?{ + +NativeAppearance: INativeAppearance, + // Cache the color scheme to reduce the cost of reading it between changes. + // NOTE: If `NativeAppearance` is null, this will always be null. + appearance: ?Appearance, + // NOTE: This is non-nullable to make it easier for `onChangedListener` to + // return a non-nullable `EventSubscription` value. This is not the common + // path, so we do not have to over-optimize it. + +eventEmitter: EventEmitter<{change: [Appearance]}>, +}; -if (NativeAppearance != null) { - new NativeEventEmitter( - NativeAppearance, - ).addListener('appearanceChanged', (newAppearance: AppearancePreferences) => { - const colorScheme = toColorScheme(newAppearance.colorScheme); - appearance = {colorScheme}; - eventEmitter.emit('change', appearance); - }); +/** + * Ensures that all state and listeners are lazily initialized correctly. + */ +function getState(): $NonMaybeType { + if (lazyState != null) { + return lazyState; + } + const eventEmitter = new EventEmitter<{change: [Appearance]}>(); + // NOTE: Avoid initializing `NativeAppearance` until it is actually used. + const NativeAppearance = require('./NativeAppearance').default; + if (NativeAppearance == null) { + // Assign `null` to avoid re-initializing on subsequent invocations. + lazyState = { + NativeAppearance: null, + appearance: null, + eventEmitter, + }; + } else { + const state: $NonMaybeType = { + NativeAppearance, + appearance: null, + eventEmitter, + }; + new NativeEventEmitter<{ + appearanceChanged: [AppearancePreferences], + }>(NativeAppearance).addListener('appearanceChanged', newAppearance => { + state.appearance = { + colorScheme: toColorScheme(newAppearance.colorScheme), + }; + eventEmitter.emit('change', state.appearance); + }); + lazyState = state; + } + return lazyState; } /** @@ -55,15 +82,17 @@ export function getColorScheme(): ?ColorSchemeName { } } let colorScheme = null; + const state = getState(); + const {NativeAppearance} = state; if (NativeAppearance != null) { - if (appearance == null) { - // Lazily initialize `appearance`. This should only happen once because - // we never reassign a null value to `appearance`. - appearance = { + if (state.appearance == null) { + // Lazily initialize `state.appearance`. This should only + // happen once because we never reassign a null value to it. + state.appearance = { colorScheme: toColorScheme(NativeAppearance.getColorScheme()), }; } - colorScheme = appearance.colorScheme; + colorScheme = state.appearance.colorScheme; } return colorScheme; } @@ -72,9 +101,11 @@ export function getColorScheme(): ?ColorSchemeName { * Updates the current color scheme to the supplied value. */ export function setColorScheme(colorScheme: ?ColorSchemeName): void { + const state = getState(); + const {NativeAppearance} = state; if (NativeAppearance != null) { NativeAppearance.setColorScheme(colorScheme ?? 'unspecified'); - appearance = {colorScheme}; + state.appearance = {colorScheme}; } } @@ -84,6 +115,7 @@ export function setColorScheme(colorScheme: ?ColorSchemeName): void { export function addChangeListener( listener: ({colorScheme: ?ColorSchemeName}) => void, ): EventSubscription { + const {eventEmitter} = getState(); return eventEmitter.addListener('change', listener); }