· 7 min read

Practical Performance Tips for Telegram Open Source Clients: Reducing UI Jank in Chat Lists

Telegram’s open source clients move a lot of data through the UI at once, so small inefficiencies can quickly become visible as scrolling lag or delayed message rendering. This article focuses on one high-impact area: keeping chat lists responsive by reducing unnecessary work on the main thread.

Telegram clients have an unusually demanding UI profile: large chat lists, frequent message updates, rich previews, animated elements, and rapid state changes from networking and local caches. In the open source codebase, that means performance is not only about algorithmic complexity; it is also about how much work gets scheduled onto the UI thread, how often views are invalidated, and whether repeated data transformations are being rebuilt during scrolling.

This post focuses on a specific, high-value improvement pattern: keep chat list rendering cheap by reducing unnecessary work during bind, diff, and update paths. If your client feels sluggish when opening a busy folder, switching accounts, or scrolling through a heavy conversation list, this is one of the first areas to inspect.

Why chat lists are the right place to optimize

Chat lists are visible almost immediately after launch and are updated continuously. They also combine several expensive behaviors:

  • unread counters change frequently
  • last-message previews can reflow
  • avatars and thumbnails are loaded asynchronously
  • pinned/reordered chats trigger list mutations
  • typing indicators and presence changes can cause partial invalidations

A small amount of extra work per item becomes expensive when multiplied by hundreds of visible or near-visible rows. In Telegram open source clients, that cost often shows up as:

  • dropped frames while scrolling
  • delayed appearance of updated message previews
  • main-thread stalls during list refreshes
  • battery drain from repeated recomputation

The goal is not to make the list “do nothing.” The goal is to make each update path do the minimum necessary work.

The core performance rule: bind only what changed

A common source of jank is treating every row update as a full rebind. In practice, many chat list changes affect only one or two fields. If the row view recomputes text layouts, reloads images, and restyles the entire item every time a badge count changes, you are burning CPU for no visual benefit.

Best practice

Use granular update payloads or field-level diffing so that row binding can distinguish between:

  • title changes
  • subtitle changes
  • unread count changes
  • mute/archive/pin state changes
  • avatar changes
  • selection state changes

Then update only the affected subviews.

Why this matters

A full bind often triggers:

  • text measurement
  • layout recalculation
  • drawable state updates
  • image pipeline requests
  • accessibility updates

Those operations are fine when an item first appears. They are wasteful when only the unread badge changes.

Avoid recomputing display models on every scroll

In Telegram clients, list rows often use derived presentation models rather than raw protocol data. That is a good design, but only if those models are cached intelligently.

Common mistake

Rebuilding the full display object inside onBindViewHolder, cellForRowAt, or equivalent code paths.

That can include:

  • formatting timestamps
  • resolving user/chat names
  • computing message preview text
  • generating mention or reply indicators
  • measuring formatted spans

If you do this repeatedly while scrolling, you shift work from background preparation into the hottest code path in the UI.

Better approach

Precompute presentation data when the underlying model changes, not when the row becomes visible. Cache values such as:

  • formatted time strings
  • preview text snippets
  • mention badges
  • archived/pinned labels
  • avatar initials for fallback rendering

If the same derived value is used in multiple places, compute it once and reuse it.

Keep image work off the critical path

Avatars and media thumbnails are essential to Telegram’s UI, but they are also a frequent source of perceived slowness.

Best practices for image handling

  • Use placeholder drawables that are cheap to render.
  • Decode and transform images off the main thread.
  • Cache resized avatar variants instead of scaling large sources repeatedly.
  • Avoid re-requesting the same image when the row rebinds.
  • Cancel stale requests when a row is recycled.

For open source clients, a subtle bug is allowing the same avatar request to be restarted during every bind because the cache key is unstable or the view loses track of its current target. That creates unnecessary network, decode, and GC pressure.

What to check

  • Is the avatar URL or file ID normalized into a stable cache key?
  • Are image transformations deterministic?
  • Are recycled rows protected from late callbacks?
  • Is the list requesting too much resolution for a small UI element?

A 48px avatar should not trigger a 512px decode.

Reduce layout churn in complex rows

Telegram rows often include several layered pieces of content: title, preview, time, unread badge, mute icon, pin icon, and sometimes a status indicator. Layout code for these rows can become expensive if every state change triggers a full measure-pass cascade.

Best practice

Keep the layout structure stable and toggle visibility or lightweight state rather than swapping entire subtrees.

For example:

  • hide/show badges instead of replacing the badge view
  • update text content without reconstructing the container
  • reuse a single text layout object when only color or style changes
  • avoid deep nested hierarchies when a flatter structure works

Why it helps

A stable layout reduces:

  • re-measurement
  • overdraw
  • invalidation scope
  • garbage creation from temporary view objects

If your client uses custom text rendering, pay close attention to text layout caching. String spans and measurement objects are often surprisingly expensive to recreate.

Defer non-visible work

Many Telegram screens populate data before it is visible. That’s useful for responsiveness, but only if the non-visible work is deferred properly.

Good candidates for deferral

  • rich preview generation
  • avatar decoding for offscreen rows
  • conversation state decoration
  • typing indicator updates for non-visible chats
  • background sorting or filtering of large folders

Practical rule

If the user cannot see it yet, do not let it block the frame they are trying to interact with.

Instead:

  • prioritize visible rows first
  • schedule background preparation in chunks
  • batch updates when possible
  • apply incremental updates rather than repeated global refreshes

This is especially important when switching folders or restoring state after app resume, where many changes arrive at once.

Batch list mutations instead of firing many tiny updates

Telegram data can be noisy. Multiple message, status, and badge updates may arrive close together. If each one triggers a separate adapter notification or UI diff pass, the list becomes needlessly busy.

Better pattern

Accumulate changes for a short window and apply them as one update batch when safe.

Benefits:

  • fewer layout passes
  • lower adapter overhead
  • less UI flicker
  • better scroll continuity

This does not mean delaying important feedback. It means coalescing updates that land in the same frame or event loop tick.

Measure before and after with realistic data

Optimizing chat lists without real traces is risky. Telegram workloads vary wildly between a quiet private chat, a huge channel, and a multi-folder account with dozens of active conversations.

Use realistic test cases

  • large folders with mixed pinned and unpinned chats
  • chats with frequent unread count changes
  • channels with media-rich previews
  • rapid scrolling through long lists
  • app resume with many queued updates

Track these signals

  • frame time during scroll
  • bind time per row
  • number of layout passes per update
  • image decode frequency
  • allocation churn during refresh

If your optimization does not improve one of those metrics, it is probably not worth the complexity.

A practical implementation checklist

When improving Telegram open source client performance in chat lists, start with this checklist:

  1. Profile the hot path during scrolling and refresh.
  2. Separate model updates from view binding.
  3. Cache derived presentation values like text, time, and badges.
  4. Use partial updates for small state changes.
  5. Keep avatar/image loading stable and cancellable.
  6. Avoid full layout rebuilds for minor visual changes.
  7. Batch noisy updates into one refresh.
  8. Test with large real-world folders and channels.

What good looks like

After applying these changes, you should see:

  • smoother scrolling under load
  • lower main-thread time during message bursts
  • fewer row rebinds for minor updates
  • reduced allocation and GC pressure
  • faster perceived responsiveness when opening busy chat folders

The biggest win is often not a single dramatic algorithmic change. It is the accumulation of smaller fixes that stop the UI thread from repeatedly doing the same work.

Final takeaway

For Telegram open source developers, performance tuning should start where users feel it most: the chat list. The most effective improvement is usually not adding more caching everywhere, but making rendering paths more selective, more stable, and more aware of what actually changed.

If you reduce unnecessary work in list binding, precompute derived values, keep image loading off the main thread, and batch noisy updates, you can make a noticeable difference in responsiveness without redesigning the UI.

That is the kind of optimization that matters in a client used at Telegram scale: small savings, multiplied across thousands of rows and constant updates, turn into a smoother app.

← All posts