· 5 min read

How Telegram Open Source Teams Can Cut Latency by Reworking Message List Rendering

Telegram’s open source clients have always had to balance speed, smoothness, and feature richness. One of the most effective performance wins is reducing how much work the app does when the message list changes, especially during fast scrolling, updates, and chat switches.

Telegram’s open source clients are built for heavy real-world usage: long chat histories, media-rich threads, rapid message bursts, and frequent state updates. That makes performance work less about isolated micro-optimizations and more about keeping the UI responsive when data changes constantly.

A high-impact place to improve is message list rendering. If a client spends too much time rebuilding rows, recalculating layout, or diffing large message arrays on the main thread, users feel it immediately as scroll hitching, delayed input, or slow chat transitions.

Why message lists become expensive

In a Telegram-style client, the message feed is rarely static. A single visible chat can receive:

  • new incoming messages
  • edited messages
  • read receipt changes
  • typing indicators
  • delivery state changes
  • media download progress updates
  • reactions and view count updates
  • pinned message changes

If each of those events triggers a full list refresh, the UI pays for work that only affects a small portion of the screen.

The common failure mode looks like this:

  1. A state update arrives.
  2. The message array or view model is rebuilt.
  3. The UI invalidates a large portion of the visible list.
  4. Layout and binding run for more items than necessary.
  5. Frames are dropped during scroll or animation.

The fix is not just “make it faster,” but “make the UI do less.”

The optimization: incremental updates instead of broad invalidation

A strong improvement is to switch from broad list refreshes to targeted incremental updates.

Instead of treating every event as a reason to re-render the entire conversation, classify updates by scope:

  • content changes: a message text edit or reaction count change
  • metadata changes: delivery state, read state, typing, pinned state
  • structural changes: insert, delete, reorder, or day separator changes

Only structural changes should affect list composition. Many metadata changes can be handled by updating a single visible row, often without recalculating the entire dataset.

What this changes in practice

A chat with 500 visible/near-visible items should not pay O(n) work when one message transitions from “sent” to “read.”

A better flow is:

  • identify the affected message IDs
  • map them to currently visible indexes
  • update only those rows
  • preserve row height when possible
  • avoid relayout unless the content size actually changed

This reduces CPU work and keeps the main thread available for input and animation.

Practical implementation pattern

The exact code differs across Telegram clients and platforms, but the architectural pattern is similar.

1. Keep a stable message identity

Every message needs a stable identity that survives list refreshes. If your adapter or data source relies on position alone, even a small insert near the top can cause a cascade of rebinds.

Use message IDs and, where needed, composite keys for:

  • header rows
  • date separators
  • service messages
  • grouped media items

This lets the UI diff old and new state intelligently.

2. Split model updates from presentation updates

Don’t merge network/state processing with view invalidation.

A useful structure is:

  • backend/store updates the canonical chat state
  • a projection layer derives visible rows
  • the UI subscribes only to minimal row-level changes

That separation helps prevent accidental full-list refreshes when only one badge or icon changes.

3. Diff at the row level, not the chat level

A chat-level “changed” signal is too broad. Prefer signals like:

  • row added at index 3
  • row removed at index 18
  • row 42 content updated, size unchanged
  • row 11 height changed

Once you have this granularity, the list framework can apply smaller patches and skip redundant measurement.

4. Cache expensive derived values

Telegram chat rows often include derived data such as:

  • formatted timestamps
  • text layout objects
  • media thumbnail sizing
  • link preview layout
  • sender name presentation
  • bubble height calculations

If these are recomputed on every bind, performance suffers even when the visible content barely changes.

Cache derived values by message version or render key, and invalidate them only when the underlying fields change.

A concrete bottleneck to watch: repeated layout measurement

One of the easiest ways to lose performance is repeated measurement of message bubbles during scroll.

This can happen when the client:

  • measures text every time a row is bound
  • recalculates media aspect ratios repeatedly
  • rebuilds inline reply or reaction subviews from scratch
  • reflows the same message during minor state updates

A better approach is to make row measurement deterministic and cacheable. For example:

  • precompute text layout for a given width bucket
  • store bubble height with its render inputs
  • invalidate only when width, font scale, or content version changes

This pays off especially in long chats, where the same rows are revisited constantly during scroll.

Avoiding main-thread stalls

Performance improvements only matter if they reduce work on the thread that drives the UI.

For Telegram clients, the highest-value rule is:

never block the main thread with list-wide computation that can happen incrementally or off-thread.

That means:

  • diffing message sets off the UI thread when possible
  • preparing text/layout caches in the background
  • batching rapid updates into a single UI transaction
  • coalescing repeated state changes for the same message

A common example is reactions or read receipts. If the backend emits several updates in a burst, you don’t want five separate row invalidations. Batch them into one pass.

Measuring the gain

Don’t rely on intuition. Measure before and after.

Useful metrics include:

  • frame time during rapid scroll
  • number of row rebinds per state update
  • number of layout passes per message edit
  • time spent diffing message collections
  • frequency of main-thread work during burst updates

In a mature client, a good optimization usually shows up as:

  • fewer dropped frames
  • faster chat open time
  • smoother media-heavy threads
  • lower CPU use during idle update periods

What to test after the change

A targeted rendering optimization can regress behavior if it is too aggressive. Test scenarios that stress edge cases:

  • a chat receiving 20 messages in quick succession
  • edits to a message currently on screen
  • reaction count updates across several visible rows
  • switching between chats with very different history sizes
  • scrolling while new messages arrive
  • changing text scale or theme while in a conversation

Also verify that list state remains correct when structural updates and metadata updates happen together.

The takeaway

For Telegram open source developers, one of the best performance improvements is not a new algorithm in the abstract. It’s a change in update granularity: make the message list smarter about what actually changed.

When the app updates only the affected rows, preserves stable identities, caches derived layout data, and keeps list diffing off the main thread, the result is a Telegram client that feels faster without sacrificing features.

In chat software, responsiveness is the product. A well-scoped rendering pipeline is often the difference between an app that merely works and one that feels instant.

← All posts