Skip to content
 
 

Latest commit

 

History

6,512 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OpenBubbles — Performance Fork

A fork of OpenBubbles focused on one thing: making the Android app feel like a native messaging app.

Upstream OpenBubbles is an impressive piece of engineering — it reimplements Apple's iMessage protocol stack in Rust and brings it to Android and PC. This fork doesn't touch any of that. It targets the Flutter UI layer, where a set of accumulated performance bugs made scrolling, typing, and opening chats stutter on hardware that should never break a sweat.

66 files changed, ~1,160 lines added, ~530 removed — all of it in the UI and state-management layer. No protocol changes, no feature changes, no behavior changes.

Measured result

On a Pixel 8 Pro, release build, scrolling and typing in a real chat:

Metric Value
Janky frames 0.56% – 0.68%
Median frame time 5 ms
90th percentile 5 ms
95th percentile 6 ms
99th percentile 13 ms

Measured with adb shell dumpsys gfxinfo across ~700 rendered frames. For context, a 120 Hz display gives you an 8.3 ms budget per frame.

On comparisons: a clean baseline capture from the stock build wasn't obtained, so this README makes no before/after ratio claim. The numbers above are absolute measurements of this fork. The fixes below are each verifiable by reading the diff.

What was actually wrong

The problems fall into three groups. The common thread is that most of them got worse the longer the app stayed open, which is why the app felt fine at first and sluggish later.

Leaks that compounded during a session

  • Global event listeners were never cancelled. Every message bubble, avatar, delivered-receipt, and conversation tile subscribed to a global broadcast event stream on creation and never unsubscribed. Every message you scrolled past left behind roughly four permanent listeners that ran on every subsequent app event, forever. Fixed by adding auto-cancelling subscription/worker tracking to the shared state base classes.
  • Every chat tile instantiated a full conversation controller. Rendering a row in the conversation list called a get-or-create helper just to ask "is someone typing here?" — permanently allocating focus nodes, text controllers, a scroll controller, and a keyboard-visibility subscription per chat scrolled into view. Background code paths (typing indicators, profile updates, status events) did the same for chats never opened. Replaced with a non-creating lookup plus a lightweight global typing registry.
  • Every message you sent leaked a live database watcher. A controller re-registration bug meant the old registration was never released, so each sent message left an active ObjectBox query watcher running for the rest of the session — and every watcher wakes on every write to the message box.
  • Roughly 25 undisposed animation controllers, tab/page controllers, text controllers, focus nodes, and timers across the app; video and audio player listeners re-attached to cached players on every scroll-past; unbounded caches (decoded image bytes now capped at 128 MB with LRU eviction, bubble-size cache bounded).

Work done per frame that shouldn't have been

  • The chat header ran a σ=30 Gaussian blur plus a 5×4 color-matrix filter on every frame the content beneath it moved — i.e. every scroll frame. A BackdropFilter forces a saveLayer and a readback of the region behind it. Now applied only when a transcript poster background makes the translucency actually visible, matching the pattern the text field already used.
  • The delivered/read receipt was O(N²). Each message's receipt filtered and sorted the entire loaded message list inside its own build, and because it did so within a reactive scope, every receipt subscribed to every other message's delivery fields. One incoming read receipt invalidated all of them. Now O(1) against a version-cached per-list computation.
  • The whole conversation view was one reactive block. An async background-poster load, a contact suggestion, or a settings tick rebuilt the entire subtree — including the scrolling message list, re-running the item builder for every visible message. The heavy body is now cached and reused by identity, with narrow reactive scopes around the parts that genuinely change.
  • Per-message implicit animation widgets (each carrying its own ticker) removed from rows that display nothing; a clipper that under-reported when it needed to re-clip; a synchronous database write executing from inside a render pass.

Per-keystroke work

  • Emoji autocomplete performed roughly 110,000 string comparisons per keystroke, scanning ~3,500 emoji names with prefix, substring, and linear keyword-list searches. Replaced with a reverse index built once, a 150 ms debounce, and a minimum-length guard.
  • The text controller logged the entire contents of the text box on every character typed, plus a serialized annotation dump, through the production file logger. Debug leftovers shipped to users.
  • The send-button area rebuilt both branches of a cross-fade on every keystroke; observables were reassigned even when unchanged; a ~10 KB emoji regex and a URL regex were recompiled per message per build; date formatters were reconstructed several times per message.

Install

Grab the APK from Releases, or build it yourself (below).

The build uses the alpha flavor, whose application ID is com.bluebubbles.messaging.alpha — distinct from the official com.openbubbles.messaging. It installs alongside the official app rather than replacing it, so you can run both and switch back at any time.

To migrate your data: create a backup in the official app under Settings → Backup & Restore, then restore it in this build after completing setup. Run one at a time — two clients competing over the same iMessage registration will fight.

This is an unofficial build signed with a personal key, so it will not receive Play Store updates and cannot be installed over the official app.

Building

Requires Flutter 3.24.0, the Android SDK, a Rust toolchain with the aarch64-linux-android target, and protoc.

git clone https://github.com/Soh0mD/openbubbles-app.git
cd openbubbles-app
git config url."https://github.com/".insteadOf git@github.com:   # submodules use SSH URLs
git submodule update --init --recursive
flutter pub get
flutter build apk --flavor alpha --release --split-per-abi

The --split-per-abi flag matters: a universal APK is ~392 MB because it bundles native libraries for every architecture, while the arm64 build most phones need is ~157 MB.

Credit

All of the hard work — the iMessage protocol implementation, the Rust push stack, the app itself — belongs to the OpenBubbles project and its maintainers. This fork is a set of targeted performance patches on top of their work, nothing more.

The fixes here are intended to be upstreamable, and the commit history is organized into three reviewable passes with each change explained.

Status

Personal fork, actively used daily. Issues and pull requests are welcome, but bugs in the app's core messaging functionality should go upstream — this fork changes only rendering and state-management code.

About

Performance fork of OpenBubbles: fixes listener leaks, O(n^2) render paths, and per-keystroke work in the Android app. ~0.6% janky frames on a Pixel 8 Pro.

Topics

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages