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.
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.
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.
- Full-width interactive glass cards re-blurred their entire bounds during scrolling. Interactive material is now reserved for small controls such as pills and circles.
- Transparent or zero-radius shadows still created render nodes and offscreen buffers. If a shadow is invisible in dark mode, it now leaves the tree entirely.
- Interactive scroll transitions re-evaluated every visible news card every frame. The layout now provides emphasis without continuous scale and opacity work.
- A hidden hero animation continued updating a blur, shadow and mask stack behind an opacity of zero. Its timeline now pauses when the artwork is not visible.
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.
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
- Measure invalidation frequency, not only slow functions. A cheap body evaluated 2,000 times is not cheap.
- Cache the final answer. Reusing downloaded bytes is not enough if every consumer repeats the decode and analysis.
- Visual effects need a budget proportional to their area. A live effect on a button is different from the same effect on a full-width card.
- State ownership is performance architecture. Moving task handles out of
@Statecan matter more than micro-optimizing a formatter. - Record regressions too. A trace that challenges the story is usually the one that points to the next real fix.
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.
Download Brink on the App Store.
Brink