# Swift String Performance and Copy-on-Write Explained

Source: https://devdocs10.com/articles/swift/swift-string-performance-cow  
By Murat Bo · Updated 2026-10-17 · DevDocs10 (https://devdocs10.com)

Swift strings are powerful, Unicode-correct, and designed to feel safe and convenient, but they also have performance rules that are easy to misunderstand. In this article, you will learn how Swift `String` values are stored, what copy-on-write means in practice, which string operations are cheap, which can become expensive, and how to write string-heavy code that stays fast and predictable.

> **Quick answer:** Swift `String` uses copy-on-write, which means assigning a string to another variable usually does not copy its characters immediately. A real copy is typically made only when one of the shared values is mutated, so many assignments are cheap but repeated edits, indexing, and unnecessary conversions can still hurt performance.

> **Difficulty:** Intermediate
>
> **Helpful to know first:** You will understand this better if you already know basic Swift syntax, the difference between `let` and `var`, and that Swift strings are collections of characters rather than simple byte arrays.

## 1. What Is String Performance and Copy-on-Write?

String performance is about how much time and memory different string operations use. Copy-on-write is the storage strategy that lets Swift keep value semantics while avoiding unnecessary full copies.

- `String` is a value type in Swift, so it behaves like an independent value.
- Even so, Swift often shares underlying storage between string values until one of them changes.
- This delayed copying behavior is called copy-on-write, often shortened to COW.
- Reading a string is usually cheap; mutating a shared string may trigger a real copy.
- Some operations are expensive not because of copying, but because Swift strings are Unicode-aware.

That last point matters a lot. Developers often assume string performance is only about whether copying happens. In practice, Swift string cost also depends on Unicode correctness, character boundaries, and conversions between `String`, `Substring`, and other representations.

> A common confusion is thinking that `String` behaves like an array of bytes with constant-time indexing. It does not. Swift strings prioritize correct Unicode behavior, so many seemingly simple operations require more work than they would in a byte-based string model.

## 2. Why String Performance and Copy-on-Write Matter

Strings are everywhere: parsing input, building messages, formatting data, loading files, processing JSON keys, handling search text, and generating user-visible output. Small inefficiencies can become noticeable when code runs often or handles large text.

You should care about string performance when:

- You process large text files or logs.
- You build strings repeatedly inside loops.
- You slice strings and keep those slices around.
- You frequently convert between string-related types.
- You need predictable memory usage in performance-sensitive code.

You usually do not need to micro-optimize every string operation. Swift's copy-on-write design already avoids many wasteful copies. The goal is to understand which operations are naturally efficient and which patterns create hidden costs.

## 3. Basic Syntax

### Shared storage until mutation

This example shows the central idea of copy-on-write. Two string variables can refer to the same underlying storage at first, but when one changes, Swift preserves value semantics by separating them.

```swift
var first = "Hello"
var second = first

second.append("!")

print(first)
print(second)
```

Output (tested with Swift 6.4):

```text
Hello
Hello!
```

Before `second` is mutated, Swift can often share storage. After the append, `first` still prints `Hello` and `second` prints `Hello!`. That is the visible behavior of copy-on-write.

### Strings are not random-access by integer index

Another core idea is that string performance depends on Unicode-aware indexing. You do not access characters with plain integers like `text[3]`.

```swift
let text = "Café"
let index = text.index(text.startIndex, offsetBy: 3)
print(text[index])
```

Output (tested with Swift 6.4):

```text
é
```

This code uses a proper string index rather than an integer. That design keeps character handling correct for Unicode, but it also means some indexing operations are not constant-time.

### Substring shares storage differently from a new String

Performance discussions about strings often involve `Substring`. A substring can reuse storage from the original string, which is efficient, but keeping it for a long time can also keep the original storage alive.

```swift
let message = "user:alice@example.com"
let colonIndex = message.firstIndex(of: ":")!
let namePart = message[message.index(after: colonIndex)...]
print(namePart)
```

Output (tested with Swift 6.4):

```text
alice@example.com
```

Here `namePart` is a `Substring`, not a full `String`. That is often efficient for short-lived slicing, but if you need to store it long-term, converting it to `String` may be the better memory choice.

## 4. Step-by-Step Examples

### Example 1: Assignment is usually cheap

Many beginners think assigning one string to another immediately duplicates all text. Copy-on-write means that is often not true.

```swift
let original = "Large report contents..."
let backup = original

print(original)
print(backup)
```

Output (tested with Swift 6.4):

```text
Large report contents...
Large report contents...
```

This kind of assignment is usually inexpensive because Swift can share storage internally. Since neither value is mutated, no separate copy is needed for correctness.

### Example 2: Mutation can trigger a copy

Once one shared string changes, Swift must ensure the other value stays unchanged.

```swift
var title = "Monthly Summary"
var editedTitle = title

editedTitle.append(" (Draft)")

print(title)
print(editedTitle)
```

Output (tested with Swift 6.4):

```text
Monthly Summary
Monthly Summary (Draft)
```

Appending to `editedTitle` may require new storage. Swift performs that copy only when it becomes necessary, which is exactly the optimization that copy-on-write provides.

### Example 3: Repeated concatenation in a loop

Building a string piece by piece is common, but the pattern you choose affects performance. Repeated concatenation can cause extra work.

```swift
var csv = ""

for i in 1...5 {
    csv += "Item \("+ String(i) + ")\n"
}

print(csv)
```

Output (tested with Swift 6.4):

```text
Item + String(i) + 
Item + String(i) + 
Item + String(i) + 
Item + String(i) + 
Item + String(i) +
```

This works, but repeated concatenation can reallocate storage multiple times as the string grows. For small data, it is fine. For larger workloads, collecting pieces and joining them is often better.

```swift
var lines: [String] = []

for i in 1...5 {
    lines.append("Item \("+ String(i) + ")")
}

let result = lines.joined(separator: "\n")
print(result)
```

Output (tested with Swift 6.4):

```text
Item + String(i) + 
Item + String(i) + 
Item + String(i) + 
Item + String(i) + 
Item + String(i) +
```

The second version often scales better because it avoids repeated growth of one large string in the tight loop.

### Example 4: Short-lived Substring vs stored String

Using a substring can be very efficient when you only need it briefly.

```swift
let path = "/users/alice/photos/image.jpg"
if let slash = path.lastIndex(of: "/") {
    let fileName = path[path.index(after: slash)...]
    print(fileName)
}
```

Output (tested with Swift 6.4):

```text
image.jpg
```

This is a good short-lived use of `Substring`. But if you plan to store `fileName` in a model or cache, convert it:

```swift
let storedFileName = String(fileName)
```

That creates an independent string so the full original path does not need to stay alive just because a tiny slice is being stored.

## 5. Practical Use Cases

- Parsing log lines where temporary slices are faster than creating many new full strings immediately.
- Building export data such as CSV or reports, where repeated appends can become expensive.
- Handling user input validation, where reading and checking strings is usually cheap but many transformations can add cost.
- Extracting pieces of URLs, file paths, or identifiers using `Substring` before converting only the values you keep.
- Reducing memory usage in text-processing tools by avoiding unnecessary copies of large source strings.

## 6. Common Mistakes

### Mistake 1: Assuming every assignment copies the full string

Because `String` is a value type, beginners often assume every assignment immediately duplicates all characters in memory.

**Problem:** This misunderstanding can lead to unnecessary manual optimization or avoiding clean code patterns out of fear that simple assignments are always expensive.

```swift
let source = "Very large text..."
let copy = source
```

**Fix:** Trust Swift's copy-on-write behavior for ordinary assignments. Focus optimization on repeated mutation, slicing, and conversion patterns instead.

```swift
let source = "Very large text..."
let copy = source
print(copy)
```

Output (tested with Swift 6.4):

```text
Very large text...
```

The corrected version works because simple assignment is usually cheap until mutation requires separate storage.

### Mistake 2: Keeping a Substring for too long

`Substring` is useful for temporary work, but it can keep the original string's storage alive.

**Problem:** If a tiny substring is stored long-term, the entire original string may remain in memory, which is wasteful when the source text is large.

```swift
let logEntry = "ERROR 2024-04-10 user=alice action=delete ... very long line ..."
let prefix = logEntry[..<logEntry.index(logEntry.startIndex, offsetBy: 5)]
```

**Fix:** Convert the substring to a standalone `String` when you need to store it beyond the local operation.

```swift
let logEntry = "ERROR 2024-04-10 user=alice action=delete ... very long line ..."
let prefixSlice = logEntry[..<logEntry.index(logEntry.startIndex, offsetBy: 5)]
let prefix = String(prefixSlice)
```

The corrected version works because the stored value now has its own independent string storage.

### Mistake 3: Repeatedly accessing positions by offset inside a loop

String offsets are not the same as array indexes. Recomputing indexes from the start repeatedly can be costly.

**Problem:** This code walks from the start of the string over and over, which can make repeated character access much slower than expected.

```swift
let word = "Performance"

for i in 0<word.count {
    let index = word.index(word.startIndex, offsetBy: i)
    print(word[index])
}
```

**Fix:** Iterate directly over the string's characters or reuse advancing indexes when possible.

```swift
let word = "Performance"

for character in word {
    print(character)
}
```

Output (tested with Swift 6.4):

```text
P
e
r
f
o
r
m
a
n
c
e
```

The corrected version works because it lets Swift iterate naturally over characters instead of recalculating offsets from the beginning each time.

### Mistake 4: Building one large string with endless `+=`

Appending with `+=` is fine for small tasks, but large loops can trigger many reallocations and copies as content grows.

**Problem:** Repeatedly extending the same string can become slower and more memory-intensive than collecting pieces and joining them once.

```swift
var output = ""

for i in 1...1000 {
    output += "Row \("+ String(i) + ")\n"
}
```

**Fix:** Gather the lines first, then join them into one final string.

```swift
var rows: [String] = []

for i in 1...1000 {
    rows.append("Row \("+ String(i) + ")")
}

let output = rows.joined(separator: "\n")
```

The corrected version works because it reduces repeated growth work on one continuously expanding string.

## 7. Best Practices

### Prefer direct iteration over character-by-offset access

If your goal is to inspect every character, direct iteration is simpler and usually more efficient than repeated offset calculations.

```swift
// Less preferred for full traversal
for i in 0<text.count {
    let index = text.index(text.startIndex, offsetBy: i)
    print(text[index])
}

// Preferred
for character in text {
    print(character)
}
```

This matters because Swift strings are not random-access collections in the simple integer-index sense.

### Use Substring for temporary work, String for storage

A good rule is: slice first, convert later only if needed.

```swift
// Temporary processing
let prefixSlice = text.prefix(10)
print(prefixSlice)

// Long-term storage
let savedPrefix = String(prefixSlice)
```

This practice balances performance and memory use. You avoid unnecessary copies during local processing, but you also avoid accidentally keeping a large original string alive.

### Build large outputs in stages

For larger generated text, collect parts before creating the final string.

```swift
let names = ["Ana", "Ben", "Chris"]
let lines = names.map { "Hello, \($0)!" }
let message = lines.joined(separator: "\n")
```

This is often clearer and scales better than repeatedly mutating the same string in a loop.

## 8. Limitations and Edge Cases

- Copy-on-write is an optimization detail that preserves value semantics, but you should not write code that depends on exactly when an internal copy happens.
- String indexing by offset may be more expensive than expected because Swift must respect Unicode grapheme cluster boundaries.
- `Substring` can improve short-term performance, but storing many substrings from large source strings can increase memory retention.
- Small strings may be optimized differently from large strings internally, so the cost profile of an operation can change with data size.
- Converting between `String`, `Substring`, arrays of characters, and UTF views can introduce additional work.
- If performance truly matters, measure with realistic input rather than assuming a specific string pattern is the bottleneck.

## 9. Practical Mini Project

Let’s build a small log-summary tool that extracts important information from log lines. It uses temporary slices for parsing, then converts only the final stored values into full strings.

```swift
let logs = [
    "INFO|alice|Login successful",
    "ERROR|bob|File not found",
    "WARNING|alice|Low disk space"
]

var summaries: [String] = []

for line in logs {
    let parts = line.split(separator: "|")
    
    if parts.count == 3 {
        let level = String(parts[0])
        let user = String(parts[1])
        let message = String(parts[2])
        
        summaries.append("[\(level)] user=\(user) message=\(message)")
    }
}

let report = summaries.joined(separator: "\n")
print(report)
```

Output (tested with Swift 6.4):

```text
[INFO] user=alice message=Login successful
[ERROR] user=bob message=File not found
[WARNING] user=alice message=Low disk space
```

This example is useful because `split` returns substring-like pieces efficiently for parsing, but the program converts the final saved values into full strings before storing them in the summaries array.

The design is performance-aware without becoming overly complex:

- Parsing avoids unnecessary early copying.
- Stored values become independent `String` instances.
- The final output is assembled once with `joined`.

## 10. Key Points

- Swift `String` uses copy-on-write, so assignment usually does not immediately copy all characters.
- A real copy is typically needed when one of the shared string values is mutated.
- String performance is affected by Unicode-aware indexing, not only by copying behavior.
- `Substring` is efficient for temporary slices but can retain large original storage if kept too long.
- Repeated concatenation in large loops can be slower than collecting parts and joining them once.
- Direct iteration over characters is often better than repeated offset-based indexing.

## 11. Practice Exercise

Create a program that takes a file path string, extracts the final path component, converts it to a standalone `String`, and prints an uppercase report line. Use a temporary slice for extraction and `joined` to build the final output from an array.

- Start with the path `"/projects/swift/report.txt"`.
- Find the last slash.
- Extract the file name as a slice first.
- Convert that slice to `String`.
- Create a final line like `"FILE: REPORT.TXT"`.

**Expected output:** `FILE: REPORT.TXT`

**Hint:** Use `lastIndex(of:)`, a slice range after the slash, `String(...)`, and an array with `joined()`.

```swift
let path = "/projects/swift/report.txt"

if let slashIndex = path.lastIndex(of: "/") {
    let fileSlice = path[path.index(after: slashIndex)...]
    let fileName = String(fileSlice)
    let parts = ["FILE:", fileName.uppercased()]
    let output = parts.joined(separator: " ")
    print(output)
}
```

Output (tested with Swift 6.4):

```text
FILE: REPORT.TXT
```

## 12. Final Summary

Swift string performance is shaped by two big ideas: copy-on-write storage and Unicode-correct string behavior. Copy-on-write means assigning a string is often cheap because Swift can share storage until a mutation happens. That gives you the safety of value semantics without paying for unnecessary copies up front.

At the same time, not all string costs come from copying. Indexing, slicing, repeated mutation, and conversions between `String` and `Substring` can all affect performance. If you remember to use substrings temporarily, convert them when storing, avoid heavy offset-based indexing, and build large outputs in stages, your string code will usually be both clean and efficient.

A strong next step is to study Swift `Substring`, string indexing, and Unicode views such as `utf8` and `unicodeScalars`. Those topics build directly on the performance ideas you learned here.
