KIQA.DEV
HomeServicesPortfolioBlogAboutContactDEV HUB
Get a Quote
KIQA.DEV

Professional development services.

ServicesPortfolioBlogAboutContactDev Hub
© 2026 KIQA DEV. All rights reserved.
CVBuilt with Next.js & TypeScript
Blog
React Native7 min read · Mar 28, 2026

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

A real-time social feed with 1000+ posts was grinding to a halt. Here's how I tracked the leak and fixed it for good.

The Problem

About six weeks into building Spindare's social feed, I started noticing something wrong. The feed would load fine — scroll fast, render smoothly — but after about 90 seconds of continuous use, frame rate would drop from 60fps to under 20. After three minutes, the app crashed entirely on lower-end iPhones.

The feed was using a standard FlatList with a real-time Supabase Realtime subscription pushing new posts. On the surface, everything looked fine. Under the hood, something was eating memory alive.

Diagnosing with Flipper

First step: open Flipper and watch the memory profile while scrolling. Within 60 seconds I could see it: memory climbing steadily with no GC cycles cleaning it up. The heap was growing ~4MB per minute on a feed with 1000 posts.

The two usual suspects for FlatList memory leaks are:

1. Unregistered event listeners — listeners added inside renderItem that never get cleaned up

2. Image caching — images loading into memory and never releasing

In our case it was both, plus a third thing I didn't expect.

Root Cause #1: Inline handlers in renderItem

Our renderItem was creating new function instances on every render:

renderItem={({ item }) => (
  <FeedPost
    post={item}
    onLike={() => handleLike(item.id)}
    onComment={() => openComments(item.id)}
  />
)}

Every scroll event re-renders visible cells. Each re-render creates two new closures per post. With 1000 posts and rapid scrolling, that's thousands of closures never getting freed.

Fix: useCallback with stable references, and pass item.id as a prop instead of capturing it in a closure:

const handleLike = useCallback((id: string) => {
  // handler logic
}, []);

renderItem={({ item }) => (
  <FeedPost
    post={item}
    postId={item.id}
    onLike={handleLike}
    onComment={handleComment}
  />
)}

Root Cause #2: Supabase channel subscription inside renderItem

This was the worse one. We had a component that subscribed to real-time updates for each individual post (for live like counts). The subscription was being created in useEffect but only cleaned up when the component unmounted — and FlatList's virtualization was unmounting and remounting cells constantly.

useEffect(() => {
  const channel = supabase.channel(`post:${postId}`)
    .on('postgres_changes', { ... }, handleUpdate)
    .subscribe();
  // missing return () => supabase.removeChannel(channel)
}, [postId]);

Missing the cleanup function meant every virtualized unmount left a dangling Supabase WebSocket subscription. With 1000 posts in the feed, we could have hundreds of open channels simultaneously.

Fix: Always return the cleanup from useEffect:

useEffect(() => {
  const channel = supabase.channel(`post:${postId}`)
    .on('postgres_changes', { ... }, handleUpdate)
    .subscribe();

  return () => { supabase.removeChannel(channel); };
}, [postId]);

Root Cause #3: keyExtractor returning index

This one is subtle. We had:

keyExtractor={(item, index) => index.toString()}

When new posts arrive at the top of the feed (real-time), all existing keys shift. React Native's reconciler thinks every item changed and re-renders the entire list. Use a stable ID:

keyExtractor={(item) => item.id}

The Result

After all three fixes, memory growth dropped from ~4MB/min to essentially flat. The feed now runs at 60fps for 10+ minutes of continuous use with no crashes, even on iPhone 12 Mini with limited RAM.

The lesson: FlatList memory issues are almost always about three things — stable keys, cleanup in useEffect, and avoiding inline function creation in renderItem. Get those right and you rarely have to look further.

BlogGet a quote