Clarification on SwiftUI Environment Write Performance

I'm looking for clarification on a SwiftUI performance point mentioned in the recent Optimize your app's speed and efficiency | Meet with Apple video.

(YouTube link not allowed, but the video is available on the Apple Developer channel.)

At the 1:48:50 mark, the presenter says:

Writing a value to the Environment doesn't only affect the views that read the key you're updating. It updates any view that reads from any Environment key. [abbreviated quote]

That statement seems like a big deal if your app relies heavily on Environment values.


Context

I'm building a macOS application with a traditional three-panel layout. At any given time, there are many views on screen, plus others that exist in the hierarchy but are currently hidden (for example, views inside tab views or collapsed splitters).

Nearly every major view reads something from the environment—often an @Observable object that acts as a service or provider.

However, there are a few relatively small values that are written to the environment frequently, such as:

  • The selected tab index
  • The currently selected object on a canvas

The Question

Based on the presenter's statement, I’m wondering:

  • Does writing any value to the environment really cause all views in the entire SwiftUI view hierarchy that read any environment key to have their body re-evaluated?
  • Do environment writes only affect child views, or do they propagate through the entire SwiftUI hierarchy?

Example:

View A
 └─ View B
     ├─ View C
     └─ View D

If View B updates an environment value, does that affect only C and D, or does it also trigger updates in A and B (assuming each view has at least one @Environment property)?


Possible Alternative

If all views are indeed invalidated by environment writes, would it be more efficient to “wrap” frequently-changing values inside an @Observable object instead of updating the environment directly?

// Pseudocode
@Observable final class SelectedTab {
    var index: Int
}

ContentView()
    .environment(\.selectedTab, selectedTab)

struct TabView: View {
    @Environment(\.selectedTab) private var selectedTab

    var body: some View {
        Button("Action") {
            // Would this avoid invalidating all views using the environment?
            selectedTab.index = 1
        }
    }
}

Summary

From what I understand, it sounds like the environment should primarily be used for stable, long-lived objects—not for rapidly changing values—since writes might cause far more view invalidations than most developers realize.

Is that an accurate interpretation?

Follow-Up

In Xcode 26 / Instruments, is there a way to monitor writes to @Environment?

Answered by Engineer in 865321022

Hi! I'll try to provide some clarification:

Does writing any value to the environment really cause all views in the entire SwiftUI view hierarchy that read any environment key to have their body re-evaluated?

No, the view bodies are only run if the body uses the environment value for the key(s) used in that view and the value changes. But in SwiftUI, an update doesn't always cause the view body to run again, but there is still a cost associated with these updates. In Instruments, these updates are labeled as "skipped".

The demo in the video shows that the cumulative cost of all the skipped updates (seen in the "Consequences" detail view) is significant, even without their associated view bodies running (the view that demonstrates this in the video is called CardBackView).

Do environment writes only affect child views, or do they propagate through the entire SwiftUI hierarchy?

Great question! This only applies to child views of the .environment modifier that you're writing with. So if View B uses an .environment modifier on both View C and View D, and those views read any environment key(s), that would cause those to update (along with their children), although their view bodies may be skipped if the keys they read are unrelated.

If all views are indeed invalidated by environment writes, would it be more efficient to “wrap” frequently-changing values inside an @Observable object instead of updating the environment directly?

To reiterate, all views are not invalidated, but they are in fact updated, even if their bodies end up getting skipped. You can observe this behavior for yourself using Instruments and looking for skipped updates. However, for avoiding those updates, @Observable is a great choice. This is the exact method suggested in the video.

From what I understand, it sounds like the environment should primarily be used for stable, long-lived objects—not for rapidly changing values—since writes might cause far more view invalidations than most developers realize.

This is spot on. It's best not to write the environment in hot paths, like the one demonstrated in the video. Remember that not only your views are dependent on the environment; SwiftUI views are as well, so it's not only your own updates you have to worry about.

In Xcode 26 / Instruments, is there a way to monitor writes to @Environment?

Yes, look for EnvironmentWriter in Instruments, in both the lists of updates and the Cause & Effect graph. This is shown in the demos in the talk you linked.

Accepted Answer

Hi! I'll try to provide some clarification:

Does writing any value to the environment really cause all views in the entire SwiftUI view hierarchy that read any environment key to have their body re-evaluated?

No, the view bodies are only run if the body uses the environment value for the key(s) used in that view and the value changes. But in SwiftUI, an update doesn't always cause the view body to run again, but there is still a cost associated with these updates. In Instruments, these updates are labeled as "skipped".

The demo in the video shows that the cumulative cost of all the skipped updates (seen in the "Consequences" detail view) is significant, even without their associated view bodies running (the view that demonstrates this in the video is called CardBackView).

Do environment writes only affect child views, or do they propagate through the entire SwiftUI hierarchy?

Great question! This only applies to child views of the .environment modifier that you're writing with. So if View B uses an .environment modifier on both View C and View D, and those views read any environment key(s), that would cause those to update (along with their children), although their view bodies may be skipped if the keys they read are unrelated.

If all views are indeed invalidated by environment writes, would it be more efficient to “wrap” frequently-changing values inside an @Observable object instead of updating the environment directly?

To reiterate, all views are not invalidated, but they are in fact updated, even if their bodies end up getting skipped. You can observe this behavior for yourself using Instruments and looking for skipped updates. However, for avoiding those updates, @Observable is a great choice. This is the exact method suggested in the video.

From what I understand, it sounds like the environment should primarily be used for stable, long-lived objects—not for rapidly changing values—since writes might cause far more view invalidations than most developers realize.

This is spot on. It's best not to write the environment in hot paths, like the one demonstrated in the video. Remember that not only your views are dependent on the environment; SwiftUI views are as well, so it's not only your own updates you have to worry about.

In Xcode 26 / Instruments, is there a way to monitor writes to @Environment?

Yes, look for EnvironmentWriter in Instruments, in both the lists of updates and the Cause & Effect graph. This is shown in the demos in the talk you linked.

@StevenPeterson

Thanks for chiming in and thanks for the video itself, much appreciated.

No, the view bodies are only run if the body uses the environment value for the key(s) used in that view and the value changes.

That makes a lot more sense to me and aligns with the simple demo app I made to test out my original question.

Consider:

struct ContentView: View { 
  @Environment(\.name) var name
  @Environment(\.age) var age

  var body: some View { 
    Text(name)
  }
}

If \.name is updated anywhere, then ContentView.body will be run but if \.age is updated anywhere, then body is not run because \.age is not referenced in the body.

What worried me was that if somewhere else a view wrote to \.address, then ContentView would be re-evaluated even though it doesn't refer to \.address in any way. That's how I (incorrectly) interpreted the slide above.

But in SwiftUI, an update doesn't always cause the view body to run again, but there is still a cost associated with these updates.

That's the real clarifying statement, for me. There's a SwiftUI concept of "updating a view" that doesn't require calling body and there's a concept of "updating a view" that does require calling body.

You're saying that the cost of "updating a view even if it doesn't call body" is something that we should still consider in performance sensitive areas like scrolling. (ie: The cost is less than calling .body but it's not "zero".)

Yes, look for EnvironmentWriter in Instruments, in both the lists of updates and the Cause & Effect graph. This is shown in the demos in the talk you linked.

I'm quite thankful for the new SwiftUI Instrument, but I'm still learning how to correctly interpret the data it produces.

With your comments and video, I'm going to revisit some of my @Environment usage and likely replace writes an @Observable object of properties I formerly wrote directly into the environment.

My example above is contrived, but rather than having environment values for \.name, \.age and \.address it sounds like I should just have \.person, which is an @Observable object containing the three properties. That way, writes to any of those properties don't trigger the issue you were talking about in the video.

Cheers

My example above is contrived, but rather than having environment values for .name, .age and .address it sounds like I should just have .person, which is an @Observable object containing the three properties. That way, writes to any of those properties don't trigger the issue you were talking about in the video.

You could also just use @Environment(Person.self) private var person instead of a key, and inject it with .environment(person).

However, in your example, since those environment variables wouldn't likely update very frequently (they're not really in a hot path) the cost of putting them in the environment is probably negligible. In my demo, it really adds up because the environment is basically being updated on every single frame.

I wouldn't say that you need to go back to all your uses of environment values and change them to use @Observable classes, but if you're seeing performance issues, or building something new, it's worth considering.

However, in your example, since those environment variables wouldn't likely update very frequently (they're not really in a hot path) the cost of putting them in the environment is probably negligible.

Indeed, and now that I know that every write to any environment variable is not as egregious as I initially thought, then I'm a bit less concerned about this issue.

That said, I do want to revisit this a bit. In your demo, you were worried about writes in the hot path during a critical operation like scrolling.

What I'm finding on macOS isn't that I have "hot paths" but rather I have a lot of views being updated because of a very small change. These might be infrequent, the cost is high and noticeable.

An example would be keeping track of a selected objects on a canvas such that a bunch of inspector panels and other panes need to update their state based as the selection changes.

Selection changes are relatively infrequent (compared to scrolling) but a when they occur, a lot of views need to be updated. In my app, such a scenario is giving the application an overall "sluggish" feel as the user clicks around.

I wouldn't say that you need to go back to all your uses of environment values and change them to use @Observable classes, but if you're seeing performance issues, or building something new, it's worth considering.

One big issue I have with @Observable and @Environment is that I have to use a concrete type in @Environment if I want to be able to make an @Bindable reference. Ideally, I want the environment value to be generic. I can define it as being a protocol, but then I cannot convert the protocol to an @Bindable, or at least I don't know how.

Specifically, the following doesn't work:

struct Item {
  var frame: CGRect
}

protocol SelectionProvider: Observable {
  var selectedItem: Item? { get }
}

@Observable
final class Document: SelectionProvider {
  var selectedItem: Item?
}
extension EnvironmentValues {
  @Entry var selectionProvider: (any SelectionProvider)?
}

struct SampleView: View {   

@Environment(\.selectionProvider) private var selectionProvider

  var body: some View {
    // Error: 'init(wrappedValue:)' is unavailable:
    // The wrapped value must be an object that conforms to Observable
    @Bindable var selectionProvider = selectionProvider
  }
}
Clarification on SwiftUI Environment Write Performance
 
 
Q