Skip to main content
As your features and application grow, you may run into performance problems. This article outlines common pitfalls and how to fix them.

Sharing logic with actions

There is a common pattern of using actions to share logic across multiple parts of a reducer. This is inefficient. Sending actions is not as lightweight as calling a method on a class.

The problem

Suppose you want to run shared logic after three different actions:
This sends two actions for every user action, which is inefficient.

Problems with this approach

Performance

Two actions are processed for every user interaction

Inflexibility

Shared logic must always run after, never before

Test bloat

Tests must assert on internal shared actions

The solution

Share logic using methods instead of actions:
1

Define a method on your reducer

The method can take inout State if it needs to mutate state, and return Effect<Action> if it needs to run effects.
2

Call the method directly

Call it from any action handler without sending additional actions.
3

Enjoy the flexibility

You can now run shared logic before, after, or between other logic:

Testing improvements

Tests become more streamlined:

CPU-intensive calculations

Reducers run on the main thread and are not appropriate for intense CPU work.
Never perform CPU-intensive work directly in a reducer. It will block the main thread.

The problem

The solution

Return an effect to perform work in the cooperative thread pool:
Sprinkle in Task.yield() periodically to avoid blocking threads in the cooperative pool.

High-frequency actions

Sending actions comes with a cost. Avoid high-frequency actions unless your application truly needs them.

Example: Reporting progress

Instead of reporting progress for every step:
Report it periodically:

Example: Sliders

Deriving a binding directly from the store sends an action for every pixel:
Instead, use local @State and send one action when done:

Store scoping

The most common form of scoping—scoping directly to child features—is the most performant and is the intended use.

Good scoping (performant)

Scoping directly to child state and actions:
Or for navigation:

Problematic scoping

Scoping on computed properties can cause performance issues:
Then scoping:
In version 1.5+, scoped stores hold a reference to the root store and transform on access. Heavy computed properties will be invoked many times.

The solution

Use scope only along stored properties of child features:
1

Use stored properties

2

Move computation to child

Push computed logic into the child view or reducer, towards the leaf nodes of your application.

Performance checklist

Use this checklist to identify and fix performance issues:
❌ Don’t send actions for shared logic✅ Use methods on your reducer instead
❌ Don’t perform intense calculations in reducers✅ Return effects that do work in the thread pool with periodic yields
❌ Don’t send dozens/hundreds of actions per second✅ Throttle or debounce actions, or use local @State
❌ Don’t use computed properties in scope✅ Use stored properties and move computation to child features

Profiling tips

1

Use Instruments

Profile your app with Xcode’s Instruments to identify slow reducers and view bodies.
2

Add print statements

Put prints in computed properties to see how often they’re invoked:
3

Measure action processing time

Use the library’s built-in instrumentation to see how long actions take:

Summary

Share logic with methods

Not actions

CPU work in effects

Not reducers

Throttle high-frequency actions

Or use local state

Scope stored properties

Not computed properties