Question 1
Diagnosing scrolling jank
You're building a feed with complex cells, images, gradients, async content, dynamic text, and scrolling feels janky. Walk me through how you would diagnose and fix it.
Follow-ups
- What tools would you use?
- How do you tell layout, rendering, image decoding, and main-thread work apart?
Answer outline
Reproduce the problem first on the same device class, using a Release build and a realistic amount of data. Note whether the jank is constant on every frame or spikes when new rows appear or images finish loading.
Profile before you guess. Attach a real device, open Instruments, and start with Time Profiler filtered to the main thread and sorted by weight. Look for main-thread stacks doing image decode, JSON parsing, regex, or repeated layout passes.
Most jank comes from one of four causes, and the trace tells you which:
- 1.Layout (constraint thrash, long
NSISEnginestacks): simplify constraints, split heavy cells, cache row heights, and removelayoutIfNeeded()storms. - 2.Rendering (offscreen layers, blur, shadows): reduce layer effects, or rasterize deliberately where the content is static.
- 3.Images (large assets hitting the main thread): downsample to display size and decode off the main thread.
- 4.Main-thread work (JSON parsing, disk I/O): move it into async/await pipelines and touch UI only on the main actor.
SwiftUI has its own version of this problem. Unstable identity in ForEach, or a row body that reads more state than it needs, rebuilds rows on every change and looks like layout jank. In UIKit the equivalent is cell reuse that rebuilds subviews every time the cell is configured instead of resetting them.
Principles
- Test one hypothesis at a time, because two changes in one trace tell you nothing about either.
- A frame budget is about 16 ms at 60 Hz and 8 ms at 120 Hz, so decode and layout must fit.
- Wrong identity in SwiftUI or wrong reuse in UIKit causes extra rebuilds that drop frames just like slow layout does.
- Decode images off the main thread and at display size, since oversized bitmaps are the most common single cause of feed jank.
Load and decode in the background, then assign the finished image on the main actor:
Task {
let data = try await loader.data(for: url)
let image = await decodeOffMain(data) // CPU work off the main thread
await MainActor.run { imageView.image = image }
}
Follow-up angles
- Put
os_signpostintervals around cell configuration and image assignment, then match spikes in the trace to those named regions.



