How to Efficiently Build Strings in Rust

Use `String::with_capacity()` to pre-allocate memory when you know the approximate final size, or rely on `String`'s internal growth strategy for unknown sizes, as it already doubles capacity efficiently to minimize reallocations.

Use String::with_capacity() to pre-allocate memory when you know the approximate final size, or rely on String's internal growth strategy for unknown sizes, as it already doubles capacity efficiently to minimize reallocations. Avoid concatenating strings with + in loops, which creates a new allocation on every iteration, and instead use push_str() or format! for single-pass construction.

For a loop where you know the total length, pre-allocating prevents multiple memory reallocations:

let mut result = String::with_capacity(1000); // Pre-allocate for ~1000 chars

for i in 0..100 {
    result.push_str(&format!("Item {}\n", i));
}

If the final size is unknown but you are building from many small parts, String handles growth automatically, but you should still avoid + in loops. Here is the inefficient pattern to avoid versus the efficient push_str approach:

// ❌ Inefficient: Allocates a new String on every iteration
let mut bad = String::new();
for i in 0..1000 {
    bad = bad + &format!("Line {}\n", i); 
}

// ✅ Efficient: Reuses the same buffer
let mut good = String::new();
for i in 0..1000 {
    good.push_str(&format!("Line {}\n", i));
}

For complex formatting where you need to combine multiple values, format! is generally preferred over manual pushing because it handles internal capacity estimation well for a single call. However, if you are constructing a massive string from many format! calls inside a loop, combine pre-allocation with push_str:

let items: Vec<&str> = vec!["a", "b", "c", "d"];
let total_len: usize = items.iter().map(|s| s.len() + 1).sum(); // Estimate length

let mut builder = String::with_capacity(total_len);
for item in items {
    builder.push_str(item);
    builder.push(',');
}

When dealing with very large strings or streaming data where you don't want to hold the entire result in memory at once, consider using std::io::Write with a Vec<u8> or a custom writer, but for standard application logic, String with pre-allocation is the standard high-performance approach. The key is ensuring the buffer grows in large chunks rather than byte-by-byte or small increments.