react-native-pager-view icon indicating copy to clipboard operation
react-native-pager-view copied to clipboard

Memory Leaks when component is disposed

Open Shaw-Signaturize opened this issue 2 years ago • 6 comments

Environment

System: OS: macOS 13.2.1 CPU: (8) x64 Intel(R) Core(TM) i7-7820HQ CPU @ 2.90GHz Memory: 16.34 MB / 16.00 GB Shell: 3.2.57 - /bin/bash Binaries: Node: 16.17.1 - ~/.nvm/versions/node/v16.17.1/bin/node Yarn: 1.17.3 - /usr/local/bin/yarn npm: 8.15.0 - ~/.nvm/versions/node/v16.17.1/bin/npm Watchman: 4.9.0 - /usr/local/bin/watchman Managers: CocoaPods: 1.11.2 - /usr/local/bin/pod SDKs: iOS SDK: Platforms: DriverKit 22.2, iOS 16.2, macOS 13.1, tvOS 16.1, watchOS 9.1 Android SDK: Not Found IDEs: Android Studio: 3.4 AI-183.6156.11.34.5522156 Xcode: 14.2/14C18 - /usr/bin/xcodebuild Languages: Java: Not Found npmPackages: @react-native-community/cli: Not Found react: 18.2.0 => 18.2.0 react-native: 0.71.3 => 0.71.3 react-native-macos: Not Found npmGlobalPackages: react-native: Not Found

Description

When using pager view with larger resources, memory seems to leak by the amount contained in the active page. Originally discovered in a project using a routing library but for this example the toggle component simulates a page disposal. As resources are proportionally small for the example to reproduce press the toggle button multiple times Although the leaking memory from this example is small It has been able to reach over 8gb where images have client side processing applied such as grayscale.

Memory Profile with pager view included image

Memory Profile with pager view excluded

image

Reproducible Demo

import React, { useMemo, useState, useCallback } from 'react';
import { 
  View, 
  Button,
  StyleSheet, 
  SafeAreaView, 
} from 'react-native';

import FastImage from 'react-native-fast-image'

import PagerView from 'react-native-pager-view';

const data = new Array(100).fill(1)

function App() {
  const [visible, setVisible] = useState(true)

  const onPress = useCallback(() => {
    setVisible((x) => !x)
  }, [])

  return (
    <SafeAreaView style={{ flex: 1}}>
      <View style={{ flex: 0 }}>
        <Button title={'Toggle'} onPress={onPress} />
      </View>
      <Toggle visible={visible}>
        <Pager />
      </Toggle>
    </SafeAreaView>
  );
};

function Toggle({ visible, children }) {
  return visible
    ? children
    : null
}

function Pager() {  
  const content = useMemo(() => {
    return data.map((x, i) => {
      return <Image value={i} key={i} />
    })
  }, [data])

  return (
    // Apply comment to the below line to demo memory is stable without this component
    <PagerView style={styles.pagerView} initialPage={0}>
      <View key="1">
        {content}
      </View>
    </PagerView>
  )
}

function Image({ value }) {
  const source = useMemo(() => {
    const i = value % 40
    return { uri: `https://unsplash.it/1200/1200?image=${i}` }
  }, [value])

  return (
    <FastImage
      style={styles.image}
      source={source}
      resizeMode={FastImage.resizeMode.cover}
    />
  )
}

const styles = StyleSheet.create({
  pagerView: {
    flex: 1,
  },
  image: {
    margin: 8,
    aspectRatio: 1,
    width: 400,
    height: 400
  }
});

export default App

Shaw-Signaturize avatar Mar 20 '23 21:03 Shaw-Signaturize

Hello, which version of pager-view did you test this on?

okwasniewski avatar Apr 05 '23 20:04 okwasniewski

@okwasniewski

Originally discovered with 5.4.9 but persisted after upgrading to 6.1.4

Also seems to persist in fresh projects with 6.1.0-rc.2

Shaw-Signaturize avatar Apr 06 '23 10:04 Shaw-Signaturize

Just change 'NSHashTableStrongMemory' to 'NSHashTableWeakMemory' in file ReactNativePageView.m. The item view outside the window can then be freed. And memory can be easily stabilized.

Use this patch react-native-pager-view+5.4.25.patch as below

diff --git a/node_modules/react-native-pager-view/ios/ReactNativePageView.m b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
index eacfbe8..c61745f 100644
--- a/node_modules/react-native-pager-view/ios/ReactNativePageView.m
+++ b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
@@ -44,7 +44,7 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher {
         _dismissKeyboard = UIScrollViewKeyboardDismissModeNone;
         _coalescingKey = 0;
         _eventDispatcher = eventDispatcher;
-        _cachedControllers = [NSHashTable hashTableWithOptions:NSHashTableStrongMemory];
+        _cachedControllers = [NSHashTable hashTableWithOptions:NSHashTableWeakMemory];
         _overdrag = NO;
         _layoutDirection = @"ltr";
     }

zyestin avatar Jun 05 '24 09:06 zyestin

I face the same problem exactly. My pager view version is 6.2.3

  • Memory leak for active pages
  • I display web views each tabs => massive memory leak

solominh avatar Nov 25 '24 04:11 solominh

@zyestin hi, have you used this patch in production? is it stable? Just found about this issue, just checking in.

efstathiosntonas avatar Nov 16 '25 09:11 efstathiosntonas

https://github.com/callstack/react-native-pager-view/pull/1040#issuecomment-3538503014

diff --git a/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/NestedScrollableHost.kt b/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/NestedScrollableHost.kt
  index 87b58d0f..e9d0ace1 100644
  --- a/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/NestedScrollableHost.kt
  +++ b/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/NestedScrollableHost.kt
  @@ -25,6 +25,7 @@ class NestedScrollableHost : FrameLayout {
     constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
     public var initialIndex: Int? = null
     public var didSetInitialIndex = false
  +  public var pageChangeCallback: ViewPager2.OnPageChangeCallback? = null
     private var touchSlop = 0
     private var initialX = 0f
     private var initialY = 0f
  diff --git a/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/PagerViewViewManager.kt b/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/PagerViewViewManager.kt
  index 8ec286a7..19f46363 100644
  --- a/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/PagerViewViewManager.kt
  +++ b/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/PagerViewViewManager.kt
  @@ -52,7 +52,7 @@ class PagerViewViewManager : ViewGroupManager<NestedScrollableHost>(), RNCViewPa
           vp.isSaveEnabled = false

           vp.post {
  -            vp.registerOnPageChangeCallback(object : OnPageChangeCallback() {
  +            val callback = object : OnPageChangeCallback() {
                   override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
                       super.onPageScrolled(position, positionOffset, positionOffsetPixels)
                       UIManagerHelper.getEventDispatcherForReactTag(reactContext, host.id)?.dispatchEvent(
  @@ -79,7 +79,9 @@ class PagerViewViewManager : ViewGroupManager<NestedScrollableHost>(), RNCViewPa
                               PageScrollStateChangedEvent(host.id, pageScrollState)
                       )
                   }
  -            })
  +            }
  +            host.pageChangeCallback = callback
  +            vp.registerOnPageChangeCallback(callback)
               UIManagerHelper.getEventDispatcherForReactTag(reactContext, host.id)?.dispatchEvent(
                       PageSelectedEvent(host.id, vp.currentItem)
               )
  @@ -200,6 +202,20 @@ class PagerViewViewManager : ViewGroupManager<NestedScrollableHost>(), RNCViewPa
           }
       }

  +    override fun onDropViewInstance(view: NestedScrollableHost) {
  +        // Unregister the page change callback to prevent memory leaks
  +        val viewPager = PagerViewViewManagerImpl.getViewPager(view)
  +        view.pageChangeCallback?.let { callback ->
  +            viewPager.unregisterOnPageChangeCallback(callback)
  +            view.pageChangeCallback = null
  +        }
  +
  +        // Clear the adapter to release references to child views
  +        viewPager.adapter = null
  +
  +        super.onDropViewInstance(view)
  +    }
  +
       override fun getExportedCustomDirectEventTypeConstants(): MutableMap<String, Map<String, String>> {
           return MapBuilder.of(
                   PageScrollEvent.EVENT_NAME, MapBuilder.of("registrationName", "onPageScroll"),
  diff --git a/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/ViewPagerAdapter.kt b/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/ViewPagerAdapter.kt
  index fd3530e1..8aab5f66 100644
  --- a/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/ViewPagerAdapter.kt
  +++ b/node_modules/react-native-pager-view/android/src/main/java/com/reactnativepagerview/ViewPagerAdapter.kt
  @@ -30,6 +30,12 @@ class ViewPagerAdapter() : Adapter<ViewPagerViewHolder>() {
       container.addView(child)
     }

  +  override fun onViewRecycled(holder: ViewPagerViewHolder) {
  +    super.onViewRecycled(holder)
  +    // Clean up the holder's container to prevent memory leaks
  +    holder.container.removeAllViews()
  +  }
  +
     override fun getItemCount(): Int {
       return childrenViews.size
     }
  diff --git a/node_modules/react-native-pager-view/ios/ReactNativePageView.m b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
  index eacfbe8..c61745f 100644
  --- a/node_modules/react-native-pager-view/ios/ReactNativePageView.m
  +++ b/node_modules/react-native-pager-view/ios/ReactNativePageView.m
  @@ -44,7 +44,7 @@ - (instancetype)initWithEventDispatcher:(RCTEventDispatcher *)eventDispatcher {
           _dismissKeyboard = UIScrollViewKeyboardDismissModeNone;
           _coalescingKey = 0;
           _eventDispatcher = eventDispatcher;
  -        _cachedControllers = [NSHashTable hashTableWithOptions:NSHashTableStrongMemory];
  +        _cachedControllers = [NSHashTable hashTableWithOptions:NSHashTableWeakMemory];
           _overdrag = NO;
           _layoutDirection = @"ltr";
       }

troZee avatar Nov 16 '25 10:11 troZee