Lists in Dart — Performance, Alternatives, and When Not to Use Them
A deep dive into Dart Lists: why they exist, their performance characteristics, when to use alternatives like Queue or typed data, and AI prompts to help you choose the right collection.
Why Lists exist — the OOP perspective
Before collections existed, programmers managed multiple items with numbered variables: item0, item1, item2. This approach doesn't scale. What if you need 1000 items? How do you iterate? How do you pass them to a function?
Lists solve this by providing a single abstraction that encapsulates the complexity of managing multiple items. The backing array, capacity management, and memory allocation are all hidden behind a clean interface.
This is OOP at its best: encapsulation (hiding implementation details), abstraction (simple interface), polymorphism (List<T> works with any type), and interface segregation (Dart's List implements Iterable, EfficientLengthIterable, and more).
Operation complexity — where Lists shine and struggle
Not all operations on a List are equal. Some are blazing fast; others can be painfully slow. Understanding complexity helps you write performant code.
Where Lists win: list[i] (O(1)), add() at end (amortized O(1)), iteration.
Where Lists lose: contains() (O(n)), remove(item) (O(n)), insert(0, x) (O(n)).
If you're frequently checking membership, consider a Set. If you need fast insertion at both ends, consider a Queue.
When NOT to use a List — better alternatives
Lists are versatile, but they're not always the best choice. Here's a decision tree to help you pick the right collection.
Queue — use when you need efficient add/remove at both ends (FIFO/LIFO patterns).
Typed Data Lists — use for large numeric arrays (Float64List, Int32List) for better memory efficiency and performance.
Set — use when you need unique items and fast membership testing.
LinkedList — rarely needed; only when you need O(1) splicing in the middle and can sacrifice random access.
Performance optimizations for Lists
When working with large Lists, small optimizations can make a big difference. Here are practical tips.
1. Pre-allocate capacity when you know the size:
// Slow: grows multiple times
var list = <int>[];
for (var i = 0; i < 10000; i++) {
list.add(i); // triggers resizing
}
// Fast: no resizing needed
var list = List<int>.filled(10000, 0);
for (var i = 0; i < 10000; i++) {
list[i] = i;
}
2. Use addAll() instead of repeated add():
// Slower
for (var item in otherList) {
myList.add(item);
}
// Faster — single operation
myList.addAll(otherList);
3. Avoid insert(0, x) in loops — it's O(n²) total:
// Terrible: O(n²)
for (var item in items) {
list.insert(0, item);
}
// Better: build normally, then reverse
var list = items.toList();
list = list.reversed.toList();
4. Use List.generate() for computed values:
// Creates a list of squares
var squares = List.generate(100, (i) => i * i);
Lists and parallel computations
Dart runs on a single-threaded event loop, but you can use Isolates for true parallel computation with Lists. The key challenge is that Isolates don't share memory.
import 'dart:isolate';
// Heavy computation in a separate isolate
Future<List<int>> processInParallel(List<int> data) async {
return await Isolate.run(() {
// This runs in a separate isolate
return data.map((x) => x * x).toList();
});
}
void main() async {
var data = List.generate(1000000, (i) => i);
var result = await processInParallel(data);
print('Processed ${result.length} items');
}
When to parallelise:
- CPU-bound operations on large Lists (>10k items)
- Complex transformations (encryption, compression, parsing)
- When you can afford the overhead of copying data to the isolate
When NOT to parallelise:
- Small Lists (overhead exceeds benefit)
- Simple operations (
map, filter on primitives)- When you need to share state (isolates can't share mutable state)
Typed data Lists — when performance is critical
For numerical data, Dart's typed data lists offer significant performance benefits. They store values directly in memory without boxing overhead.
import 'dart:typed_data';
// Standard List — each int is a boxed object
var normalList = <int>[1, 2, 3, 4, 5];
// Typed List — compact, contiguous memory
var typedList = Int32List.fromList([1, 2, 3, 4, 5]);
// Float64List for floating-point data
var floats = Float64List(1000);
for (var i = 0; i < 1000; i++) {
floats[i] = i * 0.1;
}
Available typed lists:
-
Int8List, Int16List, Int32List, Int64List-
Uint8List, Uint16List, Uint32List, Uint64List-
Float32List, Float64List
Benefits: Lower memory usage (no boxing), better cache locality, faster iteration, interop with native code (FFI).
Tradeoffs: Fixed size, limited to numeric types, slightly less convenient API.
List flavours — growable, fixed, unmodifiable, const
Dart has four flavours of List, each with different mutability characteristics.
// 1. Growable (default) — can add/remove/modify
var growable = [1, 2, 3];
growable.add(4); // OK
growable[0] = 10; // OK
// 2. Fixed-length — can modify, cannot resize
var fixed = List.filled(3, 0);
fixed[0] = 10; // OK
// fixed.add(4); // Error!
// 3. Unmodifiable — cannot modify or resize
var unmod = List.unmodifiable([1, 2, 3]);
// unmod[0] = 10; // Error!
// unmod.add(4); // Error!
// 4. Const — compile-time constant, shared
const constList = [1, 2, 3];
// Identical to all other const [1, 2, 3] literals
When to use each:
- Growable: Default choice for most use cases
- Fixed-length: When you know exact size and want to prevent accidental resizing
- Unmodifiable: Defensive copies in APIs, returning internal state safely
- Const: Static configuration data, truly immutable values
AI prompts — choosing the right List strategy
Use these prompts with AI assistants to help choose optimal List strategies for your use case.
Prompt 1 — Collection type selection:
I have a Dart application that needs to store [describe your data].
Operations I frequently perform:
- [list operations: add, remove, lookup, iterate, etc.]
- Approximate size: [number of items]
- Constraints: [memory, performance, thread-safety]
Should I use List, Set, Map, Queue, or typed data?
What are the tradeoffs for my specific use case?
Prompt 2 — Performance optimization:
Here is my Dart code that uses Lists:
[paste your code]
I'm experiencing [performance issue: slow, high memory, etc.].
The List size is approximately [size].
Analyze this code for List-related performance issues and
suggest specific optimizations. Consider:
- Pre-allocation strategies
- Alternative data structures
- Algorithm improvements
- Typed data lists if applicable
Prompt 3 — Parallel processing decision:
I need to process a List of [size] items in Dart.
Each item requires [describe computation].
Current execution time: [time].
Should I use Isolates for parallel processing?
If yes, provide an implementation using Isolate.run().
If no, explain why and suggest alternatives.
Prompt 4 — Memory optimization:
My Dart app stores [type of data] in Lists.
Total items: [count]
Current memory usage: [if known]
Suggest ways to reduce memory footprint while
maintaining acceptable performance. Consider typed data,
lazy loading, chunking, or structural changes.
Lists in Dart
Lists are the workhorse of Dart collections. This episode explores when to use them, when to avoid them, and how to optimise them for performance-critical applications.
Lists Deep Dive Quiz
7 questions
Test your understanding of Dart Lists, performance, and alternatives