Skip to main content

Optimizing FlatList Performance

Learn how to optimize FlatList for smooth 60 FPS scrolling, even with large datasets. FlatList is React Native’s high-performance list component that virtualizes content.

How FlatList Works

FlatList uses VirtualizedList under the hood to:
  1. Render only visible items: Items outside viewport are unmounted
  2. Recycle components: Reuse components as you scroll
  3. Batch updates: Group multiple updates for efficiency
  4. Windowing: Keep a buffer of items above and below viewport

Basic FlatList Setup

import React from 'react';
import { FlatList, View, Text, StyleSheet } from 'react-native';

const DATA = Array.from({ length: 1000 }, (_, i) => ({
  id: `item-${i}`,
  title: `Item ${i}`,
}));

const Item = ({ title }) => (
  <View style={styles.item}>
    <Text>{title}</Text>
  </View>
);

const App = () => {
  return (
    <FlatList
      data={DATA}
      renderItem={({ item }) => <Item title={item.title} />}
      keyExtractor={item => item.id}
    />
  );
};

const styles = StyleSheet.create({
  item: {
    padding: 20,
    borderBottomWidth: 1,
    borderBottomColor: '#ccc',
  },
});

Essential Optimizations

1. Implement getItemLayout

Prevent expensive layout calculations by providing item dimensions:
const ITEM_HEIGHT = 80;

<FlatList
  data={DATA}
  renderItem={renderItem}
  keyExtractor={item => item.id}
  getItemLayout={(data, index) => ({
    length: ITEM_HEIGHT,
    offset: ITEM_HEIGHT * index,
    index,
  })}
/>
Benefits:
  • No layout measurements needed
  • Instant scroll position calculation
  • Smooth scrollToIndex behavior
Note: Only use if all items have the same height.

2. Use keyExtractor Correctly

Provide stable, unique keys:
// Good: stable unique key
keyExtractor={item => item.id}

// Bad: index as key (can cause issues)
keyExtractor={(item, index) => index.toString()}

// Bad: unstable key
keyExtractor={item => Math.random().toString()}

3. Optimize renderItem with React.memo

Prevent unnecessary re-renders:
import React, { memo } from 'react';

const Item = memo(({ title, onPress }) => {
  console.log('Rendering:', title);
  return (
    <TouchableOpacity onPress={onPress} style={styles.item}>
      <Text>{title}</Text>
    </TouchableOpacity>
  );
});

const App = () => {
  const renderItem = useCallback(({ item }) => (
    <Item title={item.title} onPress={() => handlePress(item.id)} />
  ), []);
  
  return (
    <FlatList
      data={DATA}
      renderItem={renderItem}
      keyExtractor={item => item.id}
    />
  );
};

4. Configure Window Size

Control how many items to keep mounted:
<FlatList
  data={DATA}
  renderItem={renderItem}
  keyExtractor={item => item.id}
  
  // Initial items to render
  initialNumToRender={10}
  
  // Items to render per batch
  maxToRenderPerBatch={10}
  
  // Number of screen lengths to keep in memory
  windowSize={5}
  
  // Update cells in batches
  updateCellsBatchingPeriod={50}
/>
Parameter Guide:
  • initialNumToRender: First batch size (default: 10)
  • maxToRenderPerBatch: Items per subsequent batch (default: 10)
  • windowSize: Screen lengths to keep mounted (default: 21)
  • Lower values = less memory, more blank content during fast scrolling

5. Remove Clipped Subviews

Unmount off-screen components (Android primarily):
<FlatList
  data={DATA}
  renderItem={renderItem}
  removeClippedSubviews={true}
/>
Benefits:
  • Reduced memory usage
  • Better performance on Android
Caveats:
  • May cause issues with animations
  • Can have bugs with complex layouts

Advanced Optimizations

Avoid Anonymous Functions

// Bad: creates new function every render
<FlatList
  data={DATA}
  renderItem={({ item }) => <Item title={item.title} />}
  onEndReached={() => loadMore()}
/>

// Good: stable function references
const renderItem = useCallback(({ item }) => (
  <Item title={item.title} />
), []);

const handleEndReached = useCallback(() => {
  loadMore();
}, []);

<FlatList
  data={DATA}
  renderItem={renderItem}
  onEndReached={handleEndReached}
/>

Optimize Images

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

const Item = memo(({ imageUrl, title }) => (
  <View style={styles.item}>
    <FastImage
      source={{
        uri: imageUrl,
        priority: FastImage.priority.normal,
      }}
      style={styles.image}
      resizeMode={FastImage.resizeMode.cover}
    />
    <Text>{title}</Text>
  </View>
));

Use ListEmptyComponent

const EmptyComponent = () => (
  <View style={styles.empty}>
    <Text>No items found</Text>
  </View>
);

<FlatList
  data={DATA}
  renderItem={renderItem}
  ListEmptyComponent={EmptyComponent}
/>

Implement Pull to Refresh

import { RefreshControl } from 'react-native';

const App = () => {
  const [refreshing, setRefreshing] = useState(false);
  
  const onRefresh = useCallback(async () => {
    setRefreshing(true);
    await fetchData();
    setRefreshing(false);
  }, []);
  
  return (
    <FlatList
      data={DATA}
      renderItem={renderItem}
      refreshControl={
        <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
      }
    />
  );
};

Infinite Scroll

const App = () => {
  const [data, setData] = useState(initialData);
  const [loading, setLoading] = useState(false);
  
  const loadMore = useCallback(() => {
    if (loading) return;
    
    setLoading(true);
    fetchMoreData().then(newData => {
      setData(prev => [...prev, ...newData]);
      setLoading(false);
    });
  }, [loading]);
  
  return (
    <FlatList
      data={data}
      renderItem={renderItem}
      keyExtractor={item => item.id}
      onEndReached={loadMore}
      onEndReachedThreshold={0.5}
      ListFooterComponent={
        loading ? <ActivityIndicator /> : null
      }
    />
  );
};

Measuring Performance

Monitor Render Times

import { Profiler } from 'react';

const onRenderCallback = (id, phase, actualDuration) => {
  console.log(`${id} (${phase}): ${actualDuration}ms`);
};

<Profiler id="FlatList" onRender={onRenderCallback}>
  <FlatList data={DATA} renderItem={renderItem} />
</Profiler>

Track Scroll Performance

const handleScrollBeginDrag = () => {
  performance.mark('scroll-start');
};

const handleScrollEndDrag = () => {
  performance.mark('scroll-end');
  performance.measure('scroll-duration', 'scroll-start', 'scroll-end');
  
  const measure = performance.getEntriesByName('scroll-duration')[0];
  console.log(`Scroll took ${measure.duration}ms`);
};

<FlatList
  data={DATA}
  renderItem={renderItem}
  onScrollBeginDrag={handleScrollBeginDrag}
  onScrollEndDrag={handleScrollEndDrag}
/>

Common Performance Issues

Issue 1: Slow Scrolling

Symptoms: Choppy scrolling, frame drops Solutions:
  • Implement getItemLayout
  • Reduce windowSize
  • Optimize renderItem with React.memo
  • Avoid heavy computations in render

Issue 2: Blank Content While Scrolling

Symptoms: White space during fast scrolling Solutions:
  • Increase initialNumToRender
  • Increase maxToRenderPerBatch
  • Increase windowSize
<FlatList
  initialNumToRender={20}  // Increase from 10
  maxToRenderPerBatch={15} // Increase from 10
  windowSize={10}          // Increase from 5
/>

Issue 3: Memory Issues

Symptoms: App crashes with large lists Solutions:
  • Decrease windowSize
  • Enable removeClippedSubviews
  • Optimize images (compress, use thumbnails)
  • Implement pagination

Issue 4: Incorrect Item Updates

Symptoms: Wrong items update or display Solutions:
  • Use stable keyExtractor
  • Implement proper extraData prop
const [selectedId, setSelectedId] = useState(null);

<FlatList
  data={DATA}
  renderItem={renderItem}
  keyExtractor={item => item.id}
  extraData={selectedId} // Re-render when selection changes
/>

FlatList vs SectionList

Use SectionList for grouped data:
import { SectionList } from 'react-native';

const SECTIONS = [
  { title: 'A', data: ['Alice', 'Amy'] },
  { title: 'B', data: ['Bob', 'Bill'] },
];

<SectionList
  sections={SECTIONS}
  renderItem={({ item }) => <Item title={item} />}
  renderSectionHeader={({ section }) => (
    <Text style={styles.header}>{section.title}</Text>
  )}
  keyExtractor={item => item}
/>
Both support the same optimization props.

Testing Performance

Create Test Data

const generateTestData = (count) => {
  return Array.from({ length: count }, (_, i) => ({
    id: `item-${i}`,
    title: `Item ${i}`,
    description: `Description for item ${i}`,
    imageUrl: `https://picsum.photos/100/100?random=${i}`,
  }));
};

const TEST_DATA = generateTestData(10000);

Benchmark Configurations

const configs = [
  { name: 'Default', props: {} },
  {
    name: 'Optimized',
    props: {
      initialNumToRender: 15,
      maxToRenderPerBatch: 10,
      windowSize: 5,
      removeClippedSubviews: true,
      getItemLayout: (data, index) => ({
        length: ITEM_HEIGHT,
        offset: ITEM_HEIGHT * index,
        index,
      }),
    },
  },
];

Best Practices Checklist

  • Use stable keyExtractor (not index)
  • Implement getItemLayout for fixed-height items
  • Wrap items with React.memo
  • Use useCallback for event handlers
  • Configure initialNumToRender, maxToRenderPerBatch, windowSize
  • Enable removeClippedSubviews on Android
  • Optimize images (size, caching, loading)
  • Avoid anonymous functions in props
  • Profile and measure performance
  • Test on low-end devices

Next Steps

Build docs developers (and LLMs) love