← Back to Rahul Tak

iOS Engineering · Performance

Practical iOS Performance Optimization: What I Check Before Reaching for Instruments

Performance problems are often easier to find than we make them. Before opening Instruments, I usually start with the product behavior, the main thread, the data being loaded, and the work we are asking the device to do.

By Rahul TakPublished: October 7, 2026Updated: October 7, 2026

After working on iOS applications for many years, one thing I have learned about performance is that the hardest part is not using Instruments. Instruments is an excellent tool, but it is rarely the first thing I need.

When someone says, "The app is slow," that sentence is too broad to be useful. Slow when launching? Slow while scrolling? Slow after login? Slow only on a poor network? Slow after the user has kept the application open for several hours?

Those are very different problems, and they usually have very different causes.

My first job is therefore not to optimize code. It is to turn a vague performance complaint into a specific, reproducible behavior.

1. Start With the User-Visible Problem

Performance work becomes much easier when you describe the problem from the user's point of view.

Instead of:

  • "The home screen has performance issues."
  • "The API layer is slow."
  • "SwiftUI is rendering too much."

I try to get to something more concrete:

  • "The home screen takes around three seconds before the user can interact with it."
  • "The product list starts stuttering when the user scrolls through the first 50 items."
  • "Opening the checkout screen becomes noticeably slower after navigating through several product pages."
  • "The first screen is responsive, but typing becomes delayed while a search request is running."

Once the problem is specific, I can decide what to investigate instead of randomly profiling the entire application.

2. Reproduce It Consistently

The next question is simple: can I reproduce the problem?

If one developer sees a two-second delay and another sees almost none, I do not immediately start changing code. I first compare the environment.

  • Device model
  • iOS version
  • Debug or Release configuration
  • Network conditions
  • Amount of existing local data
  • Logged-in versus logged-out state
  • Cold launch versus warm launch
  • Whether the issue appears after repeated navigation

This matters because performance can be highly dependent on state. A screen that is fast with ten records may behave very differently with ten thousand records.

3. Check Whether I Am Measuring a Debug Build

This is one of the easiest mistakes to make during performance investigations.

Debug builds are useful for development, but they are not always a realistic representation of production performance. Compiler optimizations, logging, assertions and other development-time behavior can change what you observe.

If a performance complaint is important enough to fix, I want to reproduce it in an environment that resembles the actual shipped application.

4. Look at the Main Thread First

On iOS, a surprisingly large number of "the app is slow" complaints eventually come down to too much work happening on the main thread.

The main thread has an important job: keeping the interface responsive. If we make it perform expensive work, the user sees the result immediately as a frozen or laggy interface.

Typical examples include:

  • Large JSON decoding
  • Heavy sorting or filtering
  • Image processing
  • Database queries
  • Large file operations
  • Expensive layout calculations
  • Repeated synchronous work triggered by UI updates

For example, code like this may look harmless when the data set is small:

Swift
DispatchQueue.main.async {
    let sortedProducts = products.sorted {
        $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending
    }

    self.products = sortedProducts
}

The problem is not that sorting is inherently bad. The problem is that the sorting work is being performed on the main thread.

With a large collection, this can become visible as delayed taps, dropped frames or a screen that feels unresponsive.

With modern Swift concurrency, I would generally move expensive non-UI work away from the main actor and only bring the resulting state back to the UI when needed.

Swift Concurrency
let sortedProducts = await Task.detached {
    products.sorted {
        $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending
    }
    return products
}.value

self.products = sortedProducts

The exact solution depends on the ownership and Sendable requirements of the real code. The important part is the reasoning: identify what work actually needs the main thread and what does not.

5. Look at the Network Before Blaming the UI

A screen can feel slow even when rendering is perfectly fine. Sometimes the application is simply waiting for data.

When a screen depends on several API calls, I ask:

  • How many requests are being made?
  • Which requests are actually required for first render?
  • Are any requests duplicated?
  • Are requests being made sequentially when they can run concurrently?
  • Are we downloading more data than the screen needs?
  • Are images being downloaded at their original resolution?
  • Are we repeatedly requesting data that could be cached?

Consider a home screen that needs profile data, categories, promotions and products. If the implementation waits for one request before starting the next, the total waiting time can become unnecessarily long.

Sequential approach
let profile = try await api.fetchProfile()
let categories = try await api.fetchCategories()
let products = try await api.fetchProducts()

If these requests are independent, concurrency may be a better approach:

Concurrent approach
async let profile = api.fetchProfile()
async let categories = api.fetchCategories()
async let products = api.fetchProducts()

let result = try await (profile, categories, products)

This is not a rule that every request should run concurrently. Dependencies still matter. But it is one of the first things I check when a screen spends too much time waiting on a collection of independent services.

6. Check What the API Is Returning

Backend payload size is easy to overlook because the problem may appear to be an iOS problem.

