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

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:

In other words:

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:

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:

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:

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:

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!