On this page25 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. Measure First
- 2. One Timing Is Not Enough
- 3. A Fair Micro-Benchmark
- 4. Choosing the Right Collection
- 5. StringBuffer Instead of + in Loops
- 6. Lazy Iterable vs Eager toList()
- 7. Avoid Repeated Work: Hoisting
- 8. Avoid Repeated Work: Caching
- 9. const Objects Avoid Allocations
- 10. Garbage Collection, Simply Explained
- 11. Two Common Leaks
- 12. JIT vs AOT: Measure the Difference
- 13. A Real-World Example
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 126 — Performance & Memory
| Category | 21 — Professional Dart Development |
| Lecture | 126 of 140 |
| Estimated video length | 9 min |
| Target | Dart 3.12.2 |
| Level | Advanced |
DevTools shows where a program is slow or growing. This lecture shows what to change — and how to measure that the change helped.
Contents
Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways
Every output in this lecture is real, produced on Dart 3.12.2 on one laptop. Timing and memory figures are approximate — yours will differ; the comparisons matter.
Prerequisites
Students should already understand, from Lectures 001–125:
Set,Map,putIfAbsent, lazyIterables (043, 047, 050) and records (078)StringBuffer,identical()andconstconstructors (018, 020, 055)Stopwatch(087) andStreamSubscription.cancel()(092–093)dart compile exe(113) and the DevTools CPU and Memory pages (116)
Connection With Previous Lectures
In Lecture 116 the CPU Profiler named a hot function and the Memory page showed a growing
list. Lecture 100 said measure with Stopwatch before optimizing. Two questions remain:
- How do you measure fairly?
- Once a function is to blame, what do you change?
Learning Objectives
By the end of this lecture you will be able to:
- Write a fair micro-benchmark with warm-up and repeats
- Choose a
Setover aListfor repeated lookups - Build long text with
StringBufferinstead of+ - Decide between a lazy
IterableandtoList() - Avoid repeated work by hoisting and caching
- Use
constobjects to avoid allocations - Explain garbage collection and recognize two common leaks
- Compare JIT (
dart run) and AOT (dart compile exe) by measuring
1. Measure First
Performance is how fast a program runs; memory is how much space its objects take. Both are easy to guess wrong, so profile to find the slow function (Lecture 116), then measure each fix.
Analogy — a race official uses a stopwatch, not opinion — and lets the runner warm up, then times several laps.
2. One Timing Is Not Enough
Time the same function five times in a row:
int sumMarks(List<int> marks) {
var total = 0;
for (var mark in marks) {
total += mark;
}
return total;
}
void main() {
var marks = List.generate(1000000, (i) => i % 101);
for (var run = 1; run <= 5; run++) {
var stopwatch = Stopwatch()..start();
sumMarks(marks);
print('Run $run: ${stopwatch.elapsedMicroseconds} µs');
}
}Run 1: 4296 µs
Run 2: 3466 µs
Run 3: 736 µs
Run 4: 736 µs
Run 5: 736 µsThe first runs are five times slower. dart run uses a JIT compiler (Lecture 001):
it starts with quickly-compiled code, then recompiles busy parts into faster machine code.
Run 1 measures the warm-up, not your code.
3. A Fair Micro-Benchmark
A micro-benchmark times one small piece of code. Do it fairly: warm up, repeat, average.
void benchmark(String label, void Function() action, {int runs = 20}) {
for (var i = 0; i < 5; i++) {
action(); // warm-up: not timed
}
var stopwatch = Stopwatch()..start();
for (var i = 0; i < runs; i++) {
action();
}
var average = stopwatch.elapsedMicroseconds / runs;
print('$label: ${average.toStringAsFixed(1)} µs per run');
}
// sumMarks from section 2
void main() {
var marks = List.generate(1000000, (i) => i % 101);
var total = 0;
benchmark('sumMarks', () => total = sumMarks(marks));
print('Total: $total');
}sumMarks: 744.5 µs per run
Total: 49999950Two runs gave 744.5 and 743.5. Later sections reuse it.
4. Choosing the Right Collection
List.contains checks items one by one; Set.contains jumps straight to the answer
(Lecture 047). With 10,000 items the difference is huge:
void main() {
var idList = List.generate(10000, (i) => 'S$i');
var idSet = idList.toSet();
var lookups = List.generate(1000, (i) => 'S${i * 10}');
var found = 0;
benchmark(
'List.contains',
() => found = lookups.where(idList.contains).length,
);
benchmark('Set.contains', () => found = lookups.where(idSet.contains).length);
print('Found: $found');
}List.contains: 16268.0 µs per run
Set.contains: 47.0 µs per run
Found: 1000Over 300 times faster. A Map lookup by key is just as fast.
5. StringBuffer Instead of + in Loops
Strings never change, so text += ... copies all the old text into a new string each
time. StringBuffer collects the pieces and joins them once:
void main() {
var text = '';
benchmark('String +', () {
text = '';
for (var i = 0; i < 10000; i++) {
text += 'Order $i\n';
}
});
benchmark('StringBuffer', () {
var buffer = StringBuffer();
for (var i = 0; i < 10000; i++) {
buffer.write('Order $i\n');
}
text = buffer.toString();
});
print('Length: ${text.length}');
}String +: 30527.0 µs per run
StringBuffer: 559.5 µs per run
Length: 108890For a few pieces, + is fine. In a loop, use a buffer.
6. Lazy Iterable vs Eager toList()
where is lazy (Lecture 043): it checks items only when asked. toList() is eager: it
checks everything now. Count the checks:
void main() {
var marks = List.generate(1000000, (i) => i % 101);
var checks = 0;
bool isTop(int mark) {
checks++;
return mark > 95;
}
var firstLazy = marks.where(isTop).first;
print('Lazy: $firstLazy after $checks checks');
checks = 0;
var firstEager = marks.where(isTop).toList().first;
print('Eager: $firstEager after $checks checks');
}Lazy: 96 after 97 checks
Eager: 96 after 1000000 checksBut laziness repeats the work on every read (Example 10 counts it). Stay lazy to read once
or stop early; call toList() to read several times.
7. Avoid Repeated Work: Hoisting
A value that does not change inside a loop should be calculated before it — this is called hoisting:
double average(List<int> marks) =>
marks.fold(0, (sum, m) => sum + m) / marks.length;
void main() {
var marks = List.generate(20000, (i) => i % 101);
var stopwatch = Stopwatch()..start();
var above = 0;
for (var mark in marks) {
if (mark > average(marks)) above++; // recalculated 20000 times
}
print('Inside loop: $above in ${stopwatch.elapsedMilliseconds} ms');
stopwatch.reset();
above = 0;
var avg = average(marks); // calculated once
for (var mark in marks) {
if (mark > avg) above++;
}
print('Hoisted: $above in ${stopwatch.elapsedMilliseconds} ms');
}Inside loop: 10098 in 1509 ms
Hoisted: 10098 in 0 ms20,000 averages of 20,000 marks is 400 million additions; hoisted, it is one average.
8. Avoid Repeated Work: Caching
When a slow function gets the same input again, remember its answer in a Map. This is
caching:
var calls = 0;
int slowShippingCost(String city) {
calls++;
var cost = 0;
for (var i = 0; i < 1000000; i++) {
cost = (cost + city.codeUnitAt(i % city.length)) % 300;
}
return 200 + cost;
}
final shippingCache = <String, int>{};
int shippingCost(String city) =>
shippingCache.putIfAbsent(city, () => slowShippingCost(city));
void main() {
var orders = ['Karachi', 'Lahore', 'Karachi', 'Karachi'];
for (var city in orders) {
print('$city: ${shippingCost(city)}');
}
print('Slow calls: $calls');
}Karachi: 362
Lahore: 486
Karachi: 362
Karachi: 362
Slow calls: 2Four orders, two slow calls. Cache only unchanging results.
9. const Objects Avoid Allocations
Each Grade('A', 80) allocates (reserves memory for) a new object. A const one is
created once and shared (Lecture 055):
class Grade {
const Grade(this.letter, this.minMark);
final String letter;
final int minMark;
}
void main() {
var grades = <Grade>[];
benchmark('new Grade', () {
grades = [for (var i = 0; i < 100000; i++) Grade('A', 80)];
});
benchmark('const Grade', () {
grades = [for (var i = 0; i < 100000; i++) const Grade('A', 80)];
});
print(identical(grades.first, grades.last));
}new Grade: 3091.6 µs per run
const Grade: 1161.8 µs per run
trueThe const list holds 100,000 references to one object.
10. Garbage Collection, Simply Explained
In Dart you never free memory by hand. The garbage collector (GC) finds objects nothing
can reach any more and reuses their memory. ProcessInfo.currentRss (dart:io) reports the
program's memory use:
import 'dart:io';
class Order {
Order(this.total);
final double total;
}
String megabytes() => '${ProcessInfo.currentRss ~/ (1024 * 1024)} MB';
final kept = <Order>[];
void main() {
print('Start: ${megabytes()}');
for (var round = 1; round <= 3; round++) {
for (var i = 0; i < 1000000; i++) {
Order(99.5 + i); // garbage straight away
}
print('Temporary round $round: ${megabytes()}');
}
for (var round = 1; round <= 3; round++) {
for (var i = 0; i < 1000000; i++) {
kept.add(Order(99.5 + i)); // still reachable
}
print('Kept round $round: ${megabytes()}');
}
}Start: 168 MB
Temporary round 1: 158 MB
Temporary round 2: 157 MB
Temporary round 3: 159 MB
Kept round 1: 193 MB
Kept round 2: 207 MB
Kept round 3: 294 MBTemporary orders: memory stays flat. Kept orders: memory climbs, because kept still
reaches every one. The GC frees only the unreachable.
11. Two Common Leaks
A memory leak is memory you no longer need but still reach. Two are common:
- A growing collection — Lecture 116's
historylist. Give it a maximum size. - An uncancelled subscription — a listener keeps its function, and all it uses, alive:
import 'dart:async';
final prices = StreamController<double>.broadcast();
void openPriceScreen(int number) {
prices.stream.listen((price) => print('Screen $number sees $price'));
}
Future<void> main() async {
for (var number = 1; number <= 3; number++) {
openPriceScreen(number); // the screen closes, but its listener stays
}
prices.add(99.5);
await Future.delayed(Duration.zero);
await prices.close();
}Screen 1 sees 99.5
Screen 2 sees 99.5
Screen 3 sees 99.5Three closed screens still react and stay in memory. The fix (Lecture 100): return the
StreamSubscription and cancel() it when the screen closes — see Example 9 and Question 9.
12. JIT vs AOT: Measure the Difference
Compile a prime counter and time both ways. The shell's time command reports the whole run:
// isPrime: the fast version from Lecture 116, section 9
void main() {
var stopwatch = Stopwatch()..start();
var count = 0;
for (var n = 2; n <= 5000000; n++) {
if (isPrime(n)) count++;
}
print('Primes: $count');
print('Work took ${stopwatch.elapsedMilliseconds} ms');
}$ dart compile exe primes.dart -o primes
Generated: <folder>/primes
$ time dart run primes.dart
Primes: 348513
Work took 811 ms
dart run primes.dart 0.99s user 0.06s system 80% cpu 1.302 total
$ time ./primes
Primes: 348513
Work took 671 ms
./primes 0.67s user 0.01s system 99% cpu 0.688 totalThe work is a little faster AOT (671 vs 811 ms); the big gap is start-up, since
dart run compiles first. Compiled, section 10's program printed Start: 13 MB, not
168 MB — no compiler is loaded. Develop with dart run; ship with dart compile exe.
13. A Real-World Example
Sara's shop lists VIP orders, stored as (customer, amount) records — slow with a List and
+, fast with a Set and StringBuffer:
String slowReport(List<(String, double)> orders, List<String> vips) {
var report = '';
for (var (customer, amount) in orders) {
if (vips.contains(customer)) report += '$customer: $amount\n';
}
return report;
}
String fastReport(List<(String, double)> orders, Set<String> vips) {
var report = StringBuffer();
for (var (customer, amount) in orders) {
if (vips.contains(customer)) report.writeln('$customer: $amount');
}
return report.toString();
}
void main() {
var customers = List.generate(2000, (i) => 'Customer $i');
var orders = [
for (var i = 0; i < 20000; i++) (customers[i % 2000], 100.0 + i % 50),
];
var vipList = customers.where((c) => c.endsWith('7')).toList();
var vipSet = vipList.toSet();
var slow = '';
var fast = '';
benchmark('Slow report', () => slow = slowReport(orders, vipList));
benchmark('Fast report', () => fast = fastReport(orders, vipSet));
print('Same result: ${slow == fast}');
}Slow report: 14222.4 µs per run
Fast report: 495.1 µs per run
Same result: trueSame reports, 28 times faster.
Code Examples
Example 1 — Timing One Operation
void main() {
var marks = List.generate(1000000, (i) => i * 37 % 101);
var stopwatch = Stopwatch()..start();
marks.sort();
stopwatch.stop();
print('Sorted in ${stopwatch.elapsedMilliseconds} ms');
print('Lowest ${marks.first}, highest ${marks.last}');
}Sorted in 119 ms
Lowest 0, highest 100Example 2 — Resetting a Stopwatch
void main() {
var stopwatch = Stopwatch()..start();
var squares = [for (var i = 0; i < 1000000; i++) i * i];
print('Build: ${stopwatch.elapsedMilliseconds} ms');
stopwatch.reset();
var total = squares.fold(0, (a, b) => a + b);
print('Sum $total: ${stopwatch.elapsedMilliseconds} ms');
}Build: 8 ms
Sum 333332833333500000: 4 msExample 3 — A Set for Membership
void main() {
var enrolled = {'Ali', 'Sara', 'Zayan'};
for (var name in ['Sara', 'Omar', 'Zayan']) {
print('$name enrolled: ${enrolled.contains(name)}');
}
}Sara enrolled: true
Omar enrolled: false
Zayan enrolled: trueExample 4 — A Receipt with StringBuffer
void main() {
var items = {'Notebook': 500, 'Pen': 50, 'Bag': 1500};
var receipt = StringBuffer('Receipt\n');
items.forEach((item, price) => receipt.writeln('$item: $price'));
receipt.write('Items: ${items.length}');
print(receipt);
}Receipt
Notebook: 500
Pen: 50
Bag: 1500
Items: 3Example 5 — Lazy take Stops Early
void main() {
var marks = List.generate(100000, (i) => i * 37 % 101);
var checked = 0;
var firstPasses = marks
.where((m) {
checked++;
return m >= 50;
})
.take(3);
print(firstPasses.toList());
print('Checked $checked of ${marks.length} marks');
}[74, 84, 57]
Checked 8 of 100000 marksExample 6 — const Lists Are Shared
void main() {
const a = ['Ali', 'Sara'];
const b = ['Ali', 'Sara'];
var c = ['Ali', 'Sara'];
print(identical(a, b));
print(identical(a, c));
}true
falseExample 7 — Student Marks: Hoisted Average
void main() {
var marks = {'Ali': 80, 'Sara': 92, 'Zayan': 67, 'Omar': 74};
var average = marks.values.reduce((a, b) => a + b) / marks.length; // once
var above = [
for (var entry in marks.entries)
if (entry.value > average) entry.key,
];
print('Average: $average');
print('Above average: $above');
}Average: 78.25
Above average: [Ali, Sara]Example 8 — Shopping Total: Cached Prices
var lookups = 0;
int fetchPrice(String product) {
lookups++; // imagine a slow database query here
return {'Notebook': 500, 'Pen': 50, 'Bag': 1500}[product]!;
}
final priceCache = <String, int>{};
int price(String product) =>
priceCache.putIfAbsent(product, () => fetchPrice(product));
void main() {
var cart = ['Pen', 'Notebook', 'Pen', 'Pen', 'Notebook'];
var total = 0;
for (var product in cart) {
total += price(product);
}
print('Total: $total');
print('Lookups: $lookups');
}Total: 1150
Lookups: 2Example 9 — Bank Balance: Cancel When Done
import 'dart:async';
Future<void> main() async {
var balance = StreamController<double>();
var subscription = balance.stream.listen((b) => print('Balance: $b'));
balance.add(1000);
balance.add(1250.5);
await Future.delayed(Duration.zero);
await subscription.cancel();
balance.add(900); // nobody listens any more
print('Subscription cancelled');
}Balance: 1000.0
Balance: 1250.5
Subscription cancelledExample 10 — Temperature: Convert Once, Reuse
void main() {
var celsius = [20.0, 25.0, 30.0];
var conversions = 0;
var fahrenheit = celsius.map((c) {
conversions++;
return c * 9 / 5 + 32;
}).toList(); // convert once, then reuse
print(fahrenheit);
print('Max: ${fahrenheit.reduce((a, b) => a > b ? a : b)}');
print('Conversions: $conversions');
}[68.0, 77.0, 86.0]
Max: 86.0
Conversions: 3Without toList(), printing and reduce each convert again: six conversions.
Common Beginner Mistakes
1. Timing one cold run — section 2's run 1 was five times slower than run 3.
2. Trusting exact numbers — timings change between runs. Compare ratios on one machine.
3. Optimizing without profiling — you speed up code that was never the problem.
4. Reading one lazy Iterable many times — each read repeats the work.
5. Forgetting to cancel — every listen needs a cancel() when you are done.
Best Practices
- Measure first; warm up, repeat, and label timings approximate.
- Write
constwherever a value is fixed. - Bound long-lived collections and cancel subscriptions.
- Benchmark the way you ship: AOT behaves differently from
dart run.
Mini Challenge
Zayan's word game checks the guesses mango, mangoo, peach, plum against the dictionary
apple, mango, grape, lemon, peach. Store the dictionary as a Set, build the result
with a StringBuffer, give 10 points per valid word, and end with Score: 20.
Try it yourself before opening the solution.
Show solution
void main() {
var dictionary = {'apple', 'mango', 'grape', 'lemon', 'peach'};
var guesses = ['mango', 'mangoo', 'peach', 'plum'];
var result = StringBuffer();
var score = 0;
for (var word in guesses) {
var valid = dictionary.contains(word);
if (valid) score += 10;
result.writeln('$word: ${valid ? 'valid' : 'not a word'}');
}
result.write('Score: $score');
print(result);
}mango: valid
mangoo: not a word
peach: valid
plum: not a word
Score: 20Practice Questions
Beginner
- Which class measures elapsed time, and which property gives microseconds?
- Why are the first runs of a function under
dart runslower? - Put
'spam','scam','fake'in aSet; print whether it has'scam'and'real'.
Application
- Build the lines
7 x 1 = 7to7 x 3 = 21with aStringBufferand print them. - How many checks does
[50, 95, 60, 99].where((m) => m > 90).firstmake? Prove it. - For marks
[80, 92, 67], print each as a percentage of the highest, calculating the highest only once. - Are two
const Duration(seconds: 1)identical? Two non-const ones?
Challenge
- Benchmark
List.containsagainstSet.containsfor 50,000 numbers and 500 lookups. - Ali, Sara and Zayan each open and close a score view on a broadcast stream; Zayan
reopens his. Make sure a score of
10reaches only that view. - Time section 12's program both ways. Give two reasons to ship the executable.
Practice Answers
Question 1 — Solution
Stopwatch; elapsedMicroseconds.
Question 2 — Solution
The JIT compiler optimizes busy code only after it has run a few times.
Question 3 — Solution
void main() {
var banned = {'spam', 'scam', 'fake'};
print(banned.contains('scam'));
print(banned.contains('real'));
}true
falseQuestion 4 — Solution
void main() {
var table = StringBuffer();
for (var i = 1; i <= 3; i++) {
table.writeln('7 x $i = ${7 * i}');
}
print(table.toString().trim());
}7 x 1 = 7
7 x 2 = 14
7 x 3 = 21Question 5 — Solution
void main() {
var marks = [50, 95, 60, 99];
var checks = 0;
var first = marks.where((m) {
checks++;
return m > 90;
}).first;
print('$first after $checks checks');
}95 after 2 checksQuestion 6 — Solution
void main() {
var marks = [80, 92, 67];
var highest = marks.reduce((a, b) => a > b ? a : b); // hoisted
for (var mark in marks) {
print('$mark is ${(mark / highest * 100).round()}% of the top mark');
}
}80 is 87% of the top mark
92 is 100% of the top mark
67 is 73% of the top markQuestion 7 — Solution
void main() {
print(identical(const Duration(seconds: 1), const Duration(seconds: 1)));
print(identical(Duration(seconds: 1), Duration(seconds: 1)));
}true
falseQuestion 8 — Solution
With section 3's benchmark function:
void main() {
var list = List.generate(50000, (i) => i);
var set = list.toSet();
var targets = List.generate(500, (i) => i * 100);
var hits = 0;
benchmark('List', () => hits = targets.where(list.contains).length);
benchmark('Set', () => hits = targets.where(set.contains).length);
print('Hits: $hits');
}List: 9315.0 µs per run
Set: 42.0 µs per run
Hits: 500Question 9 — Solution
import 'dart:async';
final scores = StreamController<int>.broadcast();
StreamSubscription<int> showScores(String player) =>
scores.stream.listen((s) => print('$player sees $s'));
Future<void> main() async {
for (var player in ['Ali', 'Sara', 'Zayan']) {
var subscription = showScores(player);
await subscription.cancel();
}
var live = showScores('Zayan');
scores.add(10);
await Future.delayed(Duration.zero);
await live.cancel();
await scores.close();
}Zayan sees 10Question 10 — Solution
Ours: about 1.3 s with dart run, 0.7 s for ./primes. The executable skips start-up
compiling and uses far less memory (and needs no SDK, Lecture 113).
Quick Revision
- Why warm up before timing?
- Why is
+=on a string slow in a loop? - What is hoisting? What is caching?
- Which objects can the garbage collector free?
Key Takeaways
- Measure first: warm up, repeat, average; timings are approximate.
SetandMaplookups stay fast as data grows;List.containsdoes not.- Build text in loops with
StringBuffer. - Lazy
Iterables stop early but repeat work on each read;toList()to reuse. - Hoist unchanging work out of loops; cache repeated slow results; share
constobjects. - The GC frees only unreachable objects; growing collections and uncancelled subscriptions leak.
- AOT executables start faster and use less memory than
dart run.
What's Next?
Lecture 127 — Dart Code Review. We now have habits for clean, organized, fast code. Next we learn to review code — ours and others' — before a change is accepted.