Performance is about optimizing code you’ve already written, but what if you start designing a new piece of code? There are many decisions along the way that influence the outcome performance. If you’re better at making performance decisions from the start, you’ll reduce tech debt for the future.
In this lesson, I’ll guide you through how I like to think about the code that I write today. Let’s dive in!
A simple mental model
When deciding execution style, I like to imagine a spectrum:
Synchronous → Asynchronous → Parallel
- Start on the left.
- Only move right when you prove it’s needed.
This prevents premature optimization and keeps complexity low. Let’s dive deeper into this concept.
The three different phases of concurrency
I’d like to remind you about the three phases of adopting Swift Concurrency as described in the vision document from the Swift Team:
- Phase 1: No concurrency at all
- Phase 2: Suspend execution without parallism
- Phase 3: Advanced concurrency
In other words:
- Phase 1: A synchronous method
- Phase 2: An asynchronous method
- Phase 3: Multiple asynchronous methods running at the same time
These are also the phases I like to go through when writing my code. Some methods are obviously heavy and should run asynchronously to not block the main thread. In other cases, however, it might be fine to start with a synchronous variant.
Here’s a rule of thumb. If your work:
- Doesn’t touch UI
- Touches persistent storage
- Parses or transforms large data sets
- Communicates over the network
It’s a good candidate to start thinking beyond phase 1. However, don’t just jump into background execution just yet.
Consider always starting with a synchronous method
If you’re an experienced engineer, you might be better at predicting performance hiccups in your code. But even for those engineers: I believe it’s more often than not okay to run a method synchronously.
Today’s devices are high performant and code runs incredibly fast. Jumping to a background isolation domain sometimes slows down your code more than it brings performance.
This is why I’d like to recommend starting with a synchronous method and optimizing accordingly. You might be surprised to see that your code is actually high-performing without the need for a background execution.
Here’s an example: a function loads JSON from disk. This might feel like it belongs in the background. However, most configs are tiny, memory-mapped, and cached by the system. In practice, this call is often faster synchronously than switching to a background isolation domain and suspending back into the @MainActor.
Optimize along the way
By the time you’ve written your code and you’re testing your app, open up Instruments and let the numbers speak. Only terrible performance will be visible to the eye during regular debugging; instruments are often needed to pinpoint room for improvement.
While developing RocketSim, I often relaunch my app. Memory is clean, there’s nothing decreasing the performance of my app. Yet, I still get feedback from users every now and then about the performance of my app. Mac apps run continously for days in the background. This is not unique to macOS, but it happens more often due to the nature of Mac apps. I try to prevent these performance issues by optimizing along the way.
I do this by running my app with instruments for a longer period, simulating a real user. After using several features of the app, it became more interesting over time. Methods that performed great in a clean state suddenly suspend much longer due to the app being busier.
The first code you write does not have to be perfect, you can and should optimize along the way.
A moment on cognitive overhead
Every layer of concurrency you introduce adds:
- More suspension points
- More states to reason about
- More opportunities for cancellation bugs
Keeping a method synchronous early keeps your system simpler and easier to debug.
But I know this method will be heavy!
If so, that’s great! I don’t want to stop you from using @concurrent right away. I just want to make you aware that it’s not always what it seems.
I’ve been developing apps since 2009, and I’ve written code when we had to rasterize shadows for smooth scrolling behavior. It’s these memories that make me realize it’s easier than ever to become lazy on performance, but also to become blind for what’s possible.
I’ve trained myself to recognize potential heavy methods and optimize them from the start. It’s what we like to call Premature Optimization — Optimizing before we know that we need to.
So yes, trust your instincts and add @concurrent or use Task Groups when you know work will be heavy. However, train yourself to consciously make that decision and don’t just @concurrent all the things. Let Xcode Instruments guide you, especially for core logic in your app.
A quick checklist to confirm async/parallel is the right move:
- Will this block the main actor long enough to be visible?
- Will the work scale with user data (N items → N cost)?
- Does the work involve I/O?
- Does the work benefit from combining multiple independent operations?
- Is this logic called frequently?
If you check 2 or more boxes, async or parallel is usually justified. However, once again, use Instruments when in doubt!
UX Driven decisions
Finally, sometimes you want things to run in the background, nevertheless. You might have a beautiful loading interface, and you don’t want it to hiccup just because you’re running logic on the @MainActor. Great UI makes slow operations feel faster. Background execution can also improve battery life and optimize system resources.
Even if an operation completes in 80ms on the main thread, if it causes the progress animation to freeze for one frame, the user feels it as slow. Moving that same logic to a background actor keeps the UI buttery smooth—even if total time increases slightly.
Parallelism isn’t always a win
Parallel execution sounds attractive, but it comes at a cost:
- You increase memory pressure
- You increase CPU scheduling overhead
- You can saturate system resources
On devices where battery or thermal conditions matter (which is… all Apple devices), parallelism should be a conscious, measured choice.
I often ask myself:
“Does running this work in parallel create better time-to-first-result, or just more noise for the system?”
Many cases of parallelism are obvious (e.g. loading multiple images simultaneously). My point is mostly, once again, make concious decisions.
Summary
It’s all about knowing what you’re doing. Each app is unique, and the code is as well. It’s you who needs the skills to detect slow performance and know what to do next to improve. Be aware that background execution isn’t always faster or better.
And with that, it’s time for the assessment of this module!