Static vs Dynamic Dispatch in Swift - How Method Dispatch Really Works

February 21, 2026

Method dispatch is one of those low-level Swift concepts that quietly influences performance, architecture, and API design. Although most apps function perfectly without thinking about dispatch, understanding how Swift decides which method implementation to call helps you write more predictable, efficient, and maintainable code—especially when building frameworks, reusable components, or performance-sensitive features.

This article explains:

  • What dispatch means in Swift
  • The difference between static dispatch and dynamic dispatch
  • How structs, classes, protocols, and generics influence dispatch
  • How Swift decides which dispatch mechanism to use
  • Practical guidelines for designing APIs with dispatch in mind

The goal is not to optimize prematurely, but to build a correct mental model of how Swift executes your code.

What Is Dispatch in Swift?

In Swift, dispatch refers to how the compiler and runtime determine which function implementation to execute when a method is called.

Consider this example:

protocol Greeter {
    func greet() -> String
}

struct EnglishGreeter: Greeter {
    func greet() -> String {
        "Hello"
    }
}

let greeter: Greeter = EnglishGreeter()
print(greeter.greet())

At the call site (greeter.greet()), Swift must decide which concrete greet() implementation to run. That decision can be made in two ways:

  • At compile time → Static dispatch
  • At runtime → Dynamic dispatch

Understanding when each happens is key to understanding Swift’s performance and abstraction model.

Static Dispatch

Static dispatch means the compiler knows the exact method implementation at compile time and generates a direct call to that function. There is no runtime lookup.

Static Dispatch with Value Types

struct Counter {
    func increment(_ value: Int) -> Int {
        value + 1
    }
}

let counter = Counter()
print(counter.increment(10))

Because Counter is a value type and cannot be subclassed, Swift knows exactly which increment(_:) method to call. The compiler can inline and optimize the call aggressively.

This is one reason Swift encourages the use of structs and enums for modeling data and behavior.

Static Dispatch with final Classes

final class Logger {
    func log(_ message: String) {
        print(message)
    }
}

let logger = Logger()
logger.log("Hello")

Marking a class as final tells the compiler that the method cannot be overridden. This allows Swift to use static dispatch even for class methods.

Benefits of Static Dispatch

Static dispatch enables the compiler to:

  • Inline method calls
  • Remove abstraction overhead
  • Perform cross-function optimizations
  • Reduce binary size
  • Improve performance in hot paths

In many cases, the call disappears entirely after optimization.

Dynamic Dispatch

Dynamic dispatch means the method implementation is chosen at runtime based on the actual type of the instance. This is required when Swift cannot know the concrete implementation at compile time.

Dynamic dispatch enables polymorphism but introduces runtime overhead.

Dynamic Dispatch with Class Inheritance

class Animal {
    func speak() -> String {
        "..."
    }
}

class Dog: Animal {
    override func speak() -> String {
        "Woof"
    }
}

let animal: Animal = Dog()
print(animal.speak())

Here:

  • The static type is Animal
  • The runtime type is Dog

Swift must determine the correct method implementation at runtime.

This uses Swift’s class dispatch mechanism (virtual method tables).

Dynamic Dispatch with Protocol Existentials

protocol Storage {
    func save()
}

struct DiskStorage: Storage {
    func save() {
        print("Saved to disk")
    }
}

let storage: Storage = DiskStorage()
storage.save()

When a protocol is used as a type, Swift erases the concrete type information.

The method call is resolved at runtime using a lookup mechanism.

This flexibility allows you to write abstraction-oriented code, but comes with a small runtime cost.

Protocols, Generics, and Dispatch

Protocols in Swift can participate in either static or dynamic dispatch depending on how they are used.

Protocols as Generic Constraints (Static Dispatch)

protocol Greeter {
    func greet() -> String
}

struct EnglishGreeter: Greeter {
    func greet() -> String { "Hello" }
}

func greet<G: Greeter>(_ greeter: G) {
    print(greeter.greet())
}

Because the compiler specializes this function for each concrete type, method calls are statically dispatched. This allows the compiler to inline and optimize aggressively.

Protocols as Types (Dynamic Dispatch)

func greet(_ greeter: Greeter) {
    print(greeter.greet())
}

Here, the concrete type is erased. Swift must perform runtime lookup to determine which implementation to call.

How @objc and dynamic Change Dispatch

When you mark methods with @objc or dynamic, Swift opts into Objective-C runtime dispatch:

class Player: NSObject {
    @objc dynamic func play() {
        print("Playing")
    }
}

let player = Player()
player.play()

This uses:

  • Objective-C message sending
  • Runtime method lookup
  • KVO compatibility

This is slower than Swift v-table dispatch and should be used only when you need:

  • KVO
  • Method swizzling
  • Objective-C interoperability

Dispatch Behavior Summary

Pattern Dispatch Type
Generic constraint (<T: P>) Static dispatch
Protocol as a type (P) Dynamic dispatch
Struct / enum methods Static dispatch
Final class methods Static dispatch
Overridable class methods Dynamic dispatch

How Swift Chooses Which Dispatch to Use

Swift follows a few general rules:

  • If the method cannot be overridden, Swift prefers static dispatch
  • If the method can be overridden, Swift uses dynamic dispatch
  • If type information is erased (protocol existentials), Swift uses dynamic dispatch
  • If the compiler can specialize the call (generics), Swift prefers static dispatch

Swift is designed to favor static dispatch whenever it is safe to do so.

Practical Design Guidelines

Prefer Static Dispatch When

  • Writing performance-sensitive code
  • Implementing algorithms and transformations
  • Designing libraries and reusable components
  • Working inside tight loops

Prefer:

  • Structs and enums
  • Generics
  • final classes when subclassing is not intended

Prefer Dynamic Dispatch When

  • Modeling polymorphic domain behavior
  • Designing plugin-style systems
  • Abstracting over multiple runtime implementations
  • Building dependency injection boundaries

Dynamic dispatch improves flexibility and architecture clarity.

Common Misconceptions

"Swift always uses static dispatch"

Swift prefers static dispatch, but falls back to dynamic dispatch when runtime polymorphism is required.

“Dynamic dispatch is slow and should be avoided”

Dynamic dispatch is slightly slower than static dispatch, but the cost is usually negligible in real applications. Architecture clarity often matters more than micro-optimizations.

“Protocols are slow”

Protocols are only dynamically dispatched when used as types.

Protocols used with generics are statically dispatched and highly optimized.

Final Thoughts

Dispatch is a foundational concept in Swift’s performance and abstraction model.

Understanding how and when Swift chooses static versus dynamic dispatch helps you:

  • Design better APIs
  • Avoid accidental performance costs
  • Build more predictable architectures
  • Reason about optimization behavior

You don’t need to optimize every call site—but knowing how dispatch works lets you make intentional design choices instead of accidental ones.

Thank you for reading. If you have any questions, feel free to follow me on X and send me a DM. If you enjoyed this article and would like to support my work, Buy me a coffee ☕️