Lazy Iteration in Dart: sync vs async Explained with Benchmarks

Md. Tariqul Islam

14 July, 2026

cover image

Modern Flutter and Dart development relies heavily on iteration—working with lists, streams, large data sets, paginated APIs, and background processing.
Dart provides two powerful generator features to help developers build memory-efficient and lazy-loaded logic:

  • sync* (Synchronous Generators)
  • async* (Asynchronous Generators)

Both allow you to use the yield keyword to produce values one by one instead of constructing entire collections or streams upfront.

But when should you use which? Why not just return a List? What are the real benefits? And how do they perform in real-world scenarios?

Let’s break everything down.

What Are Generator Functions?

Generators allow functions to produce multiple values over time, instead of returning everything at once.

Dart gives you two types:

sync* – Synchronous Generator

Returns: Iterable<T>

The caller consumes values synchronously (pull-based).

				
					Iterable<int> numbersSync() sync* {
  for (int i = 0; i < 3; i++) {
    yield i;
  }
}

				
			

async* – Asynchronous Generator

Returns: Stream<T>

The caller listens to values asynchronously (push-based).

				
					Stream<int> numbersAsync() async* {
  for (int i = 0; i < 3; i++) {
    await Future.delayed(Duration(milliseconds: 500));
    yield i;
  }
}

				
			
When to Use sync* Instead of Returning a List?

The most common question:

“I can just return a list… Why do I need sync*?”

Here are the real advantages.

Memory Efficiency (Huge Lists)

Returning a list builds everything in memory first.

				
					List<int> buildHugeList() {
  final list = <int>[];
  for (var i = 0; i < 1e7; i++) {
    list.add(i);
  }
  return list;
}

				
			

This can crash for huge data.

Using sync*, items are generated on demand:

				
					
Iterable<int> hugeSequence() sync* {
  for (var i = 0; i < 1e7; i++) {
    yield i;
  }
}

				
			

No full data stored means no memory explosion
This is especially useful for:

  • Pagination
  • Large file processing
  • Database reading
  • Algorithms that generate large sequences

Laziness & Early Exit Optimization

If the caller only needs the first 5 items, sync* avoids computing the rest:

				
					for (var n in hugeSequence().take(5)) {
  print(n);
}

				
			

More Expressive Code

yield makes generators clean and readable

				
					Iterable<int> oddsUpTo(int n) sync* {
  for (int i = 1; i <= n; i += 2) {
    yield i;
  }
}

				
			
When to Use async*

Use async* when data arrives over time:

Real-world Flutter examples:

Pagination in an infinite scroll list

				
					Stream<List<Post>> fetchPosts() async* {
  int page = 1;
  while (true) {
    final posts = await api.fetchPage(page++);
    yield posts;
  }
}

				
			

Listening to sensor data

				
					Stream<double> accelerometerStream() async* {
  while (true) {
    yield await platform.getAccelerometerValue();
  }
}

				
			

File download progress

				
					Stream<double> downloadProgress() async* {
  for (var p = 0.0; p <= 1.0; p += 0.1) {
    await Future.delayed(Duration(milliseconds: 200));
    yield p;
  }
}

				
			

Bluetooth/Wi-Fi scanning

				
					Stream<Device> scanDevices() async* {
  while (true) {
    final device = await scanResult();
    yield device;
  }
}

				
			
Benchmarks (Dart Code + Results)

Benchmark #1 — Building 1,000,000 items

Comparing:

  • Returning a List
  • Using sync* generator

Code:

				
					import 'dart:math';

void main() {
  final sw = Stopwatch()..start();
  final list = List.generate(1000000, (i) => i);
  sw.stop();
  print("List generate: ${sw.elapsedMilliseconds} ms");

  final sw2 = Stopwatch()..start();
  final it = generateSync(1000000);
  final consumed = it.last;
  sw2.stop();
  print("sync* generate: ${sw2.elapsedMilliseconds} ms");
}

Iterable<int> generateSync(int n) sync* {
  for (var i = 0; i < n; i++) {
    yield i;
  }
}

				
			

Output (Typical):

List generate: 38 ms

sync* generate: 12 ms

 

sync* avoids allocating a full list, reducing memory usage.

Performance may vary, but sync* shines when not all values are consumed.

Benchmark #2 — Large sequence, but stop early

				
					final sw = Stopwatch()..start();
final firstFive = generateSync(10000000).take(5).toList();
sw.stop();
print(sw.elapsedMilliseconds);

				
			

Output: 0–1 ms

Because it stops after 5 yields.
A list version would take hundreds of milliseconds.

Benchmark #3 — async streaming (simulated API calls)

				
					Stream<int> loadData() async* {
  for (var i = 0; i < 5; i++) {
    await Future.delayed(Duration(milliseconds: 200));
    yield i;
  }
}

void main() async {
  final sw = Stopwatch()..start();
  await for (var v in loadData()) {}
  sw.stop();
  print(sw.elapsedMilliseconds);
}

				
			

Output: 1000–1020 ms (5 × 200ms delays)

Matches real async behavior
Perfect for progressive results

Summary Table
Feature      sync* async*
Returns      Iterable<T> Stream<T>
Execution      Synchronous, lazy Asynchronous, event-driven
Use Case      CPU-heavy, list-like, large dataset network, timers, sensors, I/O
Memory      Very efficient Efficient & event-driven
Best For      Algorithms, transformations, file parsing APIs, sockets, Bluetooth, events
When not to Use Generators
  • When you need random access: myList[1000]
  • When you need to mutate the collection
  • When the dataset is tiny
  • When you need to reuse an already-computed list many times
Final Thoughts

Both sync* and async* are powerful but often underused features of Dart. They can drastically improve your code’s:

  • Performance
  • Memory usage
  • Readability
  • Responsiveness

While sync* is memory efficient, it introduces iterator overhead. For small collections or when you need random access (list[index]), returning a List can actually be faster.

Generators are optimized for laziness, not raw speed.

Md. Tariqul Islam

14 July, 2026