How to Avoid Unnecessary Clones in Rust

Prevent performance issues in Rust by using references and iterators to borrow data instead of creating expensive copies.

The copy-paste trap

You write a function that takes a Vec<String>. Inside, you need to pass those strings to three different helper functions. Your instinct is to call .clone() on the vector before each call. The code compiles. The tests pass. Then you run a memory profiler and watch your heap usage triple. The CPU spends more time copying bytes than processing them. You just fell into the clone trap.

Rust makes cloning explicit because it is expensive. Every call to .clone() on a collection or string allocates new memory and copies data byte by byte. In Python or JavaScript, copying a reference is cheap and copying an object is often hidden behind the scenes. Rust forces you to choose. You either duplicate the data, or you borrow it. Borrowing is almost always the right answer.

Ownership in Rust means exactly one variable controls the lifecycle of a value. When that variable goes out of scope, the memory is reclaimed. If you pass an owned value to a function, ownership moves. The caller can no longer use it. Cloning solves this by creating a second owner, but it pays for that convenience with allocation and copy overhead.

References solve it without the overhead. A reference is a pointer to existing data paired with a set of rules enforced by the borrow checker. You hand the pointer to a function. The function reads or modifies the data. When the function returns, the pointer disappears. The original owner never loses control, and no new memory is allocated.

Think of it like a library system. Cloning is photocopying an entire book so two people can read it at once. Borrowing is checking out the book with a due date and a rule that says no writing in the margins. The library keeps the original. The reader gets access. No extra paper is wasted.

Stop reaching for .clone() until you have measured the cost of borrowing.

The fat pointer reality

Here is the smallest case: a function that calculates string lengths without taking ownership.

/// Calculates the total length of strings in a slice without taking ownership.
fn total_length(items: &[String]) -> usize {
    let mut sum = 0;
    // Iterate over references to avoid moving or cloning the strings.
    for item in items {
        sum += item.len();
    }
    sum
}

fn main() {
    let data = vec!["hello".to_string(), "world".to_string()];
    // Pass a reference to the vector. The function borrows it.
    let result = total_length(&data);
    println!("Total: {}", result);
    // data is still fully usable here because we never gave away ownership.
    println!("First item: {}", data[0]);
}

The compiler treats &[String] as a fat pointer: a memory address plus a length. Copying that pointer takes a single CPU instruction. Copying the actual strings takes time proportional to their size. The difference scales dramatically as your data grows. When you pass &data, Rust does not copy the vector. It copies the pointer and the length. The function reads directly from the original heap allocation. This is why borrowing feels instant while cloning feels heavy.

Convention aside: always prefer &[T] or &str in function parameters unless you genuinely need to take ownership. It reduces allocations, improves cache locality, and matches the standard library patterns. The community calls this "borrowing at the boundary."

Measure the pointer, not the payload.

The iterator trap

Collections in Rust expose three ways to iterate. Each one dictates what happens to the data inside the loop. Picking the wrong one is the most common source of accidental clones.

The first is .into_iter(). This consumes the collection. It moves each element out of the container and into the loop variable. You get owned values. The original collection is gone.

The second is .iter(). This borrows the collection. It yields &T for each element. You get references. The collection stays intact.

The third is .iter_mut(). This borrows the collection mutably. It yields &mut T. You can modify elements in place. The collection stays intact.

Here is how they behave side by side.

/// Demonstrates the three iterator behaviors on a vector.
fn iterator_demo() {
    let numbers = vec![10, 20, 30];

    // into_iter consumes the vector. numbers is moved and cannot be used after this loop.
    for n in numbers.clone() {
        println!("Owned: {}", n);
    }

    // iter borrows the vector. Yields &i32. No allocation, no move.
    for n in &numbers {
        println!("Borrowed: {}", n);
    }

    // iter_mut borrows mutably. Yields &mut i32. Allows in-place modification.
    for n in &mut numbers {
        *n *= 2;
    }
    println!("Modified: {:?}", numbers);
}

Notice the numbers.clone() in the first loop. I added it deliberately so the code compiles. Without it, numbers would be moved, and the subsequent loops would fail with a use-after-move error. In real code, you rarely need .into_iter() unless you are intentionally draining a collection or transferring ownership to another thread. Default to .iter() or .iter_mut(). The & and &mut prefixes on the collection automatically call .iter() and .iter_mut() under the hood. You do not need to type the method name explicitly.

Let the & prefix do the heavy lifting. You rarely need to spell out .iter().

Real-world refactoring

Consider a function that filters a list of user records and formats them for display. A common beginner pattern looks like this.

/// Formats user names, but clones unnecessarily.
fn format_users_bad(users: Vec<User>) -> Vec<String> {
    let mut result = Vec::new();
    for user in users {
        // Cloning the name string before formatting wastes memory.
        let name = user.name.clone();
        result.push(format!("[{}] {}", user.id, name));
    }
    result
}

struct User {
    id: u32,
    name: String,
}

The function takes ownership of the entire vector. It clones each name. It builds a new vector of formatted strings. The caller loses the original data, and the heap fills with duplicate allocations.

Refactor it to borrow.

/// Formats user names by borrowing the input slice.
fn format_users_good(users: &[User]) -> Vec<String> {
    // Map over references. The closure borrows each user.
    users.iter()
        .map(|user| format!("[{}] {}", user.id, user.name))
        .collect()
}

The signature changes from Vec<User> to &[User]. Slices are the idiomatic input type for read-only collections. They accept Vec, arrays, and any type that implements Deref or can be coerced to a slice. The closure borrows user, so user.name is accessed as a reference. format! accepts &str and &String seamlessly. No clones. No moves. The caller keeps their data.

Convention aside: write &str instead of &String in function signatures whenever possible. &str is a primitive string slice that strips the length and capacity metadata from the String header. It makes your API accept both String and string literals without extra coercion steps.

Take slices at the boundary. Hand back owned data only when the caller explicitly asks for it.

When the borrow checker draws a line

You will hit walls when you try to borrow too aggressively. The borrow checker enforces two hard rules: you cannot have a mutable reference while immutable references exist, and you cannot return a reference to data that will be dropped.

If you try to store a reference inside a struct that outlives the source data, the compiler rejects you with E0515 (cannot return value referencing local variable). The data you pointed to dies at the end of the function. The reference becomes a dangling pointer. Rust refuses to compile it.

If you try to move data out of a borrowed collection, you get E0507 (cannot move out of borrowed content). You cannot extract an owned String from a &Vec<String> because the vector still owns it. You must clone it, or change your design to work with references.

Here is what that wall looks like in practice.

/// Attempts to extract owned strings from a borrowed slice.
fn broken_extraction(items: &[String]) -> Vec<String> {
    // This fails to compile. items is borrowed, so we cannot move the strings out.
    // The compiler emits E0507: cannot move out of borrowed content.
    items.to_vec()
}

The fix is usually to change the return type to Vec<&String> or Vec<&str>, or to accept that cloning is the price of transferring ownership. Sometimes you need Cow<str> (Clone on Write) to handle both borrowed and owned strings in the same API. That is a specialized tool for parsing and serialization. Stick to references until you actually need the flexibility. Cow adds a runtime branch to check whether the data is borrowed or owned. Use it only when profiling shows the branch is cheaper than the clone you are avoiding.

When the compiler says you cannot move, it is protecting you from a dangling pointer. Change the signature, not the safety rules.

Picking the right tool

Use &T or &[T] when you only need to read data and the source outlives the function call. Use &mut T or &mut [T] when you need to modify data in place without reallocating. Use .iter() and .iter_mut() when looping over collections to guarantee borrowing instead of moving. Use .into_iter() when you intentionally want to consume a collection and transfer ownership of its elements. Use .clone() when you must create a second owner, such as storing data across thread boundaries or caching results that outlive the original source. Use Cow<T> when an API must accept both borrowed and owned data without forcing the caller to decide upfront.

Trust the borrow checker. It usually has a point.

Where to go next