Skip to content

[Web] FlashList renders every item when it re-renders inside a hidden (display: none) container #2522

Description

@HuuNguyen312

Description

On web, if a FlashList re-renders while its container is hidden (display: none, e.g. an inactive screen or column that stays mounted), the list measures every size as 0 and then mounts every item in the data set at once. With a few thousand items this freezes the app.

Root cause: the layout effects in RecyclerView still run while the container is hidden. On web, measureParentSize and measureItemLayout read clientWidth / clientHeight, which are 0 for an element inside a display: none subtree, so:

  1. The window size becomes 0 x 0.
  2. Every rendered item is re-measured as height: 0, overwriting its real height.
  3. The height average window (last 5 samples) drops to ~0, so unmeasured items are also estimated at ~0.

All layouts collapse to y = 0, height = 0, so getVisibleLayouts returns the whole data set as engaged.

Current behavior

All items mount at once while the container is hidden. Debug output from RecyclerViewManager.updateRenderStack in our app (inverted chat list with ~6,400 messages, hidden on logout):

engaged 0 5686 count 5687 data 6384 window {"width":0,"height":0}
layoutSize {"width":0,"height":842688} scrollOffset -8 firstItemOffset 0
measured 517 measuredZero 517
0:y=0,h=0,m=1 | 1:y=0,h=0,m=1 | 2:y=0,h=0,m=1 | ...

Stack: updateRenderStack ← updateScrollOffset ← recomputeEngagedIndices ← modifyChildrenLayout ← item-measurement useLayoutEffect in RecyclerView.

Expected behavior

While the container is hidden, FlashList should skip measuring and keep the last valid layouts. Once the container is shown again, it should re-measure and render only the visible window.

Reproduction

Expo Snack or minimal reproduction link: none yet. Minimal web reproduction:

import React, { useState } from "react";
import { Button, Text, View } from "react-native";
import { FlashList } from "@shopify/flash-list";

const makeData = (n: number) => Array.from({ length: n }, (_, i) => i);

export default function App() {
  const [hidden, setHidden] = useState(false);
  const [data, setData] = useState(() => makeData(5000));

  return (
    <View style={{ flex: 1 }}>
      <Button title="Toggle hidden" onPress={() => setHidden((h) => !h)} />
      <Button title="Update data" onPress={() => setData(makeData(5001))} />
      <View style={{ flex: 1, display: hidden ? "none" : "flex" }}>
        <FlashList
          data={data}
          keyExtractor={(item) => String(item)}
          renderItem={({ item }) => {
            console.log("render", item);
            return <Text style={{ padding: 16 }}>Item {item}</Text>;
          }}
        />
      </View>
    </View>
  );
}
  1. Run on web. Only the visible window renders.
  2. Press Toggle hidden, then Update data.
  3. The console logs render for all ~5000 items.

Platform

  • iOS
  • Android
  • Web (if applicable)

Environment

React Native Web inside Electron. React 19.2.3, TypeScript 5.8.3.

FlashList version: 2.3.3

Additional context

Proposed fix: skip the container and item measurement layout effects while the container reports 0 x 0 on web, and record a 0 x 0 container size so the existing onLayout check re-measures when the container is shown again. PR incoming.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions