• Get Started For Free →
Sign In
Get Started →
31st August 2022 · Ryan Ackermann

Making our production SwiftUI app 100x faster

Share Via

SwiftUI has come a long way since its release in 2019. The Clay iOS project has grown up alongside SwiftUI, for the better and the worse. Each year at WWDC, Apple has shown that this new technology is the way forward on its platforms. In this new era of declarative UI development, many common practices need to change.

In this article we will discuss what to do when a SwiftUI app starts to slow down. The first question is always where to start. There are many widely known performance considerations when building against UIKit.

  • Resize images to fit the frame of UIImageView
  • Cache expensive objects, such as DateFormatter
  • Optimize table views with reuse identifiers

There are many advantages of using a declarative framework. Being able to represent UI as a product of state can prevent bugs and stability issues. However, there are still three key performance principals to be aware of in SwiftUI.

  • Don't block the main thread
  • Avoid excessive layout changes
  • Store results of complex calculations

A final pain point is understanding how view identity affects the layout system. Changes to the data that View represents cause recalculations that are sometimes undesirable. Following a set of best practices, like these, should keep a SwiftUI app running smoothly.

The Need For Speed In Our Feed

Recently I went through the process of improving the performance of an existing view in Clay. My task was to swap two top level views, InboxView and FeedView. After making the change I noticed a substantial delay after a cold start of the app.

My first thought was that loading from the cache was taking too long. After light debugging with a few print statements it was clear this wasn't the problem. This issue was not as apparent before when FeedView was in a different tab bar position.

My next idea was to try removing some of the cells with complex layouts. This was a decent idea, but was hard to prove which combinations performed the best. Luckily before I continued down this road much longer, I had a light bulb moment.

I remembered the trusty tool, Instruments. This was my first time using the SwiftUI template. It tracks a few very helpful pieces of information. The .body tool helps point out which View types are slow. There is a tool to track property updates that cause a View to redraw its contents. Lastly, there is a tool to identify slow frames.

Wow, the longest average duration for a body computation is 2.17 milliseconds! From this graph it's clear that something is taking a while in LoadableImageView. This View is in charge of loading a remote image and displaying it.

Is it possible that the loaded images are too large? Or could it be that rendering placeholders from a contact name is taking too long. To see how big of an impact the images had on the layout I swapped the internals for an EmptyView.

Now that is more like it! The view loads the cached data straightaway. Time to Slack the design team to approve the removal of all images? 😅

The Solution

Continuing the trial by fire, I removed the image loading component in LoadableImageView. This reduced the view to only show the placeholder view. With this change the view loaded just as quick. That was both good and bad news. Bad because I was sure that the placeholder view was the culprit.

Here is what Clay uses to display remote images:

var body: some View {
    ZStack {
        
        if let hashImage = blurHashedImage {
            Image(uiImage: hashImage)
                .resizable()
                .renderingMode(.original)
        }
        else if let name = name, let length = sideLength {
            PlaceholderView(name: name, size: length)
        }

        if let image = imageFetcher.image {
            Rectangle()
                .fill(themeManager.themedBackground)

            Image(uiImage: image)
                .resizable()
                .renderingMode(.original)
                .transition(transition)
        }
    }
}

Sometimes the most head scratching bugs are solved with a simple fix. I finally realized that an expensive calculation was being made. A while ago we added the blurhash library. It's a neat algorithm that turns an image into a short hash. The drawback is that the calculation is expensive. It is still a lot faster to calculate the blurhash compared to loading the image from the network.

The blurhash library is easy to add into an iOS app. Check out BlurHashDecode.swift on GitHub for more information.

The hashes were being cached in-memory. However, this wasn't good enough to handle a view calculating many hashes at once. To solve this, I moved the calculation of the hash to a separate thread.

This is a huge improvement that moved LoadableImageView to the bottom of the list! The average duration went from 2.17 ms down to 24.56 µs. Which equates to a 98.86% increase in average performance for loading images. This change will also have a positive impact throughout the rest of the app.

The final improvement for this task is to use a LazyVStack for the FeedView. I actually discovered that this was missing early on in this process. It alone vastly improved the performance of the feed, but it felt like a band-aid. My perseverance paid off since the hash fix improves almost every other view in the app.

It's always a great feeling to optimize a feature. Especially if tradeoffs were made to meet a constraint or deadline. The more tools we have in our belt the easier it is to come up with a performant solution the first time.

One of the key strengths of SwiftUI is its simplicity. It used take numerous files, complicated delegates, and UI files to make a table view. Now you only need List, ForEach, and Text to achieve a basic table view of data.

With great power comes great responsibility.

Since it is this simple to build interfaces, it can be easy to overlook a problematic issue. As the iOS community continues to adopt SwiftUI more best practices for efficiency will emerge. Overall this technology is is a huge step forward for the Apple development community.

Takeaways

As SwiftUI continues to mature it will be easier to write performant apps. I hope this success story helps prevent similar problems for you. To summarize what helped solve this task:

  • Use Instruments to record from a physical device
  • Use modern SwiftUI improvements like LazyVStack
  • Be considerate of expensive calculations slowing down the .body

To learn more about profiling SwiftUI's layout system checkout these blog posts:

If you have any questions you can find me on Twitter @naturaln0va. Thanks for reading!

We're hiring!

Share Via