Performance
The chat is built for long lists. The one habit that matters most: memoize your render props and config, because each message row is React.memo'd with a comparator that deep-compares the message and reference-compares everything else - so an inline function or object re-renders that row on every parent render.
// ❌ New reference every render - the row can't skip
<Chat renderBubble={props => <MyBubble {...props} />} reactions={{ isEnabled: true, onReactionPress }} />
// ✅ Stable references - unchanged rows skip re-renders
const renderBubble = useCallback(props => <MyBubble {...props} />, [])
const reactions = useMemo(() => ({ isEnabled: true, onReactionPress }), [onReactionPress])
<Chat renderBubble={renderBubble} reactions={reactions} />
Immutability, context props and virtualization tuning
The memoization advice applies to all render props (renderBubble, renderMessageText, renderAvatar, ...), the reactions / audioRecording / videoRecording / messageActions config objects, and any style objects.
Keep messages immutable. Update messages by creating new arrays/objects (e.g. Chat.append(...)), never by mutating an existing message in place - the row comparator relies on value changes to detect updates.
Theme, icons and labels don't need drilling. theme / darkTheme, icons, and labels are read from context (useTheme, useIcons, useLabels), so passing them once on <Chat> is enough; they don't cause per-row churn.
Tune virtualization if needed. Sensible FlatList defaults ship out of the box (removeClippedSubviews on Android, initialNumToRender, maxToRenderPerBatch, windowSize, updateCellsBatchingPeriod). windowSize is measured in screen-heights (not messages); the default keeps a few screens of content mounted around the viewport. Override any of them via listProps:
<Chat listProps={{ windowSize: 7, removeClippedSubviews: true }} {...props} />
FlashList (opt-in)
Recycling rows on very long histories
On long histories FlatList can log VirtualizedList: You have a large list that is slow to update. FlashList v2 recycles rows instead of keeping them mounted, which removes that class of stall. It is supported as an optional dependency - install it yourself and flip one prop:
yarn add @shopify/flash-list
<Chat messages={messages} user={user} isFlashListEnabled />
Everything else keeps working: isInverted, the floating day header, loadEarlierMessagesProps infinite scroll, the scroll-to-bottom button, and listProps (spread last, so it overrides the defaults below).
The chat sets FlashList's maintainVisibleContentPosition for you - startRenderingFromBottom when isInverted={false}, plus autoscrollToBottomThreshold: 0.2 so new messages follow the viewport only when you are already at the bottom. Override it through listProps if you want different thresholds:
<Chat
isFlashListEnabled
listProps={{
maintainVisibleContentPosition: {
autoscrollToBottomThreshold: 0.1,
animateAutoScrollToBottom: false,
},
}}
{...props}
/>
Notes:
- FlashList v2 requires the New Architecture. On the old architecture it falls back to a slower JS path.
FlatList-only knobs (windowSize,maxToRenderPerBatch,initialNumToRender,updateCellsBatchingPeriod,removeClippedSubviews) are not forwarded to FlashList - it sizes its own render window.- If
@shopify/flash-listis not installed, the prop is ignored, a warning is logged, andFlatListis used.