Codementor Events

Do we need to dequeue tableview cells?

Published Jun 24, 2019

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.

scene.jpeg

  1. A UIViewController with a UITableView that can be either used in a "standard" dequeued mode or instantianting fresh cells for each row.

  2. A SwiftUI based List with roughly the same layout as the UIViewController 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.

dequeued_trace.png

Non-dequeued needs to do that stuff AND dearchive a cell from disk.

non_dequeued_trace.png

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.

SwiftUI_trace.png

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.
Discover and read more posts from Warren Burton
get started