If a mobile screen needs five fields but the endpoint returns a large object containing dozens of fields, the application is doing unnecessary work.

Large payloads affect more than network transfer. They can also increase decoding time, memory usage and the amount of work needed to transform the response into UI models.

I therefore look at the actual response rather than assuming the client is the only place where optimization is needed.

7. Images Are One of My First Checks

Images are a common source of memory and scrolling problems, especially in commerce, social and content-heavy applications.

A device may display an image at a relatively small size while the application downloads a much larger original image.

The mistake is easy to make:

Conceptual example
Server image:
2400 × 2400 pixels

ImageView:
120 × 120 points

The displayed size does not automatically mean the application only has to deal with a small image. Decoding a large image can consume significant memory before it is finally displayed at the smaller size.

In a scrolling list, this becomes even more important because many images may be alive or being decoded around the same time.

I check image dimensions, compression, caching, downsampling and whether the backend can provide appropriately sized assets.

8. Check Lists and Scroll Performance

If a user reports that scrolling feels bad, I do not immediately assume the list component itself is the problem.

I look at what each cell is doing.

  • Is every cell doing expensive computation?
  • Are images being decoded while scrolling?
  • Are views being recreated unnecessarily?
  • Is there nested layout complexity?
  • Are we performing network calls from individual cells?
  • Are state changes causing too many view updates?

A list containing simple text can perform very differently from a list where every row contains images, animations, formatted text, network state and several nested containers.

9. SwiftUI: Watch What Causes View Updates

SwiftUI makes UI development much easier, but it also changes how we think about rendering performance.

One thing I pay attention to is state ownership.

If a high-level state change causes a large part of the view hierarchy to recompute, the resulting work can become noticeable.

SwiftUI
struct DashboardView: View {
    @State private var searchText = ""
    @State private var isShowingProfile = false

    var body: some View {
        VStack {
            SearchView(text: $searchText)
            DashboardContent()
            ProfileButton(isPresented: $isShowingProfile)
        }
    }
}

The code itself is not automatically a performance problem. What matters is how state flows through the real view hierarchy and what work is performed as state changes.

I prefer small, focused views with clear state ownership rather than trying to optimize every SwiftUI view prematurely.

10. UIKit Has Its Own Version of the Same Problem

The same principle applies to UIKit. A table or collection view can be perfectly capable of handling a large amount of content, but the work performed during cell configuration can still make scrolling expensive.

I look closely at:

  • Cell reuse
  • Auto Layout complexity
  • Image loading and decoding
  • Attributed string construction
  • Repeated formatting
  • Network activity triggered from cells
  • Expensive work inside layout callbacks

A useful rule is to keep cell configuration predictable and cheap. If a cell needs a surprising amount of business logic just to appear on screen, that is worth investigating.

11. Look for Repeated Work

Performance issues are not always caused by one expensive operation. Sometimes a relatively cheap operation is being performed hundreds or thousands of times.

This is one of the first patterns I look for.

For example, suppose a function calculates the same derived value every time a row is rendered. One calculation may cost almost nothing. Repeating it for every item during every update can become expensive.

Before optimizing the calculation itself, I ask a simpler question: "Why are we doing this calculation again?"

12. Be Careful With Logging

Logging is extremely useful during development, but excessive logging can make performance testing misleading.

I have seen applications where a frequently executed path contains enough logging to make the debug experience look much worse than the actual production behavior.

Before drawing conclusions from a performance test, I check whether debug logging, analytics logging or development diagnostics are influencing the result.

13. Memory: Look for Growth, Not Just a Large Number

Memory problems can be subtle. A large memory footprint does not automatically mean there is a leak.

I pay attention to behavior over time.

If I open a screen, leave it, open it again, and repeat the process several times, does memory return close to its previous level? If not, something deserves investigation.

Common areas I check include:

  • Retain cycles
  • Closures capturing objects strongly
  • Timers and observers
  • Notification subscriptions
  • Large image caches
  • Long-lived view models
  • Collections that continuously grow

If the memory graph keeps climbing as the same workflow is repeated, then Instruments and Xcode's memory tools become much more useful because I already have a specific behavior to investigate.

14. Check Database and Local Storage Work

Local persistence can also become a performance bottleneck.

Core Data, SQLite and other persistence layers are powerful, but fetching or transforming large amounts of data unnecessarily can still hurt responsiveness.

I check whether the application is:

  • Fetching more records than needed
  • Performing expensive filtering in memory
  • Running storage work on the main thread
  • Repeatedly fetching the same data
  • Loading large object graphs unnecessarily

The same principle applies here as with networking: understand the amount of work first, then optimize the implementation.

15. Check Navigation and Lifecycle Behavior

Some performance problems only appear after the user has moved through several screens.

When that happens, I look for objects that should have gone away but are still alive.

