How to Fix UI Hangs and Freezes Using Instruments and the Time Profiler

Frozen UI and beach-balling screens come from blocked main-thread work. Here is how to use Instruments to find the cause of hangs and fix them for good.

What a Hang Actually Is

A hang is a moment when your app's interface stops responding to touches. The screen feels frozen, animations stall, and the app appears stuck.

The root cause is almost always the main thread being blocked. The main thread handles UI updates and user input, so if it is busy doing something heavy, nothing else can happen.

Unlike a crash, a hang often resolves itself once the heavy work finishes. But to the user, a long pause feels just as broken.

Instruments is a good tool to find these. By examining what the main thread was doing during the freeze, you can pinpoint the blocking work.

Cause: Heavy Work on the Main Thread

The classic cause is performing expensive work synchronously on the main thread. Examples include large data parsing, synchronous network or disk access, image decoding, and heavy computation.

Any of these can occupy the main thread for long enough to freeze the UI. The longer the operation, the longer the hang.

Synchronous I/O is especially dangerous because its duration is unpredictable. A slow disk or network turns a quick call into a multi-second freeze.

The pattern to internalize is simple. The main thread should be reserved for UI work, and everything heavy belongs somewhere else.

Step 1: Reproduce and Record

First, find a reliable way to trigger the freeze. A reproducible hang is far easier to diagnose than an intermittent one.

In Xcode choose Product, then Profile, and select the Time Profiler template. Profile on a real device for accurate timing.

Press record, then perform the action that causes the freeze. Capture the entire frozen interval and a little before and after it.

Stop recording once the freeze ends. You now have a trace covering exactly the moment the UI was stuck.

Step 2: Isolate the Main Thread

Open the Call Tree and filter to the main thread. The freeze happened there, so that is where your attention belongs.

Invert the call tree and hide system libraries to surface your own heavy functions. The leaf functions consuming the most main-thread time during the hang window are your suspects.

Narrow the timeline selection to just the frozen interval if you can. Restricting the analysis to that window removes noise from before and after.

The function that dominates main-thread time during the freeze is almost certainly the cause. That is the code you need to move or speed up.

Step 3: Move Work Off the Main Thread

A common fix is to perform the heavy work on a background queue and then hop back to the main thread only to update the UI. This keeps the interface responsive while the work proceeds.

For data loading, do the fetch and parse in the background and deliver the finished result to the main thread for display. For images, decode off the main thread before assigning to a view.

Modern Swift concurrency makes this pattern natural, letting you run heavy tasks away from the main actor and return results safely. The principle is the same regardless of the concurrency mechanism you choose.

Be careful to touch UI only on the main thread. Background work prepares data, and the main thread presents it.

Step 4: Reduce the Work Itself

Sometimes the work cannot simply be moved, because it must produce a result the UI needs right away. In that case, make the work cheaper.

Consider caching results so you do not recompute them every time. A value computed once and reused removes repeated freezes.

Consider doing less, such as loading a smaller batch, paginating, or computing only what is visible. Lazy and incremental approaches keep each step short.

Algorithmic improvements pay off here too. A more efficient approach to the same task can turn a multi-second hang into an imperceptible pause.

Step 5: Verify the Fix

After your change, profile the same scenario again with the Time Profiler. Use the identical trigger so the comparison is fair.

The previously dominant function should no longer block the main thread for a meaningful duration. The UI should stay responsive during the action.

Interact with the app by hand as well. The most important confirmation is that the freeze is simply gone from the user's perspective.

Keep the before-and-after traces as evidence. They document that the hang is genuinely resolved rather than merely shifted.

Tell a Hang Apart From Slowness

Not every sluggish moment is a true hang, and the distinction guides how you investigate.

A hang is a hard stop: the main thread is blocked, so touches do nothing and the interface is completely unresponsive until the work finishes. This is the case where filtering Instruments to the main thread is so effective.

General slowness is different. The app still responds, but everything feels heavy, perhaps because of inefficient layout, too much work spread across frames, or contention between threads.

For a frozen screen, look for a single dominant function holding the main thread during the freeze window. For broad slowness, look instead at where time accumulates across the whole interaction, including work that is technically off the main thread but still starves it.

Naming which problem you have keeps you from applying a hang fix to a slowness issue, or vice versa. The Time Profiler supports both investigations, but you read its output with a different question in mind for each.

A quick test is to try interacting during the bad moment. If touches are completely ignored and then all register at once when it ends, you are looking at a hang and a blocked main thread. If the app still responds but sluggishly, you are looking at general inefficiency, and your fix is more about trimming work per frame than unblocking a single stuck call.

Prevention and Honest Scope

Adopt a habit of keeping the main thread light. Treat any synchronous I/O or heavy computation on the main thread as a bug waiting to happen.

Profile proactively during development, not just when users complain. Catching a hang early is far cheaper than fixing it after a bad review.

Build responsiveness into your design by loading lazily, caching aggressively, and pushing heavy work to the background by default. Good defaults prevent most hangs before they start.

Finally, note the scope of these techniques. Instruments and the Time Profiler analyze compiled native apps, so if your product is web output from an app builder rather than native Swift, you would first compile a real native build in Xcode before this main-thread analysis applies.

Frequently Asked Questions

What causes a UI hang in an iOS app?

A hang happens when the main thread is blocked by heavy work such as large parsing, synchronous I/O, or image decoding. Because the main thread handles UI and input, the interface freezes until that work finishes.

How do I find what is blocking the main thread?

Record the freeze with the Time Profiler, filter to the main thread, invert the call tree, and hide system libraries. The function consuming the most main-thread time during the freeze is your cause.

What is the usual fix for a hang?

Move the heavy work to a background queue and return to the main thread only to update the UI. If the work cannot move, make it cheaper through caching, pagination, or a more efficient algorithm.

Should I profile hangs on a device or simulator?

Use a real device for accurate timing. The simulator's CPU can mask or exaggerate how long the main thread is actually blocked on real hardware.