From ec4bbe374db32dc605cc99fcf6b6be3d91ad3439 Mon Sep 17 00:00:00 2001 From: Andrei Calazans Date: Fri, 26 Jun 2026 15:35:38 -0300 Subject: [PATCH] perf(mobile): cache initial SkPath in usePathTransition `usePathTransition` was calling `Skia.Path.MakeFromSVGString` on every render to compute `initialSkiaPath`, but the result is only ever consumed as the initial value of the four `useSharedValue` calls below it. Since `useSharedValue` ignores its argument on every render after the first, every later parse was discarded. Switch to a `useState` lazy initializer so the parse runs exactly once per hook instance. In an Android prod CPU trace (1:03 capture), this hot path accounted for ~64% of all `MakeFromSVGString` self-time (~350ms over the trace) across `AnimatedPath` and `DottedArea` chart renders. `useState` is used rather than `useMemo` because `useMemo` is documented as a perf hint that React may discard, and any realistic dependency array would either fail the exhaustive-deps lint rule (`[]`) or re-parse on every `currentPath` change while the shared-value initializers continue to ignore the new result. --- .../visualizations/chart/utils/transition.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/mobile/src/visualizations/chart/utils/transition.ts b/packages/mobile/src/visualizations/chart/utils/transition.ts index c202738aec..b17e0448bb 100644 --- a/packages/mobile/src/visualizations/chart/utils/transition.ts +++ b/packages/mobile/src/visualizations/chart/utils/transition.ts @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { type ExtrapolationType, type SharedValue, @@ -265,8 +265,21 @@ export const usePathTransition = ({ const interpolatorRef = useRef<((t: number) => string) | null>(null); const progress = useSharedValue(0); - const initialSkiaPath = - Skia.Path.MakeFromSVGString(initialPath ?? currentPath) ?? Skia.Path.Make(); + // Parse the SVG path exactly once per hook instance via `useState`'s lazy + // initializer. `initialSkiaPath` is only consumed as the initial value for + // the `useSharedValue` calls below, and `useSharedValue` ignores its + // argument on every render after the first. Recomputing the parse on every + // render is therefore pure waste — in an Android prod CPU trace this hot + // path accounted for ~64% of all `Skia.Path.MakeFromSVGString` self-time + // (~350ms over 1:03) across `AnimatedPath` / `DottedArea` chart renders. + // + // `useState` (not `useMemo`) is intentional: `useMemo` is documented as a + // performance hint that React may discard, and any realistic dependency + // array would either lint-fail (`[]`) or re-parse on every `currentPath` + // change while the shared-value initializers still ignore the new result. + const [initialSkiaPath] = useState( + () => Skia.Path.MakeFromSVGString(initialPath ?? currentPath) ?? Skia.Path.Make(), + ); const normalizedStartShared = useSharedValue(initialSkiaPath); const normalizedEndShared = useSharedValue(initialSkiaPath); const fallbackPathShared = useSharedValue(initialSkiaPath);