How I Doubled iOS App Speed Using Swift Structs

How I Doubled iOS App Speed Using Swift Structs

18 min read Learn how switching to Swift structs can dramatically speed up your iOS app, with practical comparisons and performance benchmarks.
(0 Reviews)
Discover how leveraging Swift structs instead of classes boosted my iOS app's performance. This article breaks down the technical reasons behind the improvement, offers concrete before-and-after metrics, and discusses best practices for maximizing struct benefits in real-world iOS development.
How I Doubled iOS App Speed Using Swift Structs

How I Doubled iOS App Speed Using Swift Structs

Releasing an iOS app is exciting—until you watch animations stutter, list scrolling lag, and users start to leave negative reviews about performance. This was my reality not long ago. Like many, I leaned on class-based model objects, foundation patterns I trusted, and code inherited from frameworks. But our app was growing, functionality ballooned, and it had become slow—frustratingly so for a codebase only two years old.

Curious if there was a better way, I started dissecting Apple’s own design philosophies. The result? An ambitious, app-wide experiment in replacing reference-heavy patterns with Value Semantics: namely, Swift structs. The outcome astonished me: Not only did I double the performance by key metrics, but I also simplified code and future-proofed maintainability. Here’s the journey—let’s explore each phase, from motivation to insight and benchmarks to practical steps.

Seeing the Lag: Identifying Growing Pains

iOS app, lag, user frustration, performance graph, stuttering animation

Performance regressions snuck up on me as modules proliferated and data models became more complex. Sign-in screens bubbled with cumbersome spinner delays. Feed reconstruction caused visible lags when users refreshed. Simple list scrolling—a joy in the earliest builds—now stuttered if lists were even moderately large.

We tracked performance daily on production usage with Firebase Performance Monitoring and custom logging. Typical pain points included:

  • Sluggish Launch: Cold start was up to 1.5 seconds slower than a fresh template app.
  • Slow Scrolling: UITableView cells stuttered with high TTI (Time To Interact) exceeding 120 ms when rendering complex items.
  • Hitches On Data Updates: Changing a favorite, posting a comment, or updating a profile triggered brief but noticeable UI freezes.

Profiling in Instruments showed suspiciously high CPU usage during simple operations. Formally, our technical debt diagnosis pointed to excessive copying, retain/release churn, and ARC overheads—especially around complex model classes reused across view controllers.

Understanding Reference vs. Value Types

Swift class, struct, memory graph, ARC cycle, value vs reference

Swift, unlike Objective-C, features first-class value types: namely, structs. By default, many Cocoa and Apple frameworks (like String, Array, Dictionary) are built atop copy-on-write structs. Our models, on the other hand, were classes—classically modeling entities with inheritance chains:

class UserProfile: NSObject, Codable {
    var id: String
    var name: String
    var points: Int
}

Imagine a hierarchical feed structure with FeedItem, UserProfile, and Comment—all classes. Navigating, filtering, and sorting required copying arrays of objects, but with classes, only references were copied. This led to subtle side effects: shared mutable state, thread-safety issues, strong reference cycles, as well as expensive retain and release operations under the hood (ARC activity was a top Instruments culprit).

In contrast, structs in Swift are value types. Each assignment creates a copy unless the optimizer determines the copy unnecessary. Most importantly, there’s no reference counting. With value semantics:

  • There’s no hidden sharing between unrelated parts of code unless you explicitly pass references.
  • Memory and thread safety are improved because every variable holds its own state (unless copy-on-write applies, as in standard library collections).
  • Performance is higher when your data model doesn’t require dynamic inheritance or identity.

Refactoring With Value Semantics: First Steps

code refactoring, Swift struct, before/after code, clean code, workflow

Emboldened by Apple’s message about structs, I began refactoring model objects with the goal of minimizing classes and maximizing structs. The guiding rules I adopted:

  1. Prefer struct if model represents data, not behavior.
  2. Classes only where identity or shared mutable state is essential.
  3. Utilize protocol conformity for code reuse and abstraction, not inheritance.

So, a typical feed item morphed from:

class FeedItem: Codable {
    var id: String
    var author: UserProfile
    var content: String
}

into:

struct FeedItem: Codable {
    let id: String
    let author: UserProfile
    let content: String
}

Every table or collection-backed model, settings state, auxiliary payload, and parsing intermediary was migrated to a struct where possible. We audited across the codebase, targeting areas where model mutability wasn’t a requirement and where performance bottlenecks had appeared.

Key Steps

  • Defined all canonical models as structs.
  • Adopted clear immutable-by-default policies, replacing var with let where feasible.
  • Marked protocols like Equatable, Hashable for sorting/search; Codable for networking.
  • Wrote migration scripts and compiler checks to catch legacy reference-type usage.

Tangible Gains: Benchmarking Performance Improvements

benchmark, speedometer, app testing, Instruments, before-and-after graph

How do you prove a speed gain wasn’t placebo? I rigorously measured impact across several areas.

Launch Time

New relic traces revealed app cold launch times dropped by 35–47%, getting from splash to interactive UI within 800 ms (from previous levels of 1.4–1.7 seconds). The profiler highlighted the elimination of ARC retain/release cycles on repeated model graph construction.

Scrolling Tests

Lists with over 200 items previously introduced micro-stutters, primarily during cell-to-model binding. Profiling found:

  • ARC activity (Measured in Instruments with “Zombie Objects”) directly correlated with frame rate drops.
  • After moving to structs, retention-related CPU spikes vanished.
  • Smooth, 60 FPS scrolling under typical working sets, no identical conditions prior.

Data Modification

Actions that updated data—like favoriting, commenting, or refreshing content—were vastly more responsive. Value type updates resolved synchronously, unburdened by main-thread ARC activity or contention for shared state locks.

User Satisfaction improved measurably: our Spring 2023 update led to a sharp reduction in performance complaints, and our app’s App Store rating rose by 0.4 points over six weeks post-launch.

Deeper Dive: Why Are Structs So Fast?

Swift memory, ARC overhead, cache line, processor, computer architecture

The reasons Swift structs accelerate iOS apps are both architectural and behavioral:

1. No ARC Overhead

Classes use Automatic Reference Counting (ARC), which adds a reference count field, tracks ownership, and injects retain/release calls into code. For high-frequency model lifecycles, this imposes significant work. Structs don’t participate in ARC—they’re just data.

Instruments View — Pre-Refactor: Yellow in the “Allocations” graph depicted waves of retain/release during model-creation.

Post-Refactor: ARC events flatlined. Code ran cleaner, spending more time on actual business logic.

2. Stack Allocation & Cache Locality

Swift structs, especially small ones (< 3 word size), often reside on the stack (unless optimized away entirely). Memory locality means less L1/L2 cache thrash and fewer heap allocations—your app benefits every frame.

3. Copy-on-Write (CoW) Safety

For collections (Array, Dictionary), Swift employs CoW. Replacing custom class-based tree/graph models with struct compounding allowed the optimizer to minimize wasteful copies. Our biggest feed-generation bottleneck—a list merge for pagination—became negligible after this change.

4. Thread Safety by Design

Because value types don’t share state by default, thread synchronizations shrink dramatically. Structs contributed to safer background parsing and diffing, banishing hard-to-reproduce crashes due to unexpected data races.

Real-World Example: Struct Refactor in Practice

Swift code, app development, refactoring process, real-world example

Take our UserProfile model:

Before (class-based, hidden costs):

class UserProfile: Codable, CustomStringConvertible {
    var id: String
    var screenName: String
    var status: Status
}

enum Status: String, Codable {
    case active, deleted
}

Antipatterns included routine modification, unnecessary mutability, and ARC overhead. Tracking users or copying for async refreshes led to thread confusion.

After (struct-based, value semantics):

struct UserProfile: Codable, Equatable, Hashable, CustomStringConvertible {
    let id: String
    let screenName: String
    let status: Status
}
  • All fields switched to let (immutable after decoding or creation).
  • Model updated via value replacement, not property mutation.
  • No hidden reference sharing.

Practical gain: We saw a 2X speed-up in rendering user info lists compared to the class-based implementation—traced via repeated synthetic list loads in testing.

Comparing Structs and Classes: When to Use Which

comparison chart, class vs struct, decision tree, app architecture

