How do I use sequences to optimize performance with large Kotlin collections?

In Kotlin, sequences let you process large collections lazily, which can reduce temporary allocations and improve performance for chained operations.

The problem with regular collections

Collection operations like map, filter, and flatMap are usually eager:

val result = users
    .filter { it.isActive }
    .map { it.email }
    .take(10)

With a List, Kotlin typically creates intermediate collections:

  1. one list after filter
  2. another list after map
  3. then takes 10 items

For small collections, this is fine. For large collections, it can waste memory and CPU.

Use asSequence()

Convert the collection to a sequence:

val result = users
    .asSequence()
    .filter { it.isActive }
    .map { it.email }
    .take(10)
    .toList()

Now each element flows through the pipeline one at a time:

user -> filter -> map -> maybe included in result

Also, because of take(10), processing can stop as soon as 10 matching items are found.

Example

data class User(
    val name: String,
    val email: String,
    val isActive: Boolean
)

val emails = users
    .asSequence()
    .filter { it.isActive }
    .map { it.email.lowercase() }
    .distinct()
    .take(100)
    .toList()

This avoids building large intermediate lists before getting the final 100 emails.

When sequences help

Sequences are usually useful when you have:

  • large collections
  • multiple chained operations
  • short-circuiting operations, such as:
    • take
    • first
    • firstOrNull
    • any
    • all
    • none
    • find

Example:

val firstAdminEmail = users
    .asSequence()
    .filter { it.isActive }
    .filter { it.role == "admin" }
    .map { it.email }
    .firstOrNull()

This stops as soon as the first matching user is found.

When sequences may not help

Sequences are not always faster. They can be slower for:

  • small collections
  • simple one-step operations
  • cases where you need the entire result anyway
  • performance-critical tight loops where sequence overhead matters

For example, this may not benefit much:

val names = users.map { it.name }

A plain collection operation is often fine here.

Common pattern

Use this pattern:

val result = largeList
    .asSequence()
    .filter { condition(it) }
    .map { transform(it) }
    .take(50)
    .toList()

The important parts are:

  • asSequence() starts lazy processing
  • intermediate operations stay lazy
  • terminal operations execute the pipeline

Terminal operations include:

toList()
count()
first()
firstOrNull()
sumOf { ... }
any { ... }
forEach { ... }

Rule of thumb

Use sequences when your pipeline is large, chained, and potentially short-circuited.

For small or simple transformations, prefer normal collection operations for readability and often better performance.