---
title: "Streaming UI: Rendering AI Responses Token by Token"
description: "Incremental text rendering, streaming tool results, and optimistic UI updates without layout jank. Here is how we built real-time chat rendering."
canonical: https://nowah.xyz/blog/streaming-ui-rendering-ai-responses
lastModified: "2026-08-07T03:50:01.575Z"
---

# Streaming UI: Rendering AI Responses Token by Token

Incremental text rendering, streaming tool results, and optimistic UI updates without layout jank. Here is how we built real-time chat rendering.

A 5-second search that streams progress feels faster than a 3-second search behind a spinner. This is not opinion; it is backed by perceived latency research. Humans tolerate waiting much better when they can see something happening.

For an AI travel agent, streaming is not optional. Our agent needs time to think, search, and compose responses. Without streaming, users stare at a blank screen for 5-15 seconds per turn. With streaming, they watch the response materialize in real time and can start processing the information before the agent finishes. The engineering to make this feel smooth is surprisingly involved.

## Incremental text rendering without layout jank

![Illustration for this section](https://pics.nowah.xyz/website-media/engineering-029-img-1.webp)

The naive approach to streaming text is: receive a token, append it to a string, re-render. This works for about 50 tokens before you notice the problems.

Each time the text content changes, the browser (or the mobile app framework) recalculates the layout of the text element. If the text wraps to a new line, every element below it shifts down. If the chat auto-scrolls, the scroll position jumps. Do this 30 times per second and you get visible jank: the text flickers, the scroll stutters, and the whole experience feels janky.

We solve this in layers. First, we batch state updates. Instead of updating state on every single token, we batch tokens over a short window (roughly 16ms, one frame) and apply them together. This reduces the number of React re-renders from hundreds per second to a manageable 60.

Second, we decouple the streaming text component from the rest of the message list. The streaming message sits in its own component with its own state. Only that component re-renders as tokens arrive. The rest of the conversation stays completely stable.

Third, we handle scroll anchoring carefully. When the user is at the bottom of the chat, we pin the scroll to the bottom as new content arrives. We do this by measuring content height changes and adjusting scroll position in the same frame as the DOM update, so there is no visual gap between content appearing and scroll adjusting.

## Streaming state management in React

Managing streaming state in React requires thinking differently about the update cycle. Normal React state updates are discrete: something happens, state changes, component re-renders. Streaming updates are continuous: tokens arrive constantly for seconds at a time.

We use a ref to accumulate tokens and a state variable that updates on a throttled schedule. The ref is the source of truth for the current text. The state variable triggers re-renders at a controlled rate. When the stream ends, we do one final state update to ensure the component reflects the complete response.

This pattern avoids the classic problem of queueing hundreds of setState calls that React has to process sequentially. Instead, we accumulate cheaply in the ref and flush to state on a schedule.

For streaming tool results, the pattern is different. When the AI agent executes a search, we receive structured progress events: "searching for flights," "found 47 results," "ranking by your preferences," "selected top 3." These are discrete events, not a continuous token stream, so we handle them with normal state updates. They appear as status messages in the chat that update in place as new progress events arrive.

## Handling partial tool results

![Supporting diagram](https://pics.nowah.xyz/website-media/engineering-029-img-2.webp)

One of our more interesting streaming challenges is showing partial search results. When the agent searches for flights, the search might take 5-8 seconds. Rather than showing nothing during that time, we stream progress indicators that tell the user what is happening.

The flow looks like this: the user asks for flights. The agent decides to search. We immediately show a "Searching for flights..." message in the chat. As the backend processes the search, we receive events: "Checking 4 airlines," "Found 23 options," "Ranking results." Each event updates the status message in place. When results are ready, the status message transitions into the actual flight cards.

This transition from status indicator to result cards has to be seamless. We pre-allocate space for the cards based on the expected result format, so when the data arrives, the layout does not jump. The cards fade in where the loading indicator was.

## Optimistic UI updates during streaming

We use optimistic updates in several places during streaming. The most visible one is the user's own message. When you type a message and hit send, it appears in the chat immediately. We do not wait for the server to acknowledge it. This makes the chat feel instant.

If the server rejects the message (rate limit, content filter, connection error), we mark the message as failed and show a retry option. This happens rarely enough that the tradeoff is worth it.

We also use optimistic updates for card interactions. When you tap "Select" on a flight card, the card immediately shows a selected state with a visual indicator. The actual booking review modal might take a moment to load with full pricing, but the selection feedback is instant.

## Testing streaming UI components

Testing streaming UI is harder than testing normal UI. A snapshot test tells you what a component looks like at a point in time. A streaming component's whole point is that it changes continuously.

Our approach is to test at three levels. First, we test the streaming [state management](/blog/state-management-ai-conversations) logic in isolation. Given a sequence of tokens, does the batching work correctly? Does the final state match the complete response? Second, we test the individual components with static props to verify they render correctly with partial and complete data. Third, we run integration tests that simulate a full streaming sequence and verify that the final rendered output matches expectations.

We also do visual regression testing on the streaming components. We capture screenshots at specific points during a simulated stream (empty, 25% complete, 50%, 75%, complete) and diff them against baselines. This catches layout jank that unit tests miss.

## Performance profiling for streaming chat

We profile streaming performance regularly, especially on lower-end mobile devices. Our key metrics are:

**Frames per second during streaming.** We target a consistent 60fps on mid-range devices. If frames drop during token-by-token rendering, users see stutter.

**Memory allocation during streaming.** Continuous state updates can create garbage collection pressure if not managed carefully. We watch for memory spikes during long streams.

**Time to first visible token.** From the moment we start receiving the stream to the moment the first character appears on screen. This should be under 100ms.

**Render count per stream.** How many React renders does a typical streaming response cause? Fewer is better. Our batching keeps this under 200 for a typical response, regardless of how many tokens it contains.

The performance characteristics differ between platforms. the mobile app framework on iOS handles streaming updates differently than the web framework in a browser. We profile on both and optimize for the worse performer, which is usually older Android devices. If streaming feels smooth on a mid-range Android phone from three years ago, it feels smooth everywhere.

---

Nowah is an AI travel agent that searches and books real flights and hotels through conversation — no filters, no thirty open tabs. [Plan your next trip](https://app.nowah.xyz).
