SwiftUI and AnyView: Performance benchmarks

AnyView - The dreaded two-headed monster that everyone should avoid...

Or should they? Anyone out there who can tell me--specifically--what impact AnyView has on performance and view rendering cycles?

Don't tell be to use @ViewBuilder. Don't tell me about doing func x() -> some View. Don't explain myViewContent:View. Don't parrot the vague "performance" warnings Apple gives in the SwiftUI documentation, or the offhand mentions made during the Demystify SwfitUI developer sessions.

Yes. I know it supposedly has an impact when diffing the view tree, but then again, when an update occurs the view tree is diff'ed anyway.

I'm looking for hard data on exactly what the performance penalty might be. Apple provided the tool to be used if/when needed. I'm trying to determine the consequences of doing so.

6 Likes

Aside from performance which I don’t see much difference in my own test. `AnyView’ cause problem with animation due to view diff is not possible with AmyView?

This app illustrates real slowdown cause by AnyView.

The app
import SwiftUI

class Model {
    static let shared = Model()
    let items1K = (0 ..< 1000).map { Item(id: $0) }
    let items10K = (0 ..< 10_000).map { Item(id: $0) }
    let items100K = (0 ..< 100_000).map { Item(id: $0) }
    let items1000K = (0 ..< 1000_000).map { Item(id: $0) }
}

struct Item: Identifiable {
    var id: Int
    var text: String { String(id) }
}

struct NormalItemView: View {
    let item: Item
    
    var body: some View {
        HStack {
            VStack {
                Text(item.text)
            }
        }
    }
}

struct AnyItemView: View {
    let item: Item
    
    var body: some View {
        AnyView(Text(item.text))
    }
}

struct NormalListView: View {
    var items: [Item]
    
    var body: some View {
        List(items) { item in
            NormalItemView(item: item)
        }.listStyle(.plain)
    }
}

struct AnyListView: View {
    var items: [Item]
    
    var body: some View {
        List(items) { item in
            AnyItemView(item: item)
        }.listStyle(.plain)
    }
}

struct ContentView: View {
    private var model = Model.shared
    
    var body: some View {
        NavigationView {
            VStack(spacing: 20) {
                NavigationLink { NormalListView(items: model.items1K) } label: { Text("NormalView 1K") }
                NavigationLink { NormalListView(items: model.items10K) } label: { Text("NormalView 10K") }
                NavigationLink { NormalListView(items: model.items100K) } label: { Text("NormalView 100K") }
                NavigationLink { NormalListView(items: model.items1000K) } label: { Text("NormalView 1000K") }
                
                NavigationLink { AnyListView(items: model.items1K) } label: { Text("AnyView 1K") }
                NavigationLink { AnyListView(items: model.items10K) } label: { Text("AnyView 10K") }
                NavigationLink { AnyListView(items: model.items100K) } label: { Text("AnyView 100K") }
                NavigationLink { AnyListView(items: model.items1000K) } label: { Text("AnyView 1000K") }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

@main struct LSTApp: App {
    var body: some Scene {
        WindowGroup { ContentView() }
    }
}

When showing a list with 1K items AnyView version is quick, with 10K items it's just a tad slower, but then it's significantly slower with 100K and 1000K items; while "normal" view version is equally fast with any number of items.

1 Like

What I find interesting is if I do this...

struct AnyListView: View {
    var items: [Item]
    var body: some View {
        AnyView(NormalListView(items: items))
    }
}

Wrapping the list view in AnyView has little to no issues with performance, while managing a list of 100,000 AnyViews is somewhat more problematic.

It's not just AnyView, this is also slow for large number of items:

        List(items) { item in
            if (item.id % 100) != 0 {
                NormalItemView(item: item)
            }
        }
1 Like

That one was brought up in the latest Demystify SwiftUI session. Basically if you do something like that List will need to build every item in the list in order to determine the true size of the list.

If you filtered items by that first, before passing it to list, then performance would return to normal.

2 Likes

Yes, for that please checkout Demystify SwiftUI performance - WWDC23 - Videos - Apple Developer till 16-17 minute approximately why it's slow

1 Like

I don't think it's the size specifically, e.g. this is fast for 1M items:

        List(items) { item in
            NormalItemView(item: item)
                .frame(height: 10 + CGFloat((item.id * 12345) % 100))
        }

and this is slow:

        List(items) { item in
            NormalItemView(item: item)
                .id(item.id)

It is diffed, but since AnyView is a type erasure, it eliminates the knowledge of the real view under the hood, so the SwiftUI is unable to perform a lot of operations on that, since the real type is unknown.

For basic rendering, especially not in the list/stack/grid, it should not cause any performance issues, but as mentioned, might break some other features such as animations.

Swift is static typed and a lots of thing depend on being specific types, especially when it comes to UI. Especially when you work with something like SwiftUI which is already an abstraction over different UI frameworks, so bringing another level of abstraction with AnyView would make no good.

1 Like

Thanks for the response. Can you expand on "a lot of operations"? Otherwise this is basically the same set of generalizations of it being "bad" that everyone else has read and repeats.

Again, Apple provided AnyView for situations where type-erasure might be necessary (e.g. some protocol that returns a view). What I need to know is just how "bad" the consequences of doing so might be.

1 Like

I'd say: measure. In situations like the above list of 100K items AnyView is really bad, and if it's just a few views then it is ok. And if you have an easy option of not using AnyView - don't use it "just in case", may save you time in the future.

Still no good answer to this?

Since it is Swift, not Apple, forum, I suppose there is a little chance that somebody would be able to explain internals of the black box SwiftUI framework in more details than there it is already.

If you'll specify the problem more concrete, there will be more chances that someone would help. Unless that, IMO just do not use AnyView whenever possible, use when it is inevitable, and take a look if everything works fine. It obviously won't crash your app just because of AnyView wrapper.

I fail to see why the above code example that demonstrates the observable slowdown of AnyView with a large number of items is not a good answer... I believe that's exactly the kind of issue the mentioned WWDC video was talking about. Granted, a list with a large number of items might not be something of a concern in your app, and if it so happens that you use AnyView sparingly without triggering SwiftUI slowdown you could be fine using it.

By making the AnyView version Equatable it is speedy again:

-struct Item: Identifiable {
+struct Item: Identifiable, Equatable {
 
-struct AnyItemView: View {
+struct AnyItemView: View, Equatable {

         List(items) { item in
             AnyItemView(item: item)
+                .equatable()
         }.listStyle(.plain)
1 Like

That is, if it is infeasible to make your view generic over some content view, and you have to rely on AnyView, for whatever reason — you may be able to regain performance if your model can be made equatable.

That is, even though SwiftUI cannot reason about the structure of your view, you can mitigate the issues by telling SwiftUI what causes the view to change.

2 Likes

That's interesting, it does speed things up and brings the up time complexity from O(number of all elements) back to O(number of visible elements). Why is this not a default behaviour? Or to put it differently what are the drawbacks of doing views equatable?

So does this...

struct AnyItemView: View {
    let item: Item
    init(item: Item) {
        self.item = item
        print("\(item.id) initialized")
    }
    var body: some View {
        let _ = Self._printChanges()
        VStack {
            AnyView(NormalItemView(item: item))
        }
    }
}

Wrap the AnyView in a VStack or ZStack and the list goes back to normal. Remove it, or change it to Group and it goes back to building everything,

1 Like

That's puzzling indeed. add an ".id(item.id)" on the VStack - and it is slow again.

I also found that when it works slow (without the mentioned workarounds) the second and the subsequent open of 100K list is must slower (about 10x) then the first opening.

I think that maybe under the hood, SwiftUI is generating calls to UITableViewDataSource.tableView(_:numberOfRowsInSection:) (or something functionally similar), and that that is inefficient for AnyViews, since there is no 1-1 correlation between a AnyView and a table view row. However, for statically known SwiftUI.TupleView, VStack and others it can infer the number of rows per ForEach "row".

On the other hand, for _ConditionalContent, AnyView and other similar views, it has to walk the entire collection in order to calculate the number of rows.

1 Like