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 reuse —
clearSession() keeps identity keys so the next authenticate() is fast.
Best Practices Summary
- One SDK instance per process — hold it in a long-lived owner.
- Defer SDK init until the first privacy-pool screen.
- All operations on a
Task — never on the main run loop.
- Refresh balances on foreground and after writes, not on a timer.
- Deduplicate concurrent refreshes through an actor.
- Parallelize independent reads with
async let.
- Page transaction history; don’t fetch it all.
- Use
clearSession() over logout() when re-auth is expected soon.
Next Steps