常见问题
我们需要用 <PerformanceMeasureView>
包裹每个屏幕吗?我们是否可以在配置 react-navigation 导航器时自动为每个屏幕注入它?
在设置 react-navigation
导航器时,有人可能会倾向于用 PerformanceMeasureView
组件包裹应用程序中的所有屏幕。就像这样
tsx
function withPerformanceMeasureView<TProps>(Screen: (props: TProps) => React.ReactElement, screenName: string) {const ProfiledScreen = (props: TProps) => {return (<PerformanceMeasureView screenName={screenName}><Screen {...props} /></PerformanceMeasureView>);};return ProfiledScreen;}function NavigationTree() {return (<NavigationContainer><Stack.Navigator><Stack.Screenname={NavigationKeys.SCREEN_1}component={withPerformanceMeasureView(Screen1, NavigationKeys.SCREEN_1)}/><Stack.Screenname={NavigationKeys.SCREEN_2}component={withPerformanceMeasureView(Screen2, NavigationKeys.SCREEN_1)}/></Stack.Navigator></NavigationContainer>);}
tsx
function withPerformanceMeasureView<TProps>(Screen: (props: TProps) => React.ReactElement, screenName: string) {const ProfiledScreen = (props: TProps) => {return (<PerformanceMeasureView screenName={screenName}><Screen {...props} /></PerformanceMeasureView>);};return ProfiledScreen;}function NavigationTree() {return (<NavigationContainer><Stack.Navigator><Stack.Screenname={NavigationKeys.SCREEN_1}component={withPerformanceMeasureView(Screen1, NavigationKeys.SCREEN_1)}/><Stack.Screenname={NavigationKeys.SCREEN_2}component={withPerformanceMeasureView(Screen2, NavigationKeys.SCREEN_1)}/></Stack.Navigator></NavigationContainer>);}
我们建议不要这样做。像这样包裹屏幕组件会在该组件渲染任何东西(包括初始加载指示器)的那一刻将其标记为 interactive
。这可能不是你想要的。
在大多数情况下,目标屏幕将获取某些资源,并在一定延迟后渲染为“交互式”状态。在屏幕“交互式”之前,它将渲染一个中间的“非交互式”状态(又名加载屏幕)。PerformanceMeasureView
需要通过 interactive: boolean
和 renderPassName: string
属性获取此信息。有关更多详细信息,请参阅 README。提供此信息将允许库计算你可能真正关心的内容:屏幕有意义地渲染并变为交互式所需的时间。获得此洞察力的一个权衡是,这个过程在大多数应用程序中可能无法自动化。在大多数应用程序中,需要根据每个屏幕的资源获取逻辑来评估每个屏幕上的 interactive
和 renderPassName
属性的值。
但是,如果你正在构建一个“静态”应用程序,该应用程序立即渲染每个屏幕的交互式版本,而无需经历增量渲染过程,那么你可能可以像上面的示例那样包裹所有屏幕。因此,在这种情况下,请使用你最佳的判断力。
我们需要每次在安排屏幕导航时手动调用 useStartProfiler hook 返回的函数吗?我们是否可以观察 react-navigation 事件并自动化此过程?
每当请求导航操作时,react-navigation
都会发出某些 事件。有人可能想知道我们是否可以观察这些事件,并通过 useStartProfiler
hook 自动通知分析库开始性能分析。但是,我们不想这样做,因为这样做会导致输出报告出现不准确的情况。
我们希望尽可能地接近用户实际从 UI 请求时记录“流程开始”事件。理想情况下,这需要在按下给定的原生视图(例如,按钮)时发生(选项 1)。但是,如果不提供需要在整个应用程序代码库中侵入性引入的自定义 ProfiledTouchable
组件,则无法做到这一点。这种自定义组件还需要抛弃所有投入到库存 Touchable
组件中的好处(和多年的边缘情况覆盖),这感觉就像是在重新发明轮子。
下一个最佳选择是在调用 JS Touchable.onPress
回调时记录该事件(选项 2)。这不如选项 1 准确,因为它没有考虑通过桥从原生层传播到 JS 层的触摸事件,以及在 JS 事件队列中被消耗所造成的延迟。
幸运的是,Touchable.onPress
函数接收一个 GestureResponderEvent
类型的对象作为第 0 个参数。此对象带有一个 nativeEvent.timestamp: number
属性。此 timestamp
是我们通过选项 1 需要的确切时间。因此,如果在使用选项 2 时向库提供此(可选)GestureResponderEvent
对象,则库可以在内部有效地切换到选项 1 计算,从而为我们提供极其准确的结果。此外,我们可以计算原生触摸事件和 JS onPress
调用之间的时间,以了解原生到 JS 通信通道的繁忙程度;这通过输出 RenderPassReports
中的 timeToConsumeTouchEvent
属性传达。如果你发现较高的 timeToRenderMillis
值,此信息可以提供额外的调试信息。
观察 react-navigation
事件(选项 3)意味着我们在计算中添加了更多不准确性。react-navigation
的实现细节在大多数情况下,当我们调用 navigation.navigate
(在 Touchable.onPress
回调中)和发出相应的 react-navigation 事件之间增加了可测量的延迟。如果你通过另一个事件系统(例如,Redux)进一步解耦这两个代码片段,则此延迟会变得更糟。这些延迟足够大,足以使此选项成为性能监控用例的致命缺陷。
此外,这些事件不携带 GestureResponderEvent
对象,因此我们甚至无法逆向工程实际按下原生视图的时间。我们调查了向 react-navigation
本身添加补丁,以便适用的事件携带此元数据需要多少工作,但这并非易事。例如,抽屉导航器的 iOS 实现使用的可触摸对象(请参阅 此处 的代码)使用 react-native-gesture-handler
提供的 BaseButton
组件,该组件不会通过其 onPress
回调发送 GestureResponderEvent
对象。因此,实现此选项 3 需要对我们不一定控制的许多 OSS 库进行补丁,并且依赖关系图将非常脆弱。因此,考虑到正确实现选项 3 所需的维护和开发工作,它没有通过筛选。
也就是说,可以理解的是,在 Touchable.onPress
回调中进行两个调用是容易出错(且令人讨厌)的:一个用于安排 react-navigation 操作,另一个用于通知 rn-performance 该事件。为了帮助解决这个问题,配套的 react-native-performance-navigation
库带有一个 useProfiledNavigation
hook,以简化此过程。
我们是否可以在目标屏幕中调用函数来结束分析器计时器,而不是将其包装在组件中?
不,我们不能。这样做会在渲染 JS 组件时结束计时器,这与通过原生视图将屏幕实际物化到屏幕上的时间点不同。用 <PerformanceMeasureView>
包裹目标屏幕允许我们在屏幕上注入一个不可见的自定义视图(作为实际屏幕内容的兄弟),并等待它被原生渲染。我们将其用作屏幕实际渲染时间的近似值。请注意,我们实际上仍然没有测量真实视图的渲染时间;但是由于注入的视图是兄弟,因此这是一个相当不错的近似值。至少它比在渲染 JS 组件时结束计时器要好得多。