Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.privacyboost.io/llms.txt

Use this file to discover all available pages before exploring further.

Performance

This guide covers performance optimization strategies for the Privacy Boost React Native SDK. Unlike the web SDK there’s no WASM to lazy-load — the heavy lifting runs in native iOS/Android binaries linked into the app. The costs to manage are SDK init, JS↔native bridge traffic, balance refresh frequency, and re-renders.

SDK Lifecycle

One Instance Per JS Runtime

PrivacyBoost holds a JWT, identity keys, and an internal note cache. Construct it once in a module-level ref or a context provider — not inside a component that may unmount and remount.
import {
  PrivacyBoost,
  PrivacyBoostConfig,
} from '@sunnyside-io/privacy-boost-react-native';

let cached: PrivacyBoost | null = null;

export function getPrivacyBoost(): PrivacyBoost {
  if (cached) return cached;
  cached = new PrivacyBoost(PrivacyBoostConfig.create({
    serverUrl: 'https://test-api.privacyboost.io',
    chainId: undefined,
    shieldContractAddress: undefined,
    wethContractAddress: '0x4200000000000000000000000000000000000006',
    teePublicKey: undefined,
    appId: 'app_abc123xyz',
    persistenceStorage: undefined,
    persistenceUnlock: undefined,
  }));
  return cached;
}
Or via context, lifted above any screen that needs it:
const PrivacyBoostContext = React.createContext<PrivacyBoost | null>(null);

export function PrivacyBoostProvider({ children }: { children: React.ReactNode }) {
  const ref = React.useRef<PrivacyBoost | null>(null);
  if (ref.current === null) {
    ref.current = getPrivacyBoost();
  }
  return (
    <PrivacyBoostContext.Provider value={ref.current}>
      {children}
    </PrivacyBoostContext.Provider>
  );
}

export const usePrivacyBoost = () => {
  const sdk = React.useContext(PrivacyBoostContext);
  if (!sdk) throw new Error('PrivacyBoostProvider missing');
  return sdk;
};

Defer Init Until First Use

The first call into the native SDK pays one-time setup cost. If your app has flows that don’t touch the privacy pool (onboarding, splash, marketing), defer construction until the first gated screen — keep it off cold-start.

Bridge Traffic

Every method on the SDK is a bridge call. The cost is small but not free — for tight loops or per-render fetches it adds up.

Avoid Per-Render Fetches

// BAD — fetches on every render
function BalanceLabel({ token }: { token: string }) {
  const sdk = usePrivacyBoost();
  const [balance, setBalance] = React.useState<string>('0');
  sdk.getBalance(token).then(b => setBalance(b.shieldedBalance));  // wrong
  return <Text>{balance}</Text>;
}

// GOOD — fetched once, refresh on demand
function BalanceLabel({ token }: { token: string }) {
  const sdk = usePrivacyBoost();
  const [balance, setBalance] = React.useState<string>('0');
  React.useEffect(() => {
    let active = true;
    sdk.getBalance(token).then(b => {
      if (active) setBalance(b.shieldedBalance);
    });
    return () => { active = false; };
  }, [sdk, token]);
  return <Text>{balance}</Text>;
}

Batch Where Possible

// SLOW — N bridge calls
const balances = [];
for (const token of tokens) {
  balances.push(await sdk.getBalance(token));
}

// FAST — one call
const balances = await sdk.getAllBalances();

Don’t await Inside forEach

// BAD — `forEach` doesn't await; promises run unsupervised
tokens.forEach(async (t) => {
  await sdk.getBalance(t);
});

// GOOD — Promise.all if you really need per-token, otherwise getAllBalances
const balances = await Promise.all(tokens.map(t => sdk.getBalance(t)));

Balance Refresh

Centralize Through One Hook

Put refresh logic in a single hook so screens don’t each schedule their own polling:
import { create } from 'zustand';

interface BalancesState {
  byToken: Record<string, TokenBalance>;
  lastFetch: number;
  refresh: (sdk: PrivacyBoost, force?: boolean) => Promise<void>;
}

const STALENESS_MS = 30_000;
let inflight: Promise<void> | null = null;

export const useBalances = create<BalancesState>((set, get) => ({
  byToken: {},
  lastFetch: 0,
  refresh: async (sdk, force = false) => {
    if (inflight) return inflight;
    if (!force && Date.now() - get().lastFetch < STALENESS_MS) return;
    inflight = (async () => {
      try {
        const fresh = await sdk.getAllBalances();
        const byToken: Record<string, TokenBalance> = {};
        for (const b of fresh) byToken[b.tokenAddress] = b;
        set({ byToken, lastFetch: Date.now() });
      } finally {
        inflight = null;
      }
    })();
    return inflight;
  },
}));
The module-scoped inflight deduplicates concurrent refreshes.

Refresh on Foreground, Not on a Timer

Tying refresh to setInterval drains battery. Use AppState:
import { AppState } from 'react-native';

React.useEffect(() => {
  const sub = AppState.addEventListener('change', (state) => {
    if (state === 'active') {
      useBalances.getState().refresh(sdk);
    }
  });
  return () => sub.remove();
}, [sdk]);

Re-Renders

Selector Subscriptions

If you’re using a global store (Zustand, Redux, Jotai), subscribe to the smallest slice:
// BAD — re-renders on every balance change anywhere
const balances = useBalances(s => s.byToken);

// GOOD — re-renders only when this token changes
const usdcBalance = useBalances(s => s.byToken[USDC]?.shieldedBalance);

Stable Keys in FlatList

For transaction history, key by txHash so the list reuses rows:
<FlatList
  data={transactions}
  keyExtractor={(tx) => tx.txHash}
  renderItem={({ item }) => <TransactionRow tx={item} />}
/>

Memoize Heavy Rows

If a row component does meaningful work (formatting amounts, deriving labels), wrap it in React.memo:
const TransactionRow = React.memo(function TransactionRow({ tx }: Props) {
  // ...
}, (a, b) => a.tx.txHash === b.tx.txHash && a.tx.status === b.tx.status);

Network

Parallelize Independent Reads

const [balances, history, fees] = await Promise.all([
  sdk.getAllBalances(),
  sdk.getTransactionHistory(undefined, 20, undefined),
  sdk.getFees(tokenId),
]);

Page History — Don’t Fetch It All

const firstPage = await sdk.getTransactionHistory(undefined, 20, undefined);
// later
if (firstPage.next) {
  const nextPage = await sdk.getTransactionHistory(undefined, 20, firstPage.next);
}

Memory

Drop Off-Screen Pages

For long histories, store the visible window only and refetch on scroll-back instead of accumulating every page in state. FlatList’s removeClippedSubviews and windowSize reduce native view memory but won’t help your JS state.

Don’t Wrap Big Arrays in useState

Arrays of Transaction objects can be heavy. Prefer a normalized store keyed by txHash so component state holds only ids:
const ids = useTransactions(s => s.visibleIds);    // string[]
const tx  = useTransactions(s => s.byHash[id]);    // single row

Operation Timing

Wrap SDK calls to observe timing during development:
async function timed<T>(label: string, op: () => Promise<T>): Promise<T> {
  const start = performance.now();
  try {
    return await op();
  } finally {
    const ms = performance.now() - start;
    if (__DEV__) console.log(`[PB] ${label}: ${ms.toFixed(1)}ms`);
    if (ms > 5_000) reportSlow(label, ms);
  }
}

const result = await timed('shield', () => sdk.shield(token, amount));
In production, send timings to your APM (Sentry Performance, Datadog RUM, Firebase Performance) so slow operations surface as percentile histograms.

What the SDK Already Optimizes

Things you do not need to layer on top:
  • Note cache — the SDK caches unspent notes natively and only refetches new merkle tree leaves.
  • Merkle tree pruning — proven nodes are kept; the rest are pruned.
  • JWT reuseclearSession() keeps identity keys so the next authenticate() is fast.
  • Native execution — proofs run on the native side; the JS thread is not blocked while a shield is being generated.

Best Practices Summary

  1. One SDK instance per JS runtime — hold it in a module-level ref or a context provider.
  2. Defer SDK init until the first privacy-pool screen.
  3. Don’t fetch in render bodies — use useEffect with cleanup.
  4. Refresh balances on AppState === 'active' and after writes, not on a timer.
  5. Deduplicate concurrent refreshes with an inflight ref.
  6. Subscribe to the smallest slice of state your screen needs.
  7. Page transaction history; don’t fetch it all.
  8. Use clearSession() over logout() when re-auth is expected soon.

Next Steps