Skip to content

Conversation

@mfazekas
Copy link
Collaborator

@mfazekas mfazekas commented Jan 12, 2026

Summary

Fix retain cycle causing memory leak when using custom asset loaders. The customLoader closure was strongly capturing self, preventing RiveReactNativeView from being deallocated.

Fixes: https://community.rive.app/c/bug-reports/memory-leak-in-rive-react-native-native-rive-instances-persist-in-memory-after-component-unmount-causing-app-crashes

Before fix: Memory grew to 500MB+ after repeated mount/unmount cycles
After fix: Memory stabilizes around 308MB

Changes

Added weakCustomLoader computed property that wraps the asset loader in a closure capturing self weakly, breaking the retain cycle:

RiveFile → customAssetLoader → [weak self] → no retain cycle
image
Test component to reproduce the issue
import * as React from 'react';
import {
  Alert,
  Button,
  SafeAreaView,
  ScrollView,
  StyleSheet,
  Text,
  View,
} from 'react-native';
import Rive, { Fit, Alignment } from 'rive-react-native';

declare const global: {
  gc?: () => void;
};

function triggerGC(): boolean {
  if (typeof global.gc === 'function') {
    console.log('[MEMORY-TEST] Triggering JavaScript GC...');
    global.gc();
    console.log('[MEMORY-TEST] GC triggered');
    return true;
  } else {
    console.log('[MEMORY-TEST] global.gc() not available');
    return false;
  }
}

const TOTAL_ITERATIONS = 100;
const MOUNT_DURATION_MS = 50;
const UNMOUNT_DURATION_MS = 50;

function RiveInstance({ iteration }: { iteration: number }) {
  React.useEffect(() => {
    console.log(`[MEMORY-TEST] Rive mounted (iteration ${iteration})`);
    return () => {
      console.log(`[MEMORY-TEST] Rive unmounting (iteration ${iteration})`);
    };
  }, [iteration]);

  return (
    <Rive
      fit={Fit.Contain}
      alignment={Alignment.Center}
      style={styles.rive}
      resourceName="truck_v7"
      autoplay={true}
    />
  );
}

export default function MemoryLeakTest() {
  const [showRive, setShowRive] = React.useState(false);
  const [testRunning, setTestRunning] = React.useState(false);
  const [currentIteration, setCurrentIteration] = React.useState(0);
  const [completedCycles, setCompletedCycles] = React.useState(0);

  const runTest = React.useCallback(async () => {
    setTestRunning(true);
    setCompletedCycles(0);

    for (let i = 0; i < TOTAL_ITERATIONS; i++) {
      setCurrentIteration(i + 1);
      setShowRive(true);
      await new Promise((resolve) => setTimeout(resolve, MOUNT_DURATION_MS));
      setShowRive(false);
      await new Promise((resolve) => setTimeout(resolve, UNMOUNT_DURATION_MS));
      setCompletedCycles(i + 1);
    }

    setTestRunning(false);
    setCurrentIteration(0);
  }, []);

  return (
    <SafeAreaView style={styles.safeArea}>
      <ScrollView contentContainerStyle={styles.container}>
        <View style={styles.riveContainer}>
          {showRive && <RiveInstance iteration={currentIteration} />}
        </View>
        <Button
          title={testRunning ? 'Running...' : 'Start Test'}
          onPress={runTest}
          disabled={testRunning}
        />
        <Button title="Trigger GC" onPress={() => triggerGC()} />
      </ScrollView>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  safeArea: { flex: 1, backgroundColor: '#fff' },
  container: { flexGrow: 1, padding: 16 },
  riveContainer: { height: 300, backgroundColor: '#f5f5f5', marginBottom: 16 },
  rive: { width: '100%', height: '100%' },
});

Run the test, then use Xcode's "Debug Memory Graph" to inspect RiveReactNativeView instances.

Use weak self capture in customLoader closure to prevent retain cycle
between RiveReactNativeView and RiveFile.
@mfazekas mfazekas requested a review from HayesGordon January 12, 2026 11:05
@mfazekas mfazekas marked this pull request as ready for review January 12, 2026 11:05
Copy link
Contributor

@HayesGordon HayesGordon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Thanks

@mfazekas mfazekas merged commit 0068df8 into main Jan 12, 2026
5 checks passed
@mfazekas mfazekas deleted the mfazekas/fix-file-release-on-asset-load-is-pending branch January 12, 2026 12:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants