Replies: 12 comments 13 replies
-
I'd also love to hear and discuss the ideas on how to work around this in an elegant way. In the meanwhile I can share the solution I currently use in my own project. My solution was to not use neither Here's this solution adopted to your test project: public final class ChildStores {
private var childStores: [String: Store<Child.State, Child.Action>] = [:]
func createIfNeeded(for child: Child.State) -> Store<Child.State, Child.Action> {
guard let store = self.childStores[child.id] else {
let store = Store<Child.State, Child.Action>(
initialState: Child.State(item: child.item),
reducer: Child.reducer,
environment: Child.Environment(loadItem: { item in
// ... pass load item here, probably it's worth extracting it to separate object to avoid code duplication
})
)
self.childStores[child.id] = store
return store
}
return store
}
}
let childStores = ChildStores() // doesn't need to be global, really, but it must be kept somewhere Then you can use it when constructing the Child.View: extension Main {
struct View: SwiftUI.View {
let store: Store<State, Action>
var body: some SwiftUI.View {
WithViewStore(store) { viewStore in
ScrollView {
LazyVGrid(columns: [.init(), .init()]) {
ForEach(viewStore.children) { (item: Child.State) in
Child.View(store: childStores.createIfNeeded(for: item))
}
}
}
}
}
}
} As you can test, the performance is fine again. However, the solution, while working and pragmatic, looks more like a workaround to me. I'd love to scrap it for something better, so please share if you find anything! |
Beta Was this translation helpful? Give feedback.
-
Oh, and of course this brings the whole world of maintaining the separate stores (like clearing them when they are no longer needed etc.) which I'm dealing with in my actual project, I've just not include it in the samples above. |
Beta Was this translation helpful? Give feedback.
-
Just an idea, could you have an ImageLoader in the Environment which offers Publishers for loading the images? I think images are of ephemeral nature anyway, and don’t need their state persisted in a store. The ImageLoader can also employ caching if necessary. |
Beta Was this translation helpful? Give feedback.
-
Yeah, but how the View uses the image (or any kind of data) then? The view doesn't have the access to the environment, it only has the access to the state via view store. So even if ImageLoader does the caching and stuff, the image still has to be kept in the state so that it's exposed through the view store to the view. Ofc you can just inject the ImageLoaded to the view directly. It is also cool, but it means you're just not using the TCA for that part of the app. |
Beta Was this translation helpful? Give feedback.
-
True, I think that's what I'd end up doing.
IMO that's ok - TCA is probably not meant to be a low-level, high-frequency information bus. |
Beta Was this translation helpful? Give feedback.
-
With the current performance constraints looks like TCA is not a good tool for such use cases, I agree. I wonder if it could be improved by some internal optimisation. Time Profiler has led me to believe that what really slows action sending is Combine and the usage of CurrentValueSubject for keeping state in store. Maybe using an alternative mechanism there (custom subject?) could help? |
Beta Was this translation helpful? Give feedback.
-
Yeah, since the store isn't thread safe anyway, that would probably work. CombineExt has a ReplaySubject that could probably be adapted to storing only one value quickly: https://github.com/CombineCommunity/CombineExt/blob/main/Sources/Subjects/ReplaySubject.swift |
Beta Was this translation helpful? Give feedback.
-
For me, the me main issue is that scope operations + equality tests are on the main thread. public func scope<LocalValue, LocalAction>(
state toLocalState: @escaping (State) -> LocalValue,
distinctUntilChanged: @escaping (LocalValue, LocalValue) -> Bool,
action fromLocalAction: @escaping (LocalAction) -> Action
) -> Store<LocalValue, LocalAction> {
let childState = state
.map(toLocalState)
.distinctUntilChanged(distinctUntilChanged)
let localStore: Store<LocalValue, LocalAction> = .init(
state: childState,
initialValue: toLocalState(internalValue.value),
environment: environment
)
let parentDiposable = localStore
.action
.map(fromLocalAction)
.subscribe(action)
localStore.parentDiposable = parentDiposable
return localStore
} With the corresponding private initializer // Initializers
private init<Environment>(
state: Observable<State>,
initialValue: State,
environment: Environment
) {
self.environment = environment
self.state = state
self.internalValue = .init(value: initialValue)
} |
Beta Was this translation helpful? Give feedback.
-
It would be interesting to get the point of view of @stephencelis or @mbrandonw about this. |
Beta Was this translation helpful? Give feedback.
-
Thanks for all the discussion! We investigated this a bit this morning and there's definitely more work to be done with
We'll keep y'all updated with whatever we find in the future, and please let us know if you stumble upon anything! Also, I am going to convert this issue to a discussion as that's a bit more appropriate for this. |
Beta Was this translation helpful? Give feedback.
-
We tried to replace We are thinking about a solution of sending all the actions in a background thread, and switch on the main thread only when updating the view... Working on it when we got some time... |
Beta Was this translation helpful? Give feedback.
-
@jtouzy Can you give this PR branch a try? #386 It should improve performance for both index-based Edit: also please compare performance in release mode, as well! |
Beta Was this translation helpful? Give feedback.
-
Describe the bug
We are currently facing a performance issue, when triggering an action from a large number of child components (in a ForEachStore), for loading their content. When the action is dispatched (from
onAppear
event), even if nothing is done inside it, the scroll is very laggy. I can't even find a good reason in Time Profiler, but I suspect all the actions are dispatched in Main thread, that causes the lag.To Reproduce
I created a very lightweight project to reproduce the laggy scroll.
Just checkout this : https://github.com/jtouzy/PerformanceTestTCA
You will see the laggy scroll when you accelerate a bit (using a real device is better to see the difference).
If you just comment the line 42 in Child.swift, you will see that the scroll is perfectly fine, when no actions are triggered.
Even if you comment the reducer side effects for the action, and just return .none, the lag is still present.
That's why we are thinking about the thread.
Maybe you have a solution for this or maybe we need to implement it another way?
Environment
Beta Was this translation helpful? Give feedback.
All reactions