Engineering · Launch week

Making Brink feel lighter while the app grew three times larger

Between September 2 and launch week, I stopped treating performance as a list of slow functions and started treating it as a property of the architecture.

≈72%

My rough estimate of the avoidable runtime work removed since September 2. This is a weighted engineering estimate—not a claim that every screen or action is 72% faster. The device measurements below are the numbers I trust most.

On September 2, Brink worked. It also carried a lot of invisible weight. A few SwiftUI views had become small applications of their own. Preferences could redraw the entire app. Artwork colours were repeatedly downloaded, decoded and analysed. Some beautiful effects were asking the main thread to do expensive work on every frame.

The strange part is that none of this looked obviously catastrophic in isolation. A state assignment here, a shadow there, one more observer on a root view. But performance debt compounds. By launch week, I had to stop polishing symptoms and trace the whole system.

The app grew. The views got smaller.

Brink’s feature set expanded rapidly, but the shape of the code changed in the opposite direction. Large screens were split into focused components, lifecycle work moved out of view bodies, and state acquired clearer ownership.

SwiftUI view files169 → 241
Total view lines123,399 → 114,120
Largest single view7,068 → 2,314
Views above 3,000 lines13 → 0

More files are not automatically better. Here, they represent boundaries: a player control no longer needs to live beside queue persistence; episode rendering no longer shares a single enormous compilation and invalidation surface with the whole podcast page.

The largest view became 67% smaller, even as Brink gained substantially more capability.

The real enemy was invalidation

SwiftUI is exceptionally fast when the dependency graph is honest. It becomes expensive when broad views observe values they do not actually render.

The worst example was preference storage. Five high-level views held @AppStorage. A background sync writing hundreds of values caused those roots—and every tab beneath them—to re-evaluate. In one 20-second session, 468 preference writes produced 2,449 body evaluations: about 25 whole-app redraw passes per second while the interface appeared idle.

I replaced those broad dependencies with small watcher leaves. The leaves own persisted preferences and publish deduplicated values upward into local state. Writes go through paired setters so the screen updates immediately without turning every unrelated defaults change into a full-app event.

Root
74 → 0
Content
81 → 1
Feed
184 → 1
Detail
114 → 6

Remaining podcast-detail evaluations corresponded to real pagination events.

Stop drawing what nobody can see

Several visual effects were surprisingly costly—not because they were inherently bad, but because they were applied at the wrong scale.

A particularly expensive page gradient lived in the same display list as scrolling rows. Mounting a row could rerun a full-screen CPU shader. Moving that gradient into its own compositor-managed layer removed 1,802ms of main-thread shading from a 21-second Home scroll trace.

Make expensive work happen once

Brink derives colours from podcast and news artwork to create contextual backgrounds. Previously, those colours lived mainly in bounded memory caches. Relaunch the app—or scroll far enough to evict an entry—and Brink could download, decode and histogram the same artwork again.

A small persistent, URL-keyed colour index now sits beneath the memory caches. Cards, the mini-player and the full player can share known colours across sessions. Warm artwork becomes a dictionary lookup instead of an image-processing job, and gradients can be correct on their first frame.

The same principle fixed playlist menus. Every row used to decode every playlist’s episode payload to answer one membership question. Memoizing normalized membership keys removed 220ms of main-thread work from a recorded Home-feed hitch.

Launch should end when the app is ready

The splash once enforced a three-second minimum and then waited for the data container. Even a fast launch could not feel fast. After moving non-critical work behind a launch scheduler and removing a redundant accumulating delay, the visual floor fell first to 1.2 seconds and finally to 0.8 seconds.

That is a 73% shorter artificial floor. It still gives the Brink waveform a moment to register, but it no longer makes a ready app pretend to be busy.

The device traces that mattered

Code shape is evidence, not a benchmark. For the podcast-detail pass, I compared Release traces on an iPhone 17 Pro running iOS 27 and sliced the windows on the app’s own frames.

Total app main CPU13,031ms → 7,675ms
Hitches of at least 100ms28 → 5
Worst hitch540ms → 233ms
Idle work, page open14ms → 7ms

The interaction windows were not identical in duration, so the CPU totals should be read alongside the hitch counts—not as a universal percentage. One scroll window also regressed before a later synopsis-cache fix. Writing that down mattered: honest traces are more useful than flattering ones.

Performance also means not freezing

Some of the most important fixes do not appear in a smoothness chart. Opening the player after an idle relaunch could deadlock when a notification was posted while a store lock was held. The main thread and sync queue waited on each other until the watchdog killed the app. Posting after unlocking removed the cycle.

The termination path had a related problem: it synchronously waited on an unbounded iCloud flush during UIKit’s short shutdown budget. The app now attempts a bounded flush and leaves durable pending markers for the next launch. Correctness remains; the main thread no longer waits indefinitely for a daemon.

So, is Brink 72% faster?

Not in the simplistic sense. There is no single unit called “app speed,” and I do not have a perfectly matched September 2 trace for every journey.

The 70–75% range is my weighted estimate of avoidable runtime work removed across launch, scrolling, idle updates, artwork processing and the player. It combines code-path analysis with several focused traces. The defensible individual claims are narrower: a 73% shorter splash floor, sharply lower redraw counts, 82% fewer large hitches in the podcast-detail comparison, and the specific CPU reductions above.

The practical result: Brink now does far less work that never reaches a pixel or a speaker. That is the optimization I care about most.

What I learned

Brink is a much larger app than it was a few months ago. It is also calmer. The code has more boundaries, the main thread has fewer surprises, and the interface spends more of its time responding to the person using it.

GowthamBuilding Brink independently in Bengaluru.
Download Brink on the App Store.