Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/android-performance-cache.md
Original file line number Diff line number Diff line change
@@ -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.
93 changes: 93 additions & 0 deletions PERFORMANCE.md
Original file line number Diff line number Diff line change
@@ -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
<DotLottie
source={require('./icon.lottie')}
performanceMode="cpu"
cacheId="nav_home_icon" // Unique key for this specific animation role
/>
```

### 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 (
<View style={{ opacity: isReady ? 1 : 0 }}>
<DotLottie
key={lottieKey}
performanceMode="cpu"
cacheId="my_persistent_icon"
// ... other props
/>
</View>
);
};
```
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -59,6 +63,9 @@ class DotlottieReactNativeView(context: ThemedReactContext) : FrameLayout(contex
private fun createEventListeners(): List<DotLottieEventListener> {
return listOf(
object : DotLottieEventListener {
override fun onSurfaceReady() {
onReceiveNativeEvent("onSurfaceReady", null)
}
override fun onLoad() {
onReceiveNativeEvent("onLoad", null)
}
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -391,6 +410,8 @@ class DotlottieReactNativeView(context: ThemedReactContext) : FrameLayout(contex
}
}



fun release() {
if (isReleased) {
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class DotlottieReactNativeViewManager : SimpleViewManager<DotlottieReactNativeVi
}

private val bubblingEvents = arrayOf(
"onSurfaceReady",
"onLoad",
"onComplete",
"onLoadError",
Expand Down Expand Up @@ -226,8 +227,8 @@ class DotlottieReactNativeViewManager : SimpleViewManager<DotlottieReactNativeVi
}

@ReactProp(name = "playMode")
fun setPlayMode(view: DotlottieReactNativeView, value: Int) {
view.setPlayMode(value)
fun setPlayMode(view: DotlottieReactNativeView, value: Double) {
view.setPlayMode(value.toInt())
}

@ReactProp(name = "stateMachineId")
Expand All @@ -240,6 +241,18 @@ class DotlottieReactNativeViewManager : SimpleViewManager<DotlottieReactNativeVi
view.setUseOpenGLRenderer(value == "gl")
}

@ReactProp(name = "performanceMode")
fun setPerformanceMode(view: DotlottieReactNativeView, value: Double) {
view.setPerformanceMode(value.toInt())
}


@ReactProp(name = "cacheId")
fun setCacheId(view: DotlottieReactNativeView, value: String?) {
view.setCacheId(value)
}


override fun onDropViewInstance(view: DotlottieReactNativeView) {
super.onDropViewInstance(view)
view.release()
Expand Down
16 changes: 16 additions & 0 deletions src/DotLottie.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export type Dotlottie = {
loopCount: () => Promise<number>;
};


interface DotlottieNativeProps {
source: string | { uri: string };
loop?: boolean;
Expand All @@ -79,8 +80,11 @@ interface DotlottieNativeProps {
useFrameInterpolation?: boolean;
stateMachineId?: string;
renderer?: Renderer;
performanceMode?: 0 | 1 | 2;
cacheId?: string;
style: ViewStyle;
ref?: MutableRefObject<any>;
onSurfaceReady?: () => void;
onLoad?: () => void;
onComplete?: () => void;
onLoadError?: () => void;
Expand Down Expand Up @@ -149,8 +153,11 @@ interface DotlottieReactNativeProps {
useFrameInterpolation?: boolean;
stateMachineId?: string;
renderer?: Renderer;
performanceMode?: 'cpu' | 'ram';
cacheId?: string;
style: ViewStyle;
ref?: MutableRefObject<any>;
onSurfaceReady?: () => void;
onLoad?: () => void;
onComplete?: () => void;
onLoadError?: () => void;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -407,6 +415,8 @@ export const DotLottie = forwardRef(
[dispatchCommand]
);



const resolveHandle = useCallback(() => {
const handle = findNodeHandle(nativeRef.current);
if (handle == null) {
Expand Down Expand Up @@ -526,11 +536,17 @@ export const DotLottie = forwardRef(

const parsedSource = parseSource(source);

const mappedPerformanceMode = props.performanceMode === 'cpu' ? 1 : props.performanceMode === 'ram' ? 2 : 0;

return (
<DotlottieReactNativeView
ref={nativeRef}
source={parsedSource || ''}
{...props}
performanceMode={mappedPerformanceMode}
onSurfaceReady={() => {
props.onSurfaceReady?.();
}}
onLoop={(event) => {
props.onLoop?.(event.nativeEvent.loopCount);
}}
Expand Down