Coordinators, view models, subscriptions, notification observers, timers and asynchronous tasks are all worth checking.

A screen that feels progressively slower after repeated navigation is often telling you that something is accumulating.

16. Concurrency: More Threads Do Not Automatically Mean More Performance

Moving work off the main thread is useful, but indiscriminately creating background work is not an optimization strategy.

If several tasks compete for CPU, memory or disk access, adding more concurrency can sometimes make the application worse.

I first identify what is independent, what is dependent and what must happen on the main actor. Then I decide where concurrency actually helps.

Swift concurrency gives us good tools for expressing that relationship, but the architecture still has to make sense.

17. Check the First Launch Separately From a Warm Launch

Launch performance is another area where context matters.

A warm launch and a cold launch are not the same thing. Cached resources, initialized state and previously loaded data can make subsequent launches look much faster.

For a launch problem, I therefore test the scenario consistently and ask what work is actually required before the first screen is interactive.

A common mistake is putting too much initialization into application startup simply because it is convenient to do it there.

If something is not required immediately, I question whether it really belongs on the critical startup path.

18. Measure Before and After

Once I have a likely cause, I make the smallest reasonable change and measure again.

I do not want to end up with a large refactor where nobody can say whether the application actually became faster.

A simple performance loop is:

  1. Define the user-visible problem.
  2. Reproduce it consistently.
  3. Identify the likely expensive work.
  4. Change one meaningful thing.
  5. Measure the same scenario again.
  6. Keep the change only if it improves the right metric.

19. When I Finally Reach for Instruments

Instruments becomes much more powerful after I have narrowed the problem down.

I may use different instruments depending on the question:

  • Time Profiler when I need to understand where CPU time is going.
  • Allocations when I need to understand allocation behavior and memory usage.
  • Leaks when I suspect objects are being leaked.
  • Network-related profiling when network behavior needs deeper investigation.
  • SwiftUI-specific diagnostics when rendering and view updates are the suspected problem.

The important part is that I am not opening Instruments and hoping it tells me what is wrong. I already have a question.

"The product list drops frames when images appear. I want to know whether the CPU is spending time decoding images or whether something else is blocking the main thread."

That is a much better profiling question than simply saying, "The app is slow. Let's open Instruments."

20. A Practical Checklist I Use

When I receive a performance issue, this is roughly the order I work through:

iOS Performance Checklist
✓Can I reproduce the problem consistently?
✓What exactly is the user experiencing?
✓Am I testing the right build configuration?
✓Is expensive work happening on the main thread?
✓How many network requests are required?
✓Can independent requests run concurrently?
✓Are API responses larger than necessary?
✓Are images correctly sized, cached and decoded?
✓Is a list doing expensive work for every cell?
✓Are SwiftUI state changes causing unnecessary updates?
✓Are UIKit cells and layout work lightweight?
✓Is the same computation or request happening repeatedly?
✓Is memory growing as the same workflow is repeated?
✓Is local storage work happening efficiently?
✓Are objects, observers or tasks surviving longer than expected?
✓Is startup doing work that can happen later?
✓Can I measure a clear before-and-after result?
✓Do I now have a specific question that Instruments can answer?

Performance Is Usually a System Problem

One of the biggest lessons I have learned is that performance rarely belongs to a single layer of an application.

A slow screen can involve the API, JSON decoding, image downloads, persistence, business logic, state management, rendering and the main thread.

That is why I try not to start with assumptions such as "SwiftUI is slow," "the API is slow," or "Core Data is slow."

Those statements are usually too broad to help.

Instead, I look at the complete path from user action to the final pixels on the screen:

1
User Action
2
Network / Storage
3
Business Logic
4
State Update
5
UI Rendering

If any one of those stages is doing unnecessary work, the user can feel the difference.

Final Thoughts

Instruments is one of the best tools in the iOS ecosystem, and there is absolutely a time to use it. But performance optimization does not have to begin with a profiler.

In real-world projects, I usually start much closer to the behavior: reproduce the issue, understand what the user is waiting for, inspect the main thread, look at the data and network calls, check images, examine state updates and look for repeated work.

Often, that is enough to find the first meaningful improvement.

And when it is not enough, I reach for Instruments with a specific question in mind. That makes profiling faster, the results easier to interpret, and the eventual fix much easier to explain to the rest of the team.

Good performance work is not about making every line of code faster. It is about making the application do the right amount of work, at the right time, on the right execution context.


About the Author

I’m Rahul Tak, a Senior iOS Engineer with 10+ years of professional experience building iOS applications across eCommerce, government, finance and retail.

My experience includes Swift, SwiftUI, UIKit, Objective-C, MVVM, VIPER, Clean Architecture, Swift Concurrency and enterprise mobile application development. I also led iOS teams and worked on large-scale applications where architecture, maintainability and technical leadership are as important as writing the code itself.

Visit buildswiftly.in →