KIQA.DEV
Back to writing
React Native
Mar 28, 2026
7 min read
by Kristian Gjergji

How I fixed a memory leak that was crashing Spindare's social feed

The feed was slowing to a halt after a few minutes of use. Here's how I tracked down the problem and fixed it.

Spindare's social feed worked perfectly during development. A few dozen mock posts, fast scrolls, clean renders. Then we got to a proper test build with real data (a few hundred posts, real images, real timestamps) and after about four minutes of scrolling the app would slow to a crawl. On older Android devices it would crash completely.

This is the story of how I tracked it down, why it took longer than it should have, and what I'd check first if it happened again.

The symptom

Memory climbed steadily the longer the feed was open. JavaScript heap would sit at around 40 MB on launch and creep toward 200 MB after ten minutes. The performance monitor in Flipper made it obvious something was accumulating, but it wasn't obvious what.

I ran the standard checks first. No massive images being stored in state. No obvious re-renders from incorrect useCallback dependencies. Redux slices looked clean. The feed component itself was straightforward: a FlatList rendering PostCard components, each with an image, some text, and an interaction row.

The first false lead

My first assumption was the images. We were using react-native-fast-image with a memory cache, and with hundreds of posts each with a 600px-wide image, it seemed obvious. I added aggressive cache limits and switched some thumbnails to lower resolution. Memory still climbed.

Two hours wasted. The images were not the problem.

Finding the actual cause

I added logging to the PostCard component's mount and unmount cycles. Posts were mounting when scrolled into view, but a significant fraction were never unmounting. I expected FlatList's virtualisation to handle this. It was, mostly. But something was keeping a reference alive on the JavaScript side even after the native view was recycled.

The culprit was in how we were handling real-time like counts. Each PostCard was subscribing to a Supabase realtime channel to get live updates for its specific post ID:

typescript
// PostCard.tsx: the broken version
useEffect(() => {
  const channel = supabase
    .channel(`post-likes-${post.id}`)
    .on('postgres_changes', {
      event: 'UPDATE',
      schema: 'public',
      table: 'posts',
      filter: `id=eq.${post.id}`,
    }, (payload) => {
      setLikes(payload.new.like_count);
    })
    .subscribe();
}, []); // โ† empty dependency array, no cleanup

No cleanup function. The channel was created when the card mounted, but never removed when the card unmounted. Worse, because the dependency array was empty, if a post re-rendered the old channel stayed open and a second one was created.

With hundreds of posts in the feed, and each visible post holding an open WebSocket subscription, the memory just kept climbing.

The fix

typescript
// PostCard.tsx: fixed
useEffect(() => {
  const channel = supabase
    .channel(`post-likes-${post.id}`)
    .on('postgres_changes', {
      event: 'UPDATE',
      schema: 'public',
      table: 'posts',
      filter: `id=eq.${post.id}`,
    }, (payload) => {
      setLikes(payload.new.like_count);
    })
    .subscribe();

  return () => {
    supabase.removeChannel(channel);
  };
}, [post.id]); // โ† correct dependency

After the fix, memory stabilised at around 55 MB regardless of how long the feed was open. The slow-down and crashes disappeared completely.

The deeper lesson

The real mistake wasn't missing the cleanup, it was subscribing per-card at all. For a feed with potentially thousands of posts, opening individual realtime channels for each one is always going to be expensive. The better approach is a single channel at the feed level that handles all post updates, and distributing the relevant updates down to the cards via React context or a local state manager.

We shipped the cleanup fix for the September launch since it solved the crash. The architectural refactor is on the roadmap for the post-launch iteration.

๐Ÿ“Œ

If your React Native FlatList is leaking memory, check your useEffect cleanup functions in list item components before anything else, especially if those components subscribe to anything (realtime, events, timers, or animation listeners).

Written by

Kristian Gjergji

Developer ยท Kosovo / Italy

Work with me โ†’
Chat on WhatsApp