Skip to content

Performance Best Practices ​

Learn how to optimize your Nexus State applications for maximum performance.

Table of Contents ​


Core Optimization Strategies ​

1. Use Selective Subscriptions ​

Subscribe only to the state you actually need, not the entire store.

typescript
// ❌ Bad - Component re-renders on any store change
const Component = () => {
  const store = useStore();
  return <div>{store.get(userAtom).name}</div>;
};

// ✅ Good - Component only re-renders when userAtom changes
const Component = () => {
  const [user] = useAtom(userAtom);
  return <div>{user.name}</div>;
};

Why it matters: Selective subscriptions reduce unnecessary re-renders and improve UI responsiveness.


2. Minimize Store Creation Overhead ​

Create stores at the appropriate scope - avoid creating new stores unnecessarily.

typescript
// ❌ Bad - New store on every render
function Component() {
  const store = createStore();
  return <div>{store.get(countAtom)}</div>;
}

// ✅ Good - Store created once, reused across renders
const store = createStore();
function Component() {
  return <div>{store.get(countAtom)}</div>;
}

Exception: For SSR, create isolated stores per request (this is the correct pattern):

typescript
// ✅ SSR - Isolated store per request (correct!)
export async function getServerSideProps(context) {
  const store = createStore();
  store.set(userAtom, await fetchUser(context.params.id));
  return { props: { initialState: store.getState() } };
}

Batching Updates ​

When to Use Batching ​

Batch multiple state updates to trigger only a single notification cycle.

typescript
// ❌ Bad - 3 separate notifications
store.set(a, 1);
store.set(b, 2);
store.set(c, 3);

// ✅ Good - Single notification
batch(() => {
  store.set(a, 1);
  store.set(b, 2);
  store.set(c, 3);
});

Batching in React Components ​

typescript
// ❌ Bad - Multiple re-renders
function Form() {
  const [name, setName] = useAtom(nameAtom);
  const [email, setEmail] = useAtom(emailAtom);
  const [age, setAge] = useAtom(ageAtom);

  const handleSubmit = () => {
    setName('John');
    setEmail('john@example.com');
    setAge(30);
  };

  return <button onClick={handleSubmit}>Submit</button>;
}

// ✅ Good - Single re-render
function Form() {
  const store = useStore();

  const handleSubmit = () => {
    batch(() => {
      store.set(nameAtom, 'John');
      store.set(emailAtom, 'john@example.com');
      store.set(ageAtom, 30);
    });
  };

  return <button onClick={handleSubmit}>Submit</button>;
}

Batching Performance Impact ​

Scenarioops/secmean (ms)Improvement
100 sets without batch8091.24baseline
100 sets with batch7551.32single notification

Note: Batching shows minimal improvement for simple sets, but provides significant benefits when subscribers trigger expensive operations (e.g., DOM updates, API calls).


Computed Atoms Optimization ​

Limit Dependencies ​

Keep the number of dependencies per computed atom low.

typescript
// ❌ Bad - Too many dependencies (39ms latency)
const allDataAtom = atom((get) => {
  const a = get(atomA);
  const b = get(atomB);
  const c = get(atomC);
  const d = get(atomD);
  const e = get(atomE);
  const f = get(atomF);
  const g = get(atomG);
  const h = get(atomH);
  const i = get(atomI);
  const j = get(atomJ);
  return { a, b, c, d, e, f, g, h, i, j };
});

// ✅ Good - Split into smaller computed atoms
const firstHalfAtom = atom((get) => {
  const a = get(atomA);
  const b = get(atomB);
  const c = get(atomC);
  const d = get(atomD);
  const e = get(atomE);
  return { a, b, c, d, e };
});

const secondHalfAtom = atom((get) => {
  const f = get(atomF);
  const g = get(atomG);
  const h = get(atomH);
  const i = get(atomI);
  const j = get(atomJ);
  return { f, g, h, i, j };
});

const allDataAtom = atom((get) => ({
  ...get(firstHalfAtom),
  ...get(secondHalfAtom),
}));

Performance Impact:

Dependenciesmean (ms)p99 (ms)
11.754.63
510.1718.88
1039.4257.28 ⚠️

Avoid Deep Computed Chains ​

While Nexus State handles chains efficiently, very deep chains can accumulate latency.

typescript
// ⚠️ Acceptable - Chain of 5 (3.94ms)
const a = atom(0);
const b = atom((get) => get(a) + 1);
const c = atom((get) => get(b) + 1);
const d = atom((get) => get(c) + 1);
const e = atom((get) => get(d) + 1);

// ❌ Avoid - Chain of 20+ (78ms+)
// Consider restructuring with fewer intermediate steps

Memoize Expensive Calculations ​

For expensive computations, use manual memoization.

typescript
// ❌ Bad - Recalculates on every access
const expensiveAtom = atom((get) => {
  const data = get(dataAtom);
  return data.map(item => expensiveCalculation(item));
});

// ✅ Good - Memoized calculation
const expensiveAtom = atom((get) => {
  const data = get(dataAtom);
  const cache = get(expensiveCacheAtom);
  
  const key = JSON.stringify(data);
  if (cache[key]) return cache[key];
  
  const result = data.map(item => expensiveCalculation(item));
  return result;
});

Memory Management ​

Clean Up Subscriptions ​

Always unsubscribe when components unmount.

typescript
// ❌ Bad - Memory leak
useEffect(() => {
  store.subscribe(countAtom, (value) => {
    console.log(value);
  });
}, []);

// ✅ Good - Proper cleanup
useEffect(() => {
  const unsubscribe = store.subscribe(countAtom, (value) => {
    console.log(value);
  });
  return unsubscribe;
}, []);

Limit Time Travel History ​

Reduce memory usage by limiting history size.

typescript
// ❌ Bad - Unlimited history (memory leak risk)
const store = createStore({
  enableTimeTravel: true,
});

// ✅ Good - Limited history
const store = createStore({
  enableTimeTravel: true,
  maxHistory: 50, // Adjust based on needs
});

Use Computed Atoms Instead of Derived State ​

Avoid storing derived state - compute it on demand.

typescript
// ❌ Bad - Redundant state storage
const itemsAtom = atom([]);
const totalAtom = atom(0); // Manually updated

store.set(itemsAtom, [...items, newItem]);
store.set(totalAtom, total + 1); // Easy to forget!

// ✅ Good - Computed on demand
const itemsAtom = atom([]);
const totalAtom = atom((get) => get(itemsAtom).length);

store.set(itemsAtom, [...items, newItem]);
// totalAtom automatically updates

Time Travel Performance ​

Capture Strategically ​

Don't capture state too frequently.

typescript
// ❌ Bad - Captures on every change
store.subscribe(countAtom, () => {
  controller.capture('count-change');
});

// ✅ Good - Capture at meaningful points
function increment() {
  batch(() => {
    store.set(countAtom, c => c + 1);
    store.set(historyAtom, h => [...h, 'increment']);
  });
  controller.capture('user-action');
}

Use Descriptive Labels ​

Labels help with debugging and don't impact performance.

typescript
// ✅ Good - Descriptive labels
controller.capture('user-login');
controller.capture('cart-add-item');
controller.capture('form-submit');

Common Pitfalls ​

Pitfall 1: Over-Subscription ​

Problem: Subscribing to more state than needed.

typescript
// ❌ Bad
const Component = () => {
  const [state] = useAtom(largeStateAtom);
  return <div>{state.user.name}</div>; // Only needs user.name
};

// ✅ Good
const Component = () => {
  const [user] = useAtom(userAtom);
  return <div>{user.name}</div>;
};

Solution: Split large atoms into smaller, focused atoms.


Pitfall 2: Unnecessary Re-renders ​

Problem: State updates trigger re-renders when nothing visible changes.

typescript
// ❌ Bad - Updates even when value is the same
store.set(countAtom, 5);
store.set(countAtom, 5); // Unnecessary re-render

// ✅ Good - Check before updating
const currentValue = store.get(countAtom);
if (currentValue !== 5) {
  store.set(countAtom, 5);
}

Solution: Use conditional updates or implement equality checks.


Pitfall 3: Circular Dependencies ​

Problem: Computed atoms that depend on each other.

typescript
// ❌ Bad - Circular dependency (runtime error)
const atomA = atom((get) => get(atomB));
const atomB = atom((get) => get(atomA));

// ✅ Good - Clear dependency direction
const atomA = atom(0);
const atomB = atom((get) => get(atomA) * 2);

Solution: Design clear dependency graphs.


Pitfall 4: Missing Batch in Loops ​

Problem: Updating atoms in loops without batching.

typescript
// ❌ Bad - 1000 separate notifications
for (let i = 0; i < 1000; i++) {
  store.set(itemsAtom, (items) => [...items, i]);
}

// ✅ Good - Single notification
batch(() => {
  for (let i = 0; i < 1000; i++) {
    store.set(itemsAtom, (items) => [...items, i]);
  }
});

// ✅ Even better - Single update
store.set(itemsAtom, (items) => [...items, ...Array.from({ length: 1000 }, (_, i) => i)]);

Pitfall 5: Stale Closures in Subscriptions ​

Problem: Subscriptions capture stale values.

typescript
// ❌ Bad - Stale closure
let count = 0;
store.subscribe(countAtom, () => {
  console.log(count); // Always logs 0
});
store.set(countAtom, 5);

// ✅ Good - Use current value from store
store.subscribe(countAtom, () => {
  const currentCount = store.get(countAtom);
  console.log(currentCount); // Logs current value
});

Pitfall 6: Creating Atoms Inside Components ​

Problem: Atoms should be defined once, outside components.

typescript
// ❌ Bad - New atom on every render
function Component() {
  const countAtom = atom(0); // ⚠️ Creates new atom each render!
  const [count] = useAtom(countAtom);
  return <div>{count}</div>;
}

// ✅ Good - Atom defined once
const countAtom = atom(0);
function Component() {
  const [count] = useAtom(countAtom);
  return <div>{count}</div>;
}

Performance Checklist ​

Before deploying your application:

  • [ ] Used selective subscriptions (not entire store)
  • [ ] Batched multiple updates in event handlers
  • [ ] Limited computed atom dependencies (<5 ideal)
  • [ ] Cleaned up subscriptions in useEffect cleanup
  • [ ] Limited Time Travel history size
  • [ ] Avoided storing derived state (use computed atoms)
  • [ ] No atoms created inside components
  • [ ] No circular dependencies
  • [ ] Used batching in loops
  • [ ] Checked for stale closures in subscriptions

Next Steps ​