Troubleshooting Table View Performance
Htin Linn
Htin Linn

Troubleshooting Table View Performance

Htin Linn, Senior iOS Engineer

The magic of touch screen user interfaces comes from the illusion of direct manipulation. Unlike traditional PCs where pressing a key or clicking a button produces results elsewhere, there is no such disconnect between input and output with touchscreen devices. You are able to touch and directly manipulate what you see on the screen. This illusion is what makes touchscreen user interfaces so intuitive and satisfying to use.

Slow or choppy-scrolling table views break this illusion and diminish the user experience. Since most non-game mobile apps employ a table view or collection view of some sort to display content, it is important to keep the scrolling performance optimal. Fast scrolling performance is fairly easy to attain in table views if cell height and layout are static. Apple has done most of the legwork by recycling cells whenever possible and ensuring that new cells are only created when it is absolutely necessary. However, in many of our apps at Prolific, we have table views with complex cells that change and resize dynamically based on the content. Layout calculation can be particularly taxing for the CPU at scroll time when it is already tasked with animating and rendering duties. Good scrolling performance is, nonetheless, achievable with a combination of caching and refraining from executing nonessential tasks at scroll time.

The most common and straightforward way to assess scrolling performance is to measure the frames per second (FPS) of your app and observe the amount of time elapsed between frame updates. Ideally, this number would be locked at a steady 60 FPS, which is the widely-accepted threshold of perception for the human eye. This means that your app has about 16.6 milliseconds to perform its tasks before the frame rate starts to drop and the scrolling becomes choppy.[1] To keep the performance optimal in such a small window, the delegate and datasource methods need to be as efficient as possible.

Keep constraint resolutions under control.

One of the most time-consuming tasks that you could perform in a delegate method is Auto Layout constraint resolution. As wonderful as Auto Layout is for building resolution-independent user interfaces, it could be rather slow for dynamic usage, especially if you have more than a couple of nested subviews and constraints. One of the prevalent approaches to building self-sizing table view cells is to create a static cell — that never gets rendered on screen — in the tableView:heightForRowAtIndexPath: delegate method and perform the height calculation using this cell. This approach turns out to not be performant enough for cells with complex subview hierarchies. This method can get called multiple times before even a single cell is shown on screen. Depending on the number of times it gets called, the system might have to resolve tens — if not hundreds — of Auto Layout constraints to display your cell. You could dramatically improve the efficiency of this operation by caching and reusing the cell heights. Once the height for a given table view cell is determined, store it in a collection along with your model; when the system asks for the height of this cell again, you can return the value instantly instead of doing the expansive math operation. If you are not displaying too many cells, you can frontload this operation and pre-calculate the heights for all the cells before you even show the table view. [2]

Reduce. Reuse. Recycle.

By default, UITableView keeps a queue of all the cells that are in memory and only creates new ones when necessary. The rationale behind this is to minimize the amount of work necessary to show a cell on screen. To get zippy scrolling performance out of your table views, you want to follow suit and do as little as possible in your cellForRowAtIndexPath: method. If you find yourself adding or removing subviews constantly in this method, consider registering these configurations under different reuse identifiers. By doing this, the system will be able to store these cells in separate queues and reuse them instead of creating a new variant every time you need the same cell plus or minus one label. You might be spending more time up front by creating wholly separate cells, but you will be saving a lot of CPU cycles over time by not constantly reshuffling views.

Will It Blend?

Translucency and transparency can add a lot to an app’s user interface. That said, they should be used sparingly. The iPhone’s graphics processing unit (GPU), especially on older-generation devices, struggles when it comes to blending multiple layers of transparent views. Add a live blurring effect to the equation and performance can slow down to a crawl. One way to find out if your app is doing well in this regard is to profile it using the Core Animation option in Instruments. [3]  Tick the “Color Blended Layers” checkbox in debug options, and you will be able to inspect a color-coded version of your app. Green means good and red means bad. The GPU can render green regions of your views without much of a fuss. In contrast, the red regions can be an order of magnitude slower to render since the GPU needs to make multiple rendering and blending passes to draw even a single pixel on screen. You can give the GPU a hand and get much faster performance out of it by making as much of your view as opaque as possible. For instance, if you have a white background with a black overlay on top, you can probably get away with using just an opaque gray color.

Empathy for the Machine.

All of the techniques discussed up to this point can be distilled down to one fundamental principle: reduce the amount of work the system has to perform at scroll time. In addition to running apps in the foreground, iOS could be executing a couple of other processes and tasks in the background such as music playback, fetching new mails, etc. As a good platform citizen, your app should ask for only as many resources as it needs. Overloading the system with work will only slow things down, especially when you are scrolling and interpolating views for animation. By minimizing extraneous activities at scroll time, you will not only be making the system happier, you will also be making your app perform better.

  • [1] In practice, your app has even less processing time before dropping a frame.
  • [2] If your app is iOS 9 only, you might want to take a look at `UIStackView`. Apple claims that it is much more performant than vanilla container views in terms of constraint resolution.
  • [3] Profiling via Instruments only works on actual iOS devices, not on simulators.