Boost Dart String Performance with StringBuffer: A Practical Production Tip
Why String Concatenation Can Be a Hidden Performance Killer
When I first started shipping Dart apps to production, I often used the simple += operator to build strings from lists of data, log messages, or UI labels. At first glance everything worked fine, but as the app grew and the amount of concatenated text increased, I started seeing longer GC pauses and slower UI frames. The culprit was the way Dart handles immutable strings: each += creates a new String instance, copies the old content, and discards the old one. In a tight loop or when assembling a large block of text (think CSV exports, HTML snippets, or formatted log entries), the overhead quickly adds up.
The solution is StringBuffer. It provides a mutable buffer that internally uses a list of string chunks, appending to it in O(1) time and only materializing the final string when you call toString(). This reduces allocations and gives you predictable performance, especially when the number of concatenations is high.
When to Reach for StringBuffer
Any scenario where you repeatedly modify a string is a good candidate. Typical use‑cases include:
- Building log messages that combine timestamps, levels, and arbitrary data.
- Generating CSV or JSON output from a collection of objects.
- Assembling HTML or template strings in a web view.
- Formatting numbers, dates, or localized strings in a loop.
If you find yourself writing something like
String result = '';
for (var item in items) {
result += 'Item: $item\n';
}
or chaining multiple += operations in a method, it’s time to switch to StringBuffer.
A Practical Example: Building a CSV Report
Imagine we need to export a list of Employee objects to CSV for an HR dashboard. The naive approach would be:
class Employee {
final String name;
final int salary;
Employee(this.name, this.salary);
}
List employees = [
Employee('Alice', 75000),
Employee('Bob', 82000),
Employee('Charlie', 90000),
];
String buildCsvNaive(List employees) {
String csv = 'Name,Salary\n';
for (final emp in employees) {
csv += '${emp.name},${emp.salary}\n';
}
return csv;
}
Each iteration creates a new String, and the final GC has to clean up many intermediate objects. The same output can be produced more efficiently with a StringBuffer:
String buildCsvWithBuffer(List employees) {
final buffer = StringBuffer();
buffer.write('Name,Salary\n');
for (final emp in employees) {
buffer.write('${emp.name},${emp.salary}\n');
}
return buffer.toString();
}
Notice the pattern: we create a single buffer, call write (or writeln) for each line, and only at the end do we materialize the final string. The buffer internally uses a list of chunks, so appending is cheap, and the final string is built in one go.
You can also leverage the buffer’s convenience methods:
final buffer = StringBuffer()
..write('Header,Value\n')
..write('Count,${items.length}\n')
..writeAll(items, ',\n')
..write('\n');
The cascade operator (..) makes the code read like a series of sentences while still reusing the same buffer.
Best Practices and Gotchas
- Use
writelnfor line breaks. It adds a platform‑specific newline automatically, sparing you from manually adding\n. - Prefer
writeAllfor iterating over collections. It internally loops but avoids repeated method calls for each element, giving a small performance boost. - Remember that
StringBufferis not thread‑safe. In a multi‑threaded environment (e.g., isolates) each isolate should have its own buffer. - Don’t over‑engineer. For a handful of concatenations (<(5) the cost difference is negligible, and the extra allocation for a buffer may even be slower. Use the buffer when you have a clear loop or a large number of pieces.
One subtle pitfall is mixing buffer and string interpolation. If you need to embed a variable that itself is a large string, consider whether you really need to append it in the buffer or if you can build it separately first. The buffer shines when you are building the *structure* of the text, not when you are repeatedly swapping in large pre‑built blocks.
Wrap‑Up
StringBuffer is a simple yet powerful Dart utility that many developers overlook until performance issues surface. By replacing repetitive += operations with a mutable buffer, you cut down on unnecessary allocations, reduce GC pressure, and keep your code readable. Whether you are generating logs, CSV files, or UI templates, reaching for StringBuffer early can save you from subtle bottlenecks later in production.
Next time you find yourself concatenating strings in a loop, pause for a moment and ask: is this the best way to build the final text? If the answer is “probably not,” let a StringBuffer do the heavy lifting. Your app—and its users—will thank you.