Info:
This is release candidate documentation for Stream Chat React Native SDK v10. For the latest stable version, see the latest version (v9).

Performance

Most of the SDK's cost on a phone comes from two things: how many events the app receives, and how many message rows the list renders. Busy channels, such as livestreams, have more of both. This page covers the settings that keep the app responsive under load, and the habits that keep custom components cheap.

Best practices

  • Turn off the events a channel type doesn't need, on the server.
  • Keep the page size small, and cap how many messages stay loaded in long sessions.
  • Raise the state throttle only for busy channels, and check that reactions and receipts still feel live.
  • Define component overrides and store selectors at module scope.
  • Measure in a release build, on a real device.

Channel settings

Each channel type has settings on the server that apply to all its channels, including which events are sent over the WebSocket. An event that isn't sent costs the app nothing. For a livestream, turn off:

  • Connect events, the presence events sent when members connect and disconnect;
  • Read events, the read receipts;
  • Typing events, the typing indicators.

Change them per channel type in the Stream Dashboard. Slow mode also limits how often each user can send a message. Read state, unread and receipts and Typing indicators explain what users lose without them.

How many messages load

A channel's message list loads 25 messages a page. Channel sets that page size when it mounts, unless one is set in client.config. A thread's list loads 50. Every message loaded is a row the list can mount, and the newest page decides how long the first render takes.

Set the page size in client.config, for channels, threads, or both:

client.config.set({
  channel: { messagePaginator: { pageSize: 20 } },
  thread: { messagePaginator: { pageSize: 25 } },
});

A change to these keys rebuilds the settings of every channel's message list and drops the page size Channel set, so name pageSize whenever you set other messagePaginator keys, as in the examples below. What the React Native SDK sets explains it.

State update throttling

A busy channel can get dozens of events a second: new messages, reactions, read receipts. A message list publishes its messages at most once every 500 ms, so a burst of events causes about two renders a second. Loading a page, jumping, and the user's own sends, edits and reactions publish at once.

stateThrottleMs changes the interval. A longer one renders less often, but other users' messages and reactions appear later. Raise it for busy channels only:

client.config.set({
  channel: { messagePaginator: { pageSize: 25, stateThrottleMs: 1000 } },
});

Throttling and the memory cap explains what waits and what doesn't.

Capping the loaded messages

A long session in a busy channel keeps adding messages to memory, and rows to the list. maxLoadedItems caps the newest window. Once it holds more messages than the cap, the oldest are dropped as new ones arrive. Scrolling back loads them again.

client.config.set({
  channel: { messagePaginator: { maxLoadedItems: 300, pageSize: 25 } },
});

The cap is off by default. MessageList and MessageFlashList pause it while the user reads within a fifth of the cap from the oldest loaded message, so messages don't disappear under them. Unsent messages are never dropped, and a cap smaller than the page size counts as the page size.

Livestreams

isLiveStreaming on MessageList or MessageFlashList tunes the list for many messages a second. The list renders its messages at most once per frame, and reports scrolling every frame. On MessageList, a new message also scrolls the list to it while the user is within 300 points of the newest message, instead of 10. It's experimental. The Livestream message list UI cookbook combines it with the settings above.

These props of Channel take parts out of every row, at the cost of what those parts show:

  • hideDateSeparators removes the date separators between days. Each row then skips comparing its day with the day of the message above it.
  • hideStickyDateHeader removes the date header at the top of the list. The list then skips working out the day of the topmost message each time the visible messages change.
  • enableMessageGroupingByUser={false} turns grouping off. No message then counts as the last of its group, so none shows the avatar, and only a message that is sending or failed shows the footer with the author's name and the time. Rows have fewer views, and a new message doesn't take the avatar and the footer away from the same author's message before it. Show the author some other way if users need it.

With grouping off, MessageAuthor still renders an empty spacer the width of an avatar in each received message. Replace it with a component that renders nothing to remove that column:

import type { PropsWithChildren } from "react";
import type { Channel as StreamChannel } from "stream-chat";
import { Channel, WithComponents } from "stream-chat-react-native";

// Declared outside the component, so WithComponents gets the same object on every render.
const overrides = { MessageAuthor: () => null };

export const LivestreamChannel = ({
  channel,
  children,
}: PropsWithChildren<{ channel: StreamChannel }>) => (
  <WithComponents overrides={overrides}>
    <Channel
      channel={channel}
      enableMessageGroupingByUser={false}
      hideDateSeparators
      hideStickyDateHeader
    >
      {children}
    </Channel>
  </WithComponents>
);

MessageFlashList

MessageFlashList renders the messages with FlashList from @shopify/flash-list, version 2.1 or later, which you install. It reuses the cells of rows that scroll out of view for the rows that scroll in, between messages of the same shape, such as text only or with a gallery. MessageList, built on FlatList, creates a cell for each message.

import type { Channel as StreamChannel } from "stream-chat";
import {
  Channel,
  MessageComposer,
  MessageFlashList,
} from "stream-chat-react-native";

export const ChannelScreen = ({ channel }: { channel: StreamChannel }) => (
  <Channel channel={channel}>
    <MessageFlashList />
    <MessageComposer />
  </Channel>
);

MessageFlashList is experimental, and its implementation can change. Use MessageList unless profiling shows the list is the bottleneck. MessageList and MessageFlashList lists the differences.

Stable overrides and selectors

  • Overrides. WithComponents reads its overrides once, when it mounts. Define the object and the components in it at module scope, outside any component. Overrides are read once explains it.
  • Selectors. useStateStore(store, selector) subscribes with the selector it gets. An inline selector is a new function on every render, so the hook subscribes again each time. Declare selectors at module scope, and select only what the component shows: a component re-renders when a value it selected changes. Selector identity explains it.
  • Configuration functions. Functions you pass in configuration, such as hasInteractiveAccessibilityContent in the accessibility prop of OverlayProvider, are read by every message. Define them at module scope too.
  • Message rows. The list creates every view in a row each time it mounts the row. A custom message component that stays shallow, with few nested views, mounts faster.

Images

The SDK renders images with the ImageComponent override key, which defaults to React Native's Image, and to SvgUri from react-native-svg for SVG images. The image attachments, Giphy images, avatars, link previews, the quoted message preview, the composer's attachment previews and the channel details media list use it. The full-screen image gallery doesn't.

To cache images on disk, pass an image component that caches. The SampleApp uses @d11/react-native-fast-image:

import type { PropsWithChildren } from "react";
import type { ImageProps } from "react-native";
import FastImage from "@d11/react-native-fast-image";
import type { FastImageProps } from "@d11/react-native-fast-image";
import { WithComponents } from "stream-chat-react-native";
import type { ComponentOverrides } from "stream-chat-react-native";

// Takes React Native's Image props, as the SDK passes them.
const CachedImage = (props: ImageProps) => (
  <FastImage {...(props as unknown as FastImageProps)} />
);

const overrides: ComponentOverrides = { ImageComponent: CachedImage };

export const WithCachedImages = ({ children }: PropsWithChildren) => (
  <WithComponents overrides={overrides}>{children}</WithComponents>
);

A replacement renders SVG images only if it supports them itself. For images on Stream's CDN, the image attachments, the full-screen gallery and the channel details media list ask for an image of the size they show, so they don't download the full image.

Measuring

Measure in a release build, on a real device. A development build runs React's development checks and loads the JavaScript from Metro, so it's much slower, and an emulator doesn't behave like a phone. React Native's performance overview and profiling guides cover the tools.

Reference