ScrollView renders every child at once, visible or not: ideal for a form, wasteful for a catalog. FlatList renders lazily, mounting a first window of rows, adding more as they approach the viewport and unmounting rows far off screen. This benchmark counts the rows each renders before first layout:
const ROWS = Array.from({ length: 2000 }, (_, i) => `Reading note ${i + 1}`);
const MODE: 'scroll' | 'flat' = 'scroll';
const start = performance.now();
let rendered = 0;
function Row({ label }: { label: string }) {
rendered += 1;
return <Text style={{ padding: 12 }}>{label}</Text>;
}
const report = () => console.log(`${MODE}: ${rendered} rows rendered, first layout ` +
`after ${Math.round(performance.now() - start)} ms`);
export default function ListBench() {
return MODE === 'scroll' ? (
<ScrollView onLayout={report}>{ROWS.map((r) => <Row key={r} label={r} />)}</ScrollView>
) : (
<FlatList data={ROWS} onLayout={report} renderItem={({ item }) => <Row label={item} />} />
);
}Run once as printed and once with MODE set to 'flat', each from a cold start, it logged:
LOG scroll: 2000 rows rendered, first layout after 5918 ms LOG flat: 10 rows rendered, first layout after 675 ms
ScrollView built all 2,000 rows before the screen could lay out, almost six seconds in this debug build; FlatList built ten, its default initialNumToRender, and was on screen in under 0.7 s. Lists covers FlatList, SectionList and Shopify 175 's FlashList 7,238 , which recycles row views. ScrollView's default removeClippedSubviews on Android detaches off-screen children from the native hierarchy, but their JavaScript and shadow-tree work still happens.