I'm encountering a performance issue while trying to render an audio waveform in Jetpack Compose. I am using a LazyRow to display a waveform with 10,000+ items, where each item corresponds to a frequency value that updates frequently.
The issue is that as the frequency data updates (often every second), the entire LazyRow is redrawn, causing noticeable lag and slow performance, especially when there are many items in the list. I am using the drawBehind modifier to render the waveform, but it seems that drawing all items each time the frequency changes is not efficient enough.
What I've Tried:
I am using a LazyRow to display the frequencies as individual boxes.
I use drawBehind to render the waveform for each frequency value.
The frequencies list is updated frequently, and the LazyRow is scrollable
@Composable
fun AudioFrequencyUI(
frequenciesList: List<Float>,
waveWidth: Dp = 4.dp,
waveHeight: Dp = 80.dp,
upperStrokeWidth: Float = 8f,
lowerStrokeWidth: Float = 6f,
upperWaveColor: Color = Color.Yellow,
lowerWaveColor: Color = Color.Yellow,
gapSize: Dp = 4.dp,
minMagnitude: Float = 10f,
contentPadding: PaddingValues = PaddingValues(0.dp),
modifier: Modifier = Modifier
) {
val frequencies = remember { mutableStateListOf<Float>() }
val listState = rememberLazyListState()
LaunchedEffect(frequencies.size) {
if (frequencies.isNotEmpty()) {
listState.scrollToItem(frequencies.size - 1)
}
}
LaunchedEffect(frequenciesList) {
frequencies.clear()
frequencies.addAll(frequenciesList)
delay(1000)
}
LazyRow(
state = listState,
horizontalArrangement = Arrangement.spacedBy(gapSize),
modifier = modifier
.wrapContentWidth()
.height(waveHeight),
contentPadding = contentPadding
) {
itemsIndexed(frequencies, key = { index, _ -> index }) { index, magnitude ->
Box(
modifier = Modifier
.width(waveWidth)
.height(waveHeight)
) {
val maxMagnitude = frequencies.maxOrNull() ?: 1
Spacer(modifier = Modifier
.fillMaxSize()
.drawBehind {
var scaledMagnitude = magnitude / maxMagnitude.toFloat() * size.height / 2
scaledMagnitude = scaledMagnitude.coerceAtLeast(minMagnitude)
val halfHeight = size.height / 2
drawLine(
color = upperWaveColor,
start = Offset(size.width / 2, halfHeight),
end = Offset(size.width / 2, halfHeight - scaledMagnitude),
strokeWidth = upperStrokeWidth
)
drawLine(
color = lowerWaveColor,
start = Offset(size.width / 2, halfHeight),
end = Offset(size.width / 2, halfHeight + scaledMagnitude),
strokeWidth = lowerStrokeWidth
)
})
}
}
}
}
I'm encountering a performance issue while trying to render an audio waveform in Jetpack Compose. I am using a LazyRow to display a waveform with 10,000+ items, where each item corresponds to a frequency value that updates frequently.
The issue is that as the frequency data updates (often every second), the entire LazyRow is redrawn, causing noticeable lag and slow performance, especially when there are many items in the list. I am using the drawBehind modifier to render the waveform, but it seems that drawing all items each time the frequency changes is not efficient enough.
What I've Tried:
I am using a LazyRow to display the frequencies as individual boxes.
I use drawBehind to render the waveform for each frequency value.
The frequencies list is updated frequently, and the LazyRow is scrollable
@Composable
fun AudioFrequencyUI(
frequenciesList: List<Float>,
waveWidth: Dp = 4.dp,
waveHeight: Dp = 80.dp,
upperStrokeWidth: Float = 8f,
lowerStrokeWidth: Float = 6f,
upperWaveColor: Color = Color.Yellow,
lowerWaveColor: Color = Color.Yellow,
gapSize: Dp = 4.dp,
minMagnitude: Float = 10f,
contentPadding: PaddingValues = PaddingValues(0.dp),
modifier: Modifier = Modifier
) {
val frequencies = remember { mutableStateListOf<Float>() }
val listState = rememberLazyListState()
LaunchedEffect(frequencies.size) {
if (frequencies.isNotEmpty()) {
listState.scrollToItem(frequencies.size - 1)
}
}
LaunchedEffect(frequenciesList) {
frequencies.clear()
frequencies.addAll(frequenciesList)
delay(1000)
}
LazyRow(
state = listState,
horizontalArrangement = Arrangement.spacedBy(gapSize),
modifier = modifier
.wrapContentWidth()
.height(waveHeight),
contentPadding = contentPadding
) {
itemsIndexed(frequencies, key = { index, _ -> index }) { index, magnitude ->
Box(
modifier = Modifier
.width(waveWidth)
.height(waveHeight)
) {
val maxMagnitude = frequencies.maxOrNull() ?: 1
Spacer(modifier = Modifier
.fillMaxSize()
.drawBehind {
var scaledMagnitude = magnitude / maxMagnitude.toFloat() * size.height / 2
scaledMagnitude = scaledMagnitude.coerceAtLeast(minMagnitude)
val halfHeight = size.height / 2
drawLine(
color = upperWaveColor,
start = Offset(size.width / 2, halfHeight),
end = Offset(size.width / 2, halfHeight - scaledMagnitude),
strokeWidth = upperStrokeWidth
)
drawLine(
color = lowerWaveColor,
start = Offset(size.width / 2, halfHeight),
end = Offset(size.width / 2, halfHeight + scaledMagnitude),
strokeWidth = lowerStrokeWidth
)
})
}
}
}
}
Share
Improve this question
asked Mar 17 at 2:03
Santhosh KumarSanthosh Kumar
5611 silver badge11 bronze badges
1 Answer
Reset to default 0I had a similar problem with drawing large amounts of custom graphs in a lazy list. I suggest using a canvas for drawing instead of the drawBehind modifier.
Use drawBehind
for simple static or low frequency updates, like basic lines, rectangles, or occasional UI updates.
Use canvas
for high performance rendering, especially for real-time updates like animations, waveforms, or complex drawing logic. Also use canvas when drawing large or multiple complex elements, as it is more optimized for batched draw operations.
Your waveform in a canvas might look like this:
Canvas(modifier = Modifier.fillMaxSize()) {
val halfHeight = size.height / 2
var scaledMagnitude = magnitude / maxMagnitude.toFloat() * size.height / 2
scaledMagnitude = scaledMagnitude.coerceAtLeast(minMagnitude)
drawLine(
color = upperWaveColor,
start = Offset(size.width / 2, halfHeight),
end = Offset(size.width / 2, halfHeight - scaledMagnitude),
strokeWidth = upperStrokeWidth
)
drawLine(
color = lowerWaveColor,
start = Offset(size.width / 2, halfHeight),
end = Offset(size.width / 2, halfHeight + scaledMagnitude),
strokeWidth = lowerStrokeWidth
)
}
Also keep in mind that your data update method may cause the performance issue. As you clear the list with frequencies and fill it again with new data. For a brief moment there will be nothing to show at all, causing a visible 'lag' or stutter. I found it sometimes better to just update elements instead of flushing a list.
One often overlooked performance issue in jetpack compose is the debug package. As your app becomes more complex, the debug telemetry in the debug build apk can cause some performance issues. Try building a release package apk and see how your app performs.