Do we need to dequeue tableview cells?
An Investigation into how dequeuing tableview cells behaves in a modern iOS hardware environment.
Inspired by a conversation in the comments of this SO question about lagginess in SwiftUI
Lists I constructed a test harness to figure out what the effects of dequeing cells in a UITableView
are.
Requires
To run the project on simulator requires at least Xcode 11 Beta 2 or better. Catalina Beta 2 and some iOS 13 hardware might be useful too.
Introduction
Common wisdom is that UIKit collection views require their cells to be dequeued to avoid memory ramps. From a memory perspective that shouldnt be a problem any longer as the underlying hardware no longer needs such aggressive memory management. This project tests those assumptions.
Test Harness
You can download the project here
The app is a basic UITabViewController
with two view child controllers.
-
A
UIViewController
with aUITableView
that can be either used in a "standard" dequeued mode or instantianting fresh cells for each row. -
A
SwiftUI
basedList
with roughly the same layout as theUIViewController
based version.
The UIKit
cell was a custom XIB with two labels and a small image to ensure that this was not a trivial test of stock UITableViewCell
instances called ThingRow
The dataset was a simple array of 500 struct
items called Thing
The table was scrolled from top to bottom with breaks.
Results
Frame Rate
Measuring the time spent in func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
as a proxy for frame rate.
Using UIKit
, the dequeued UIViewController had the best frame rate performance.
Dequeued
Cell request rate | Hardware (iPad Air 2) | Simulator |
---|---|---|
Max Rate | 2695 cell/s | 4310 cell/s |
Min Rate | 568 cell/s | 314 cell/s |
Non-Dequeued
Cell request rate | Hardware (iPad Air 2) | Simulator |
---|---|---|
Max Rate | 428 cell/s | 795 cell/s |
Min Rate | 93 cell/s | 102 cell/s |
Using a dequeued cell gave the smoothest scrolling performance, subjectively however it was a close thing.
SwiftUI had the worst subjective performance with choppy scrolling but with no direct way to get the frame rate I resorted to using Instruments
to measure the time profile.
Memory Footprint
Memory footprint was about even for UIKit
with Dequeued mode being about 1.0 MB less than Non-Dequeued. The non-dequeued cells can be observed releasing using Console logging.
- Non-Dequeued = circa 25MB
- Dequeued = circa 24MB
- SwiftUI = circa 35MB
Why is SwiftUI so choppy
The stack traces for the UIKit version is fairly self explanatory.
Dequeued spends most time in the cell rendering phrase.
Non-dequeued needs to do that stuff AND dearchive a cell from disk.
SwiftUI is a black box but two things stood out:
-
Automatic text localisation seemed to be a heavy part of the render phase.
-
The entire system is doing a lot of work to render the list on screen as CALayers with super deep traces.
Conclusions
If frame rate (smoothest scroll performance) is important to you then you should use cell dequeueing.
However you don't need to implement cell dequeueing to keep your app from dying a memory death ( ARC is magic! ) and the frame rate was acceptable just not quite as smooth as the dequeued version.
If you have a use case where dequeuing doesn't make sense or might be overkill e.g:
- Many different cell prototypes causing a spaghetti of ReuseID's
- You just want to dynamically show a limited dataset in a table using a stock
UITableViewCell
.
it isn't going to be a deal breaker and you can refine your code from.
override func viewDidLoad() {
super.viewDidLoad()
/// registration
let nib = UINib(nibName: "ThingCell", bundle: nil)
tableView.register(nib, forCellReuseIdentifier: reuseID)
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
///conditional dequeuing
guard let cell = tableView.dequeueReusableCell(withIdentifier: reuseID, for: indexPath) as? ThingCell
else {
fatalError("dequeuing issue")
}
//cell config ...
return cell
}
to just
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = ThingCell.freshCell()
//cell config ...
return cell
}
SwiftUI
is a beta framework and will get better at doing rendering so performance is probably not worth worrying about right now.
At a guess the rendering for SwiftUI will be shifted to Metal and the GPU later on. Currently the render seems to sit on the CPU observing the CPU trace in Xcode.
Rest assured though it's probably not your SwiftUI code thats causing choppy scrolling.
Cons
As always you should test this against your apps actual requirements.
- You might be in a constrained environment where every MB counts.
- Power costs are might be more expensive to hit the disk for every cell. Storage caching magic probably assists here.