Swift doesn’t outlaw classes—a wise architect chooses tools for the job. Here’s a cheat-sheet I distilled from experience:

Use Struct Use Class
Data-centric models Reference-based identity
Pure value, e.g. DTOs Side effects/callback storage
Immutable or copy-on-write Mutability required / shared
Equatable/Hashable for free Protocol as reference anchor
Thread-safe by default Need dynamic polymorphism

Example: Models for the network/API boundary (User, Message, Feed) fit structs perfectly. Controllers, services, systems with shared ownership (e.g. NotificationCenter observer, system data caches) remain classes.

Tip: Whenever in doubt, start by designing with structs. Only refactor to classes if forced by context.

Migration Challenges and How to Overcome Them

migration, debugging, Swift errors, legacy code, teamwork

Transitioning a large codebase from class to struct-based models is rarely trivial. A few gotchas and their resolutions from our refactor:

Mutability

Developers initially struggled with losing var properties on the model. Education on creating updated copies (item = item.withUpdatedProperty(newValue)) was necessary. I introduced:

extension UserProfile {
    func updating(screenName: String) -> UserProfile {
        UserProfile(id: id, screenName: screenName, status: status)
    }
}

Identity

For some models, code relied on identity comparison (===). Replacing with id checks or protocol-based equality resolved ambiguity:

users.map { $0.id == targetId }

Mixed Ownership

Legacy code mixed classes and structs, leading to unpredictable behavior. We set strict linting rules, and documented struct/class boundaries—especially around networking, persistence, and UI logic.

UI Bindings

@propertyWrappers (@State, @ObservedObject) in SwiftUI play better with value semantics. UIKit patterns (delegates, KVO) required explicit adaptation, mostly replacing delegate callbacks with functional-style closures.

Holistic Benefits: Beyond Speed

happy team, code collaboration, software maintainability, code review

Performance was our initial motivator, but value types brought deeper, lasting gains as our codebase matured.

Safer, More Predictable Code

Less shared mutable state cut support ticket volume from elusive data bugs. Our PR review time dropped—structs empowered test-driven patterns, created rare regression bugs, and reduced codebase fragility.

Simpler Diffing and State Management

In state-centric architectures (think Redux or The Composable Architecture), working with value types simplified entire reducer setups, data flows, and made undo/redo logic nearly trivial.

Easier Testing

Immutability and value semantics meant test set-up was as easy as instantiating fresh, stateless models. No more wrestling with mocks to control hidden reference graphs.

Secure Foundation for SwiftUI Adoption

As we explored SwiftUI, we found the ecosystem intrinsically structured around value types—our refactored models and state slots could be dropped into the new architecture with comfort.

Advice for iOS Devs: How to Maximize the Value of Structs

developer tips, Swift advice, keyboard, learning Swift, tips list

If you’re reading this because your app feels slow or chaotic, adopting structs isn’t just a craft flex—it’s an engineering edge. My practical roadmap:

  1. Profile before-and-after: Use Time Profiler and Allocations in Instruments; compare on real devices.
  2. Refactor test modules individually first: Pick areas with fewest dependencies (like models for view-only screens).
  3. Automate linting for new class-type data models: SwiftLint or Danger can enforce preference.
  4. Teach your team: Share best practices and pitfalls around value semantics and migration challenges.
  5. Adopt code-generation with tools like Sourcery: For large models, automate Equatable, Hashable, and extensions to keep value types ergonomic.

Looking Forward: Structuring for Performance

future, app performance, successful app, codebase building, roadmap

Structs are not a cure-all, but for data-rich iOS apps, they are transformative. We’ve found not only doubled speed, but the remove of a wide class of concurrency, testing, and refactoring headaches. Our codebase is leaner, safer, and ready for the ever-evolving Swift and SwiftUI world—all while users experience the fluid, buttery UI that keeps them coming back.

If you’re hitting scaling pains and sluggish performance, consider the bold but pragmatic value-type approach. Double your speed, halve your stress—and let your iOS app shine.

Rate the Post

Add Comment & Review

User Reviews

Based on 0 reviews
5 Star
0
4 Star
0
3 Star
0
2 Star
0
1 Star
0
Add Comment & Review
We'll never share your email with anyone else.