skip to content

Windowing & Render Tuning

VirtualizedList mounts rows in a window measured in screen heights and fills it in batches, trading memory for blank areas on fast scrolls. Interviewers ask which knobs fix jank and at what cost.

part ofReact Nativeoverview, primer and where to startread it →
on this pageshow

explore

questions

5

In a React Native FlatList, what do windowSize, initialNumToRender and maxToRenderPerBatch control, and what are their defaults?

level: middleimportance: must knowfreq 62%

answer

  1. measured in viewport lengths
  2. first render versus later batches
  3. one odd window, two round tens
  4. a timer between low-priority batches

basics

~10 s

windowSize sets how many viewport lengths of rows stay mounted (default 21); initialNumToRender sets how many rows the first render mounts (default 10); maxToRenderPerBatch caps the new rows added per incremental batch (default 10).

solid answer

~40 s

`FlatList` sits on `VirtualizedList`, which mounts only a window of rows around the viewport and stands in for the rest with spacer views. `windowSize` sizes that window in **visible lengths**, not rows: the default `21` means the viewport plus up to 10 screens before and 10 after. `initialNumToRender` (default `10`) is how many rows the first render mounts, and those rows stay mounted for fast scroll-to-top unless `initialScrollIndex` is set. After that the window grows in batches: `maxToRenderPerBatch` (default `10`) caps new rows per pass, and `updateCellsBatchingPeriod` (default `50` ms) spaces low-priority passes. Raising them fills the window faster at the cost of memory and JavaScript-thread time; lowering them saves both but reveals blank space on fast scrolls.

code

tsx · 17 lines
tsx
import { FlatList, Text } from 'react-native';

type Quote = { symbol: string; price: number };

export function QuoteList({ quotes }: { quotes: Quote[] }) {
  return (
    <FlatList
      data={quotes}
      keyExtractor={(q) => q.symbol}
      renderItem={({ item }) => <Text>{item.symbol} {item.price}</Text>}
      initialNumToRender={15}
      windowSize={21}
      maxToRenderPerBatch={10}
      updateCellsBatchingPeriod={50}
    />
  );
}

go deeper

for a junior

Recall that FlatList only mounts rows near the screen, and name windowSize, initialNumToRender and maxToRenderPerBatch with their defaults of 21, 10 and 10.

for a middle

Explain that windowSize is counted in viewport lengths, how the first render differs from later batches, and why the first initialNumToRender rows stay mounted.

for a senior

Tie each knob to its cost on a real device: memory for the window, JavaScript-thread time for batches, time to content for the initial render, and insist on measuring a release build first.

for a principal

Frame the defaults as a general-purpose compromise and discuss when a team should tune per screen, standardize row components instead, or move a list to a different list implementation.

## What windowing means in a FlatList `FlatList` is a thin layer over **`VirtualizedList`**, the component React Native uses for every long list (including `SectionList`). A `VirtualizedList` does not mount one React element per item in `data`. It keeps a **render window**: a contiguous range of rows around the viewport that are actually mounted. Everything outside that window is replaced by **spacer `View`s** whose height (or width, for a horizontal list) is the list's best estimate of the rows they stand in for, so the scroll content keeps roughly the right total length. Because the window is measured around the viewport, it moves with every scroll: rows entering it are rendered, rows leaving it are unmounted, and the spacers grow or shrink so the scroll position stays stable. Four props decide how big that window is and how fast it fills. All four are `VirtualizedList` props that `FlatList` passes straight through. ## The four knobs and their defaults | Prop | Unit | Default in 0.87 | What it controls | |---|---|---|---| | `windowSize` | visible lengths of the list | `21` | Maximum area kept mounted: the viewport plus up to 10 viewport lengths before and 10 after | | `initialNumToRender` | rows | `10` | How many rows the very first render mounts | | `maxToRenderPerBatch` | rows | `10` | The most **new** rows added in one incremental render pass | | `updateCellsBatchingPeriod` | milliseconds | `50` | The delay before a low-priority batch runs | The unit of `windowSize` is the trap. It is **not** a row count. On an 800 px tall list whose rows are 80 px, one visible length holds 10 rows, so the default window can hold roughly 210 rows; with 40 px rows it is roughly 420. The source computes the extra area as `(windowSize - 1) * visibleLength` and splits it evenly before and after the viewport. ## How the window fills after the first render 1. On mount, the list renders the first `initialNumToRender` rows. These rows are also **kept mounted for good** as a "scroll to top" optimization, unless `initialScrollIndex` is set to a positive index. 2. On every scroll event the list recomputes the target window from the scroll offset, the viewport length and the measured (or `getItemLayout`-supplied) row sizes. 3. It then decides the priority of the update. If the viewport has already reached the edge of the rendered rows, or is closing on it quickly, the update runs **immediately**. Otherwise it is scheduled with a `setTimeout` of `updateCellsBatchingPeriod` milliseconds. 4. Each pass adds at most `maxToRenderPerBatch` new rows, working outward from the viewport, then repeats until the whole window is filled. 5. Rows that fall outside the new window are unmounted, apart from the retained initial rows, and their state goes with them. ## The tradeoffs of turning each knob - **Larger `windowSize`**: fewer blank areas on fast scrolls, but more mounted rows, more memory and more work when `data` changes. - **Smaller `windowSize`**: less memory, but the list has less pre-rendered runway, so a fling reaches unrendered space sooner. - **Larger `initialNumToRender`**: no blank area on the first frame for tall screens, but a slower first render (time to content). - **Smaller `initialNumToRender`**: faster first render, but if it does not cover the viewport, the user sees blank space until the next batch. - **Larger `maxToRenderPerBatch`**: better **fill rate**, but each batch is a longer block of JavaScript work that can delay responses to presses. - **Shorter `updateCellsBatchingPeriod`**: the off-screen part of the window fills sooner, at the price of more frequent JavaScript work. None of these values is free, and they interact: a big window with a small batch size takes many passes to fill, and a big batch with a short period keeps the JavaScript thread busy. ## Reading the defaults in an interview A good answer states the defaults, the unit of `windowSize`, and the direction of each tradeoff: **memory versus blank areas** for the window, **fill rate versus responsiveness** for the batch props. It also knows that the defaults are tuned for general use: an interviewer asking "what would you change?" wants to hear that you would measure on a release build before touching them, and that the row component's own render cost usually matters more than the numbers. Because these are `VirtualizedList` defaults, `SectionList` shares the same ones.

  • Why are the first initialNumToRender rows treated differently from the rest?
    They are kept mounted permanently as a scroll-to-top optimization, so jumping back to the top shows content immediately. That retention is skipped when `initialScrollIndex` is a positive index, because the list then starts rendering from that index instead of from row 0.
  • With windowSize 5 on an 800 px list of 40 px rows, roughly how many rows are mounted?
    One visible length holds 20 rows, and `windowSize` 5 means the viewport plus two lengths before and two after, so up to about 100 rows. The count depends on row size because `windowSize` is measured in viewport lengths, not rows.
  • Does updateCellsBatchingPeriod delay rendering when the user has already scrolled into blank space?
    No. When the viewport reaches, or is fast approaching, the edge of the rendered rows, `VirtualizedList` treats the update as high priority and renders at once instead of waiting for the timer. The period only spaces the lower-priority passes that fill the rest of the window.

saying these in an interview costs you the question

  • windowSize is the number of rows kept mounted
  • Raising initialNumToRender makes a list faster with no cost
  • maxToRenderPerBatch caps the total rows the list will ever render
  • updateCellsBatchingPeriod controls how often onScroll events fire
  • Rows leaving the window are reused for new items, like a native table
open as a page

A React Native FlatList of 10,000 stock-ticker rows shows blank rows on fast flings; what causes the blanks, and which props would you tune, at what cost?

level: seniorimportance: must knowfreq 55%

basics

~20 s

The fling outruns the fill rate: rows beyond the render window are empty spacers until JavaScript renders the next batch. Lighter rows, getItemLayout, larger batches and a wider window help, costing JavaScript-thread time and memory.

open as a page

In a React Native FlatList, why should the row component returned by renderItem usually be wrapped in React.memo?

level: juniorimportance: should knowfreq 58%

basics

~20 s

When a FlatList re-renders, every mounted cell calls renderItem again, which can mean hundreds of rows. Wrapping the row in React.memo lets rows with unchanged props skip rendering, so one updated item re-renders one row.

open as a page

In a React Native FlatList, what does getItemLayout do, when can you supply it, and what goes wrong if its offsets are wrong?

level: middleimportance: should knowfreq 52%

basics

~20 s

getItemLayout returns each row's length and offset from the data, so FlatList can skip measuring rendered cells and size spacers exactly. It suits rows whose sizes are known in advance; wrong offsets cause jumping content and misplaced scrollToIndex targets.

open as a page

In a React Native FlatList, what does removeClippedSubviews do, what is its default per platform, and why can it hide content?

level: middleimportance: should knowfreq 32%

basics

~20 s

removeClippedSubviews detaches native views outside the viewport from the native hierarchy to cut drawing work. FlatList defaults it to true on Android and false on iOS; transforms or absolute positioning can make visible content disappear.

open as a page