From c425488226508ac708007fbfebac37c983f526f6 Mon Sep 17 00:00:00 2001 From: acedmicabhishek Date: Fri, 8 May 2026 19:56:55 +0530 Subject: [PATCH 1/5] performance mode for android --- PERFORMANCE.md | 93 +++++++++++++++++++ README.md | 6 ++ android/build.gradle | 2 +- .../DotlottieReactNativeView.kt | 23 ++++- .../DotlottieReactNativeViewManager.kt | 13 +++ src/DotLottie.tsx | 16 ++++ 6 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 PERFORMANCE.md diff --git a/PERFORMANCE.md b/PERFORMANCE.md new file mode 100644 index 0000000..83bea34 --- /dev/null +++ b/PERFORMANCE.md @@ -0,0 +1,93 @@ +# Performance Optimization Guide + +This guide explains how to optimize the performance of `dotlottie-react-native` on Android, focusing on the `performanceMode`, `cacheId` features, and the deferred remount pattern. + +## Understanding Performance Modes + +On Android, `dotlottie-react-native` uses a high-performance OpenGL renderer. You can choose between two primary performance modes to balance CPU and memory usage. + +### `performanceMode="ram"` (Default) +In this mode, the animation player is destroyed when the component unmounts. Only the current frame index is saved. +- **Pros:** Lowest permanent memory footprint. +- **Cons:** Every remount requires re-parsing the Lottie file (JSON/DotLottie), which can cause CPU spikes and "jank." +- **When to use:** For large, complex animations that are only shown once or rarely remounted. + +### `performanceMode="cpu"` +This mode keeps the underlying C++ player instance alive in a static native cache. +- **Pros:** Instant remounting. Zero re-parsing overhead. No CPU spikes when switching views. +- **Cons:** Slightly higher permanent RAM usage. +- **When to use:** For frequently toggled UI elements like **Bottom Navigation Bars**, sidebars, or repeatedly used interactive icons. + +--- + +## Using `cacheId` + +The `cacheId` prop is the key to managing persistent players in `cpu` mode. + +```tsx + +``` + +### Best Practices for `cacheId`: +1. **Uniqueness:** Use unique strings for different animation roles (e.g., `tab_home`, `tab_profile`). +2. **Persistence:** If multiple instances share the same `cacheId`, they will share the same underlying player instance and state. +3. **Avoid Randomness:** Never use random strings (like `Math.random()`) for `cacheId`, as this will cause memory leaks in the native layer. + +--- + +## The "Deferred Remount" Pattern (Android) + +Android's `TextureView` (used for OpenGL rendering) has a known limitation: when a view is covered (e.g., navigating to another screen) or the app loses focus, the underlying native surface buffer is destroyed by the OS. Sometimes, when returning to the screen, the hardware layer fails to refresh correctly, leading to "invisible" or "black" icons. + +The most reliable fix is the **Deferred Remount Pattern**. + +### Why 100ms? +1. **Transition Stabilization:** React Navigation transitions usually take ~250ms. Triggering a remount at 0ms can cause race conditions while the layout is still sliding. +2. **OS Surface Provisioning:** 100ms gives the Android Window Manager enough time to stabilize the layout before requesting a fresh hardware surface. +3. **Perception:** 100ms is below the threshold of human perception, making the icons appear "instantly" without flickering. + +### Implementation Example + +```tsx +import { useNavigation } from '@react-navigation/native'; +import { useEffect, useState, useRef } from 'react'; +import { DotLottie } from '@lottiefiles/dotlottie-react-native'; + +const MyIcon = ({ isActive }) => { + const navigation = useNavigation(); + const [lottieKey, setLottieKey] = useState(0); + const [isReady, setIsReady] = useState(true); + + useEffect(() => { + const unsubFocus = navigation.addListener('focus', () => { + // 1. Hide the view briefly to avoid showing stale buffers + setIsReady(false); + + // 2. Wait for transition to settle (100ms) + setTimeout(() => { + // 3. Increment key to force a fresh TextureView instance + setLottieKey(k => k + 1); + // 4. Show the new, clean instance + setIsReady(true); + }, 100); + }); + + return unsubFocus; + }, [navigation]); + + return ( + + + + ); +}; +``` diff --git a/README.md b/README.md index 40cb409..8d7c3a2 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,10 @@ const styles = StyleSheet.create({ }); ``` +## Performance + +For Android applications, see the [Performance Optimization Guide](PERFORMANCE.md) for details on `performanceMode`, `cacheId`, and handling view lifecycles. + ## API Reference ### Props @@ -190,6 +194,8 @@ const styles = StyleSheet.create({ | `marker` | `string` | `undefined` | Specifies a marker to use for playback. | | `themeId` | `string` | `undefined` | The theme ID to apply to the animation. | | `stateMachineId` | `string` | `undefined` | The ID of the state machine to load and start automatically. | +| `performanceMode` | `'cpu' \| 'ram'` | `'ram'` | Android only: chooses between CPU caching or RAM cleanup on unmount. | +| `cacheId` | `string` | `undefined` | Android only: unique key to cache the player instance in `cpu` mode. | ### Methods diff --git a/android/build.gradle b/android/build.gradle index 6f36a3b..f3f9361 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -121,7 +121,7 @@ dependencies { //noinspection GradleDynamicVersion implementation "com.facebook.react:react-native:+" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation "com.github.LottieFiles:dotlottie-android:0.13.6" + implementation "com.github.acedmicabhishek:dotlottie-android:main-SNAPSHOT" implementation 'androidx.compose.ui:ui:1.5.0' implementation 'androidx.compose.material:material:1.5.0' implementation 'androidx.compose.ui:ui-tooling-preview:1.5.0' diff --git a/android/src/main/java/com/dotlottiereactnative/DotlottieReactNativeView.kt b/android/src/main/java/com/dotlottiereactnative/DotlottieReactNativeView.kt index 396f044..b6b43f6 100644 --- a/android/src/main/java/com/dotlottiereactnative/DotlottieReactNativeView.kt +++ b/android/src/main/java/com/dotlottiereactnative/DotlottieReactNativeView.kt @@ -39,6 +39,10 @@ class DotlottieReactNativeView(context: ThemedReactContext) : FrameLayout(contex private var stateMachineListenerRegistered: Boolean = false private var hasActiveComposition: Boolean = false private var isReleased: Boolean = false + private var performanceMode: Int = 0 + + + private var cacheId: String = "" private val composeView: ComposeView = ComposeView(context).apply { @@ -59,6 +63,9 @@ class DotlottieReactNativeView(context: ThemedReactContext) : FrameLayout(contex private fun createEventListeners(): List { return listOf( object : DotLottieEventListener { + override fun onSurfaceReady() { + onReceiveNativeEvent("onSurfaceReady", null) + } override fun onLoad() { onReceiveNativeEvent("onLoad", null) } @@ -129,7 +136,10 @@ class DotlottieReactNativeView(context: ThemedReactContext) : FrameLayout(contex marker = marker, segment = segment, playMode = playMode, - eventListeners = eventListeners + eventListeners = eventListeners, + performanceMode = performanceMode, + cacheId = cacheId + ) } else { DotLottieAnimation( @@ -249,6 +259,15 @@ class DotlottieReactNativeView(context: ThemedReactContext) : FrameLayout(contex dotLottieController.resize(width, height) } + fun setPerformanceMode(value: Int?) { + performanceMode = value ?: 0 + } + + + fun setCacheId(value: String?) { + cacheId = value ?: "" + } + fun getTotalFrames(): Float { return dotLottieController.totalFrames } @@ -391,6 +410,8 @@ class DotlottieReactNativeView(context: ThemedReactContext) : FrameLayout(contex } } + + fun release() { if (isReleased) { return diff --git a/android/src/main/java/com/dotlottiereactnative/DotlottieReactNativeViewManager.kt b/android/src/main/java/com/dotlottiereactnative/DotlottieReactNativeViewManager.kt index c3ffdf7..3ce8df3 100644 --- a/android/src/main/java/com/dotlottiereactnative/DotlottieReactNativeViewManager.kt +++ b/android/src/main/java/com/dotlottiereactnative/DotlottieReactNativeViewManager.kt @@ -46,6 +46,7 @@ class DotlottieReactNativeViewManager : SimpleViewManager Promise; }; + interface DotlottieNativeProps { source: string | { uri: string }; loop?: boolean; @@ -79,8 +80,11 @@ interface DotlottieNativeProps { useFrameInterpolation?: boolean; stateMachineId?: string; renderer?: Renderer; + performanceMode?: 0 | 1 | 2; + cacheId?: string; style: ViewStyle; ref?: MutableRefObject; + onSurfaceReady?: () => void; onLoad?: () => void; onComplete?: () => void; onLoadError?: () => void; @@ -149,8 +153,11 @@ interface DotlottieReactNativeProps { useFrameInterpolation?: boolean; stateMachineId?: string; renderer?: Renderer; + performanceMode?: 'cpu' | 'ram'; + cacheId?: string; style: ViewStyle; ref?: MutableRefObject; + onSurfaceReady?: () => void; onLoad?: () => void; onComplete?: () => void; onLoadError?: () => void; @@ -212,6 +219,7 @@ const COMMAND_SET_MARKER = 'setMarker'; const COMMAND_SET_THEME = 'setTheme'; const COMMAND_SET_LOAD_ANIMATION = 'loadAnimation'; + const ComponentName = 'DotlottieReactNativeView'; const NativeViewManager = UIManager.getViewManagerConfig(ComponentName); @@ -407,6 +415,8 @@ export const DotLottie = forwardRef( [dispatchCommand] ); + + const resolveHandle = useCallback(() => { const handle = findNodeHandle(nativeRef.current); if (handle == null) { @@ -526,11 +536,17 @@ export const DotLottie = forwardRef( const parsedSource = parseSource(source); + const mappedPerformanceMode = props.performanceMode === 'cpu' ? 1 : props.performanceMode === 'ram' ? 2 : 0; + return ( { + props.onSurfaceReady?.(); + }} onLoop={(event) => { props.onLoop?.(event.nativeEvent.loopCount); }} From 48b01b6af5399d6255fbcac79e32c5d3b9138937 Mon Sep 17 00:00:00 2001 From: acedmicabhishek Date: Fri, 8 May 2026 23:14:49 +0530 Subject: [PATCH 2/5] chore: add changeset for android performance features --- .changeset/android-performance-cache.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/android-performance-cache.md diff --git a/.changeset/android-performance-cache.md b/.changeset/android-performance-cache.md new file mode 100644 index 0000000..db78c62 --- /dev/null +++ b/.changeset/android-performance-cache.md @@ -0,0 +1,5 @@ +--- +"@lottiefiles/dotlottie-react-native": minor +--- + +Added support for `performanceMode` and `cacheId` on Android to enable native player caching. This significantly improves performance during navigation by persisting the Lottie animation player in native memory. Also added `onSurfaceReady` event for improved synchronization. From d4c94f4cb934143e3c83acadc00400548bb2d5e7 Mon Sep 17 00:00:00 2001 From: acedmicabhishek Date: Sat, 16 May 2026 17:48:42 +0530 Subject: [PATCH 3/5] fix(android): fabric interop type mismatch --- .../DotlottieReactNativeViewManager.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/android/src/main/java/com/dotlottiereactnative/DotlottieReactNativeViewManager.kt b/android/src/main/java/com/dotlottiereactnative/DotlottieReactNativeViewManager.kt index 3ce8df3..51533a7 100644 --- a/android/src/main/java/com/dotlottiereactnative/DotlottieReactNativeViewManager.kt +++ b/android/src/main/java/com/dotlottiereactnative/DotlottieReactNativeViewManager.kt @@ -227,8 +227,8 @@ class DotlottieReactNativeViewManager : SimpleViewManager Date: Mon, 18 May 2026 20:28:34 +0530 Subject: [PATCH 4/5] fix: point dotlottie-ios dependency to acedmicabhishek fork with NSLock concurrent load fix --- dotlottie-react-native.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotlottie-react-native.podspec b/dotlottie-react-native.podspec index e39fca4..4a7b2e9 100644 --- a/dotlottie-react-native.podspec +++ b/dotlottie-react-native.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| s.source_files = "ios/**/*.{h,m,mm,swift}" - s.dependency 'LottieFiles-dotLottie-iOS', '~> 0.14' + s.dependency 'acedmicabhishek-dotlottie-ios', :git => 'https://github.com/acedmicabhishek/dotlottie-ios.git', :branch => 'main' s.swift_version = '5.0' From ce4ee328550e8d670c834fa726dce33d92569fca Mon Sep 17 00:00:00 2001 From: acedmicabhishek Date: Mon, 18 May 2026 20:33:58 +0530 Subject: [PATCH 5/5] revert: use LottieFiles-dotLottie-iOS pod name, override via Podfile --- dotlottie-react-native.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotlottie-react-native.podspec b/dotlottie-react-native.podspec index 4a7b2e9..e39fca4 100644 --- a/dotlottie-react-native.podspec +++ b/dotlottie-react-native.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| s.source_files = "ios/**/*.{h,m,mm,swift}" - s.dependency 'acedmicabhishek-dotlottie-ios', :git => 'https://github.com/acedmicabhishek/dotlottie-ios.git', :branch => 'main' + s.dependency 'LottieFiles-dotLottie-iOS', '~> 0.14' s.swift_version = '5.0'