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 somewhereThen 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! |
|
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. |
|
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. |
|
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. |
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. |
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? |
|
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 |
|
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)
} |
|
It would be interesting to get the point of view of @stephencelis or @mbrandonw about this. |
|
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. |
|
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... |
|
@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! |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
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
onAppearevent), 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
All reactions