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 iOS SDK. The native binary is small enough that bundle size is rarely the bottleneck — the costs to manage are SDK init, proof generation, balance refresh frequency, and main-thread hygiene.

SDK Lifecycle

One Instance Per Process

PrivacyBoost holds a JWT, identity keys, and an internal note cache. Initialize it once and hold it in a long-lived owner — usually an @MainActor singleton or an environment object.
@MainActor
final class PrivacyBoostStore: ObservableObject {
    static let shared = PrivacyBoostStore()
    let sdk: PrivacyBoost

    private init() {
        let config = PrivacyBoostConfig(
            serverUrl: "https://test-api.privacyboost.io",
            chainId: nil,
            shieldContractAddress: nil,
            wethContractAddress: "0x4200000000000000000000000000000000000006",
            teePublicKey: nil,
            appId: "app_abc123xyz",
            persistenceStorage: nil,
            persistenceUnlock: nil
        )
        // Force-unwrap is acceptable here — a missing app id at launch is fatal.
        self.sdk = try! PrivacyBoost(config: config)
    }
}
Re-instantiating the SDK on every screen would re-derive caches, refetch the merkle tree, and force a fresh JWT.

Defer Init Until First Use

If your app has flows that don’t touch the privacy pool (onboarding, marketing screens), defer SDK construction until the first gated route. The Rust core does some setup work on first call — keep it off the cold-start path.
struct RootView: View {
    @State private var store: PrivacyBoostStore?

    var body: some View {
        Group {
            if let store {
                WalletView()
                    .environmentObject(store)
            } else {
                OnboardingView(onContinue: {
                    store = PrivacyBoostStore.shared
                })
            }
        }
    }
}

Off the Main Thread

All SDK operations are async throws. Run them via Task { ... } or .task { ... } modifiers, never on the main run loop. A misplaced try await inside a synchronous function will block the actor it’s called on; let SwiftUI’s structured concurrency schedule it.
struct DepositButton: View {
    @EnvironmentObject var store: PrivacyBoostStore
    @State private var pending = false

    var body: some View {
        Button("Deposit") {
            Task {
                pending = true
                defer { pending = false }
                _ = try? await store.sdk.shield(
                    tokenAddress: token,
                    amount: amount
                )
            }
        }
        .disabled(pending)
    }
}
For long-running proofs in widgets or notification extensions, hop off the main actor explicitly:
let result = try await Task.detached(priority: .userInitiated) {
    try await sdk.shield(tokenAddress: token, amount: amount)
}.value

Balance Refresh

Don’t Poll From Every Screen

getBalance(tokenAddress:) and getAllBalances() hit the network. Centralize refreshes through a single observable store and let screens subscribe.
@MainActor
final class BalanceStore: ObservableObject {
    @Published private(set) var balances: [String: TokenBalance] = [:]
    @Published private(set) var lastFetch: Date?

    private let sdk: PrivacyBoost
    private let staleness: TimeInterval = 30

    init(sdk: PrivacyBoost) { self.sdk = sdk }

    func refresh(force: Bool = false) async throws {
        if !force,
           let last = lastFetch,
           Date().timeIntervalSince(last) < staleness {
            return
        }
        let fresh = try await sdk.getAllBalances()
        balances = Dictionary(uniqueKeysWithValues: fresh.map { ($0.tokenAddress, $0) })
        lastFetch = Date()
    }
}

Refresh on Foreground, Not on a Timer

Repeated background polling drains battery. Refresh on scenePhase == .active and after operations that change balances:
struct AppContent: View {
    @EnvironmentObject var balanceStore: BalanceStore
    @Environment(\.scenePhase) private var scenePhase

    var body: some View {
        ContentView()
            .task(id: scenePhase) {
                if scenePhase == .active {
                    try? await balanceStore.refresh()
                }
            }
    }
}

Coalesce Concurrent Refreshes

If multiple views hit refresh simultaneously, deduplicate so only one network call happens:
actor BalanceCoordinator {
    private var inflight: Task<Void, Error>?
    private let sdk: PrivacyBoost

    init(sdk: PrivacyBoost) { self.sdk = sdk }

    func refresh() async throws {
        if let inflight { return try await inflight.value }
        let task = Task {
            _ = try await sdk.getAllBalances()
        }
        inflight = task
        defer { inflight = nil }
        _ = try await task.value
    }
}

Network

Parallelize Independent Reads

Sequential awaits serialize unnecessarily. Use async let:
async let balances = sdk.getAllBalances()
async let history  = sdk.getTransactionHistory(txType: nil, limit: 20, cursor: nil)
async let fees     = sdk.getFees(token: token)

let (b, h, f) = try await (balances, history, fees)

Page History — Don’t Fetch It All

Pass a small limit and use the returned cursor for pagination instead of asking for the full history at once:
let firstPage = try await sdk.getTransactionHistory(
    txType: nil,
    limit: 20,
    cursor: nil
)
// later
if let next = firstPage.next {
    let nextPage = try await sdk.getTransactionHistory(
        txType: nil,
        limit: 20,
        cursor: next
    )
}

Memory

Drop Heavy Caches When Backgrounded

If your app stays in the background long enough to receive a memory warning, the SDK’s internal state is the second-largest consumer after image caches. Clear non-identity state on .didReceiveMemoryWarning or scene-phase transitions.
.onChange(of: scenePhase) { phase in
    if phase == .background {
        // Drop pending transaction views, transaction history pages, etc.
        balanceStore.trimToCurrentToken()
    }
}

Don’t Hold Transaction Arrays in @State

For long histories, store an IdentifiedArray keyed by txHash and flush old entries beyond the visible window. SwiftUI re-diffs @State arrays on every change.

Operation Timing

Wrap SDK calls to observe timing in development:
func timed<T>(_ label: String, op: () async throws -> T) async throws -> T {
    let start = ContinuousClock.now
    defer {
        let elapsed = ContinuousClock.now - start
        print("[PB] \(label): \(elapsed)")
    }
    return try await op()
}

let result = try await timed("shield") {
    try await sdk.shield(tokenAddress: token, amount: amount)
}
Hook into MetricKit (MXMetricManager) in production to surface slow operations as histograms rather than print statements.

What the SDK Already Optimizes

Things you do not need to layer on top:
  • Note cache — the SDK caches unspent notes locally 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.

Best Practices Summary

  1. One SDK instance per process — hold it in a long-lived owner.
  2. Defer SDK init until the first privacy-pool screen.
  3. All operations on a Task — never on the main run loop.
  4. Refresh balances on foreground and after writes, not on a timer.
  5. Deduplicate concurrent refreshes through an actor.
  6. Parallelize independent reads with async let.
  7. Page transaction history; don’t fetch it all.
  8. Use clearSession() over logout() when re-auth is expected soon.

Next Steps