By using this site, you agree to the Privacy Policy and Terms of Use.
Accept
World of SoftwareWorld of SoftwareWorld of Software
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Search
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
Reading: How We Cut Chat UI Frame Time by 8% with One Jetpack Compose Optimization | HackerNoon
Share
Sign In
Notification Show More
Font ResizerAa
World of SoftwareWorld of Software
Font ResizerAa
  • Software
  • Mobile
  • Computing
  • Gadget
  • Gaming
  • Videos
Search
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Have an existing account? Sign In
Follow US
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
World of Software > Computing > How We Cut Chat UI Frame Time by 8% with One Jetpack Compose Optimization | HackerNoon
Computing

How We Cut Chat UI Frame Time by 8% with One Jetpack Compose Optimization | HackerNoon

News Room
Last updated: 2025/06/09 at 10:26 AM
News Room Published 9 June 2025
Share
SHARE

Introduction

Smooth scrolling is critical for chat apps – lag or stutter can severely impact user satisfaction and retention. Chat interfaces face unique challenges due to dynamic, high-density content like text bubbles, images, emojis, and timestamps.

Our team recently encountered a subtle but challenging requirement while working on our chat implementation: dynamically positioning timestamps inline with the last line of text when space permits, or dropping them to a new line when the text is too wide. This seemingly minor design decision uncovered significant performance bottlenecks.

In this article, I’ll walk you through two approaches that we used – SubcomposeLayout and the optimized Layout alternative – to demonstrate how seemingly small implementation choices can dramatically impact your app’s performance. Whether you’re building a chat UI or any complex custom layout in Compose, these techniques will help you identify and resolve critical performance bottlenecks.

Understanding the Technical Challenge

Why Dynamic Positioning Based on Text Content is Complex

Dynamic positioning of elements relative to text presents several unique challenges in UI development. In our case, positioning timestamps based on the available space in the last line of text is particularly complex for several reasons:

1.Variable Text Properties: Message text varies in length, content, and formatting. Each message could have different font sizes, weights, or even mixed formatting within a single message.

  1. Line Break Uncertainty: Text wrapping is unpredictable at design time. The same message may wrap differently based on:
    • Screen size and orientation
    • Font scaling settings
    • Dynamic container sizing
    • Text accessibility settings
  2. Measurement Dependencies: To determine if a timestamp fits inline, we need to:
    • Measure the complete text layout first
    • Calculate the width of the last line specifically
    • Measure the timestamp element
    • Compare these measurements against the container width
    • Make positioning decisions based on these calculations

Initial implementation using SubcomposeLayout

SubcomposeLayout is one of Jetpack Compose’s most powerful but resource-intensive layout APIs, designed specifically for complex layouts requiring multiple measurement and composition passes.

In essence, SubcomposeLayout works through two critical phases:

  1. Subcomposition: Compose the components individually as required, rather than all at once.
  2. Measurement: Measure these individually composed components before determining the final arrangement.

For our timestamp positioning challenge, SubcomposeLayout seemed like the perfect solution. We needed to:

  • First measure the text content to determine line metrics

  • Then decide whether to place the timestamp inline or on a new line

  • Finally compose and position the timestamp based on that decision

Here’s simplified version of how we initially implemented the dynamic timestamp positioning using SubcomposeLayout:

@Composable
fun TextMessage_subcompose(
    modifier: Modifier = Modifier,
    message: Message,
    textColor: Color,
    bubbleMaxWidth: Dp = 280.dp
) {
    val maxWidthPx = with(LocalDensity.current) { bubbleMaxWidth.roundToPx() }

    SubcomposeLayout(modifier) { constraints ->
        
        // ━━━ Phase 1: Subcompose and measure text ━━━
        var textLayoutResult: TextLayoutResult? = null
        val textPlaceable = subcompose("text") {
            Text(
                text = message.text,
                color = textColor,
                onTextLayout = { textLayoutResult = it }
            )
        }[0].measure(constraints.copy(maxWidth = maxWidthPx))
        
        // Extract text metrics after measurement
        val textLayout = requireNotNull(textLayoutResult) {
            "Text layout should be available after subcomposition"
        }
        val lineCount = textLayout.lineCount
        val lastLineWidth = ceil(
            textLayout.getLineRight(lineCount - 1) - 
            textLayout.getLineLeft(lineCount - 1)
        ).toInt()
        val widestLineWidth = (0 until lineCount).maxOf { lineIndex ->
            ceil(
                textLayout.getLineRight(lineIndex) - 
                textLayout.getLineLeft(lineIndex)
            ).toInt()
        }

        // ━━━ Phase 2: Subcompose and measure footer ━━━
        val footerPlaceable = subcompose("footer") {
            MessageFooter(message = message)
        }[0].measure(constraints)

        // ━━━ Calculate container dimensions ━━━
        val canFitInline = lastLineWidth + footerPlaceable.width <= maxWidthPx
        val containerWidth = max(widestLineWidth, lastLineWidth + footerPlaceable.width)
            .coerceAtMost(maxWidthPx)
        val containerHeight = if (canFitInline) {
            max(textPlaceable.height, footerPlaceable.height)
        } else {
            textPlaceable.height + footerPlaceable.height
        }

        // ━━━ Layout and placement ━━━
        layout(containerWidth, containerHeight) {
            textPlaceable.place(x = 0, y = 0)
            
            if (canFitInline) {
                footerPlaceable.place(
                    x = containerWidth - footerPlaceable.width,
                    y = textPlaceable.height - footerPlaceable.height
                )
            } else {
                footerPlaceable.place(
                    x = containerWidth - footerPlaceable.width,
                    y = textPlaceable.height
                )
            }
        }
    }
}

The logic seemed straightforward:

  1. Measure the text first to get line metrics and determine the last line width
  2. Measure the footer (timestamp and status icons) to know its dimensions
  3. Calculate container dimensions based on whether the footer fits inline
  4. Place both elementsaccording to the inline/separate line decision

This approach worked functionally – the timestamps were positioned correctly based on available space. However, as we scaled our chat implementation by introducing additional features, new UI elements, and increased complexity, our performance testing uncovered significant issues. Although these issues weren’t solely due to SubcomposeLayout itself, but rather emerged from the cumulative interaction of multiple components at scale, we determined it necessary to revisit our approach comprehensively.

Upon careful analysis of our TextMessage implementation, several performance bottlenecks were discovered:

  1. Elevated Composition Overhead

Each function call invokes subcompose(“text”) and subcompose(“footer”), effectively triggering two separate composition phases per message on every layout pass – doubling the composition work compared to a traditional single-pass layout approach.

  1. Increased GC Pressure

Each subcompose invocation allocates intermediary lists and lambda instances. Under heavy scrolling scenarios (hundreds of messages), these temporary objects accumulate, leading to more frequent garbage collections and frame drops.

  1. Layout pass complexity

SubcomposeLayout inherently requires more complex layout logic because composition and measurement are interleaved.

This complexity multiplies across all visible items during scrolling, creating a cumulative performance impact that becomes pronounced in production chat environments with hundreds of messages. These findings led us to explore a more efficient approach using Compose’s standard Layout API, which could maintain the same dynamic positioning behavior while significantly reducing the computational overhead.

Optimized Implementation with Layout

After identifying the performance bottlenecks in our SubcomposeLayout approach, we turned to Compose’s standard Layout API. Unlike SubcomposeLayout, the standard Layout follows Compose’s conventional composition → measurement → placement pipeline, which offers several key advantages:

  • Single Composition Phase: All child composables are created during the normal composition phase, not during layout. This allows Compose’s recomposition optimizations to work effectively – stable composables can be skipped when their inputs haven’t changed.
  • Predetermined Children: Layout works with a fixed set of measurables that are known at composition time. This eliminates the dynamic allocation overhead of subcomposition and reduces garbage collection pressure.
  • Simplified Control Flow: The layout logic becomes more straightforward since we don’t need to interleave composition and measurement operations.

Implementation Strategy

Our optimized approach maintains the same visual behavior while restructuring the implementation to work within Layout’s constraints. Here’s a simplified snippet of our optimized approach:

@Composable
fun TextMessage_layout(
    modifier: Modifier = Modifier,
    message: Message,
    textColor: Color,
    bubbleMaxWidth: Dp = 260.dp
) {
    // Shared reference for accessing text layout metrics during measurement
    val textLayoutRef = remember { Ref<TextLayoutResult>() }
    val density = LocalDensity.current

    Layout(
        modifier = modifier,
        content = {
            // Primary text content
            Text(
                text = message.text,
                color = textColor,
                onTextLayout = { result -> textLayoutRef.value = result }
            )
            // Footer containing timestamp and status indicators
            MessageFooter(message = message)
        }
    ) { measurables, constraints ->
        
        val maxWidthPx = with(density) { bubbleMaxWidth.roundToPx() }

        // ━━━ Single-pass measurement of all children ━━━
        val textPlaceable = measurables[0].measure(
            constraints.copy(maxWidth = maxWidthPx)
        )
        val footerPlaceable = measurables[1].measure(constraints)

        // ━━━ Extract text metrics for positioning logic ━━━
        val textLayout = requireNotNull(textLayoutRef.value) {
            "TextLayoutResult must be available after text measurement"
        }
        
        val lineCount = textLayout.lineCount
        val lastLineWidth = ceil(
            textLayout.getLineRight(lineCount - 1) - 
            textLayout.getLineLeft(lineCount - 1)
        ).toInt()
        
        val widestLineWidth = (0 until lineCount).maxOf { lineIndex ->
            ceil(
                textLayout.getLineRight(lineIndex) - 
                textLayout.getLineLeft(lineIndex)
            ).toInt()
        }

        // ━━━ Determine layout strategy ━━━
        val canFitInline = lastLineWidth + footerPlaceable.width <= maxWidthPx

        val containerWidth = if (canFitInline) {
            max(widestLineWidth, lastLineWidth + footerPlaceable.width)
        } else {
            max(widestLineWidth, footerPlaceable.width)
        }.coerceAtMost(maxWidthPx)

        val containerHeight = if (canFitInline) {
            max(textPlaceable.height, footerPlaceable.height)
        } else {
            textPlaceable.height + footerPlaceable.height
        }

        // ━━━ Element placement ━━━
        layout(containerWidth, containerHeight) {
            // Position text at top-left
            textPlaceable.place(x = 0, y = 0)
            
            // Position footer based on available space
            if (canFitInline) {
                // Inline: bottom-right of the text area
                footerPlaceable.place(
                    x = containerWidth - footerPlaceable.width,
                    y = textPlaceable.height - footerPlaceable.height
                )
            } else {
                // Separate line: below text, right-aligned
                footerPlaceable.place(
                    x = containerWidth - footerPlaceable.width,
                    y = textPlaceable.height
                )
            }
        }
    }
}

But why is it better?

Composition separation

 content = {
            Text(
                text = message.text,
                color = textColor,
                onTextLayout = { result -> textLayoutRef.value = result }
            )         
            MessageFooter(message = message)
        }

Both child composables are created during the normal composition phase. This allows Compose to apply its standard optimizations – if message.text and textColor haven’t changed, the Text composable can be skipped entirely during recomposition.

2. Single Measurement Pass


val textPlaceable = measurables[0].measure(rawConstraints.copy(maxWidth = maxWidthPx))
val footerPlaceable = measurables[1].measure(rawConstraints)

Each child is measured exactly once per layout pass. The measurables list is predetermined and stable, eliminating the allocation overhead of dynamic subcomposition.

  1. Shared Layout Result
val textLayoutRef = remember { Ref<TextLayoutResult>() }
//… later in Text composable:
onTextLayout = { result -> textLayoutRef.value = result }

We use a Ref to share the TextLayoutResult between the Text composable’s measurement and our subsequent line calculations. This avoids redundant text layout operations while keeping the data accessible for our positioning logic.

4. Streamlined Logic Flow The layout logic follows a clear, predictable sequence:

Measure children → Extract text metrics → Calculate container size → Place elements

This eliminates the complexity of interleaved composition and measurement that characterized our SubcomposeLayout approach.

The resulting implementation achieves identical visual behavior while working within Compose’s optimized composition pipeline, setting the stage for significant performance improvements that we’ll examine in our benchmark results.

Comparative Performance Analysis

Understanding Macrobenchmarking in Android

Before diving into our results, it’s essential to understand why macrobenchmarking provides the most accurate performance insights for real-world app scenarios. Unlike microbenchmarks that measure isolated code snippets, macrobenchmarks evaluate your app’s performance under realistic conditions – including the Android framework overhead, system interactions, and actual user behavior patterns.

Macrobenchmarking is particularly critical for UI performance analysis because it captures the complete rendering pipeline: from composition through layout to drawing and display. This comprehensive approach reveals performance bottlenecks that might be invisible in isolated testing environments.

Benchmarking and Results

We conducted macro-benchmark tests comparing both implementations (SubcomposeLayout vs. Layout). The benchmarks clearly indicated substantial performance improvements, including:

  • Reduced Frame Drops: The optimized approach significantly reduced stutter during rapid scrolling.
  • Lower Garbage Collection Pressure: Fewer intermediate object creations dramatically improved GC metrics.
  • Simpler and More Maintainable Code: By reducing complexity, the layout logic became clearer and easier for ongoing maintenance.

The benchmarks were structured using a macrobenchmark test similar to the following snippet:

@Test
fun scrollTestLayoutImplementation() = benchmarkRule.measureRepeated(
    packageName = "ai.aiphoria.pros",
    metrics = listOf(FrameTimingMetric()),
    iterations = 10,
    setupBlock = {
        pressHome()
        device.waitForIdle(1000)
        startActivityAndWait(setupIntent(useSubcompose = false))
    },
    startupMode = StartupMode.WARM
) {
    performEnhancedScrollingActions(device)
}

private fun performEnhancedScrollingActions(device: UiDevice, scrollCycles: Int = 40) {
   val width = device.displayWidth
   val height = device.displayHeight
   val centerX = width / 2
   val swipeContentDownStartY = (height * 0.70).toInt()
   val swipeContentDownEndY = (height * 0.3).toInt()
   val swipeSteps = 3
   val pauseBetweenScrolls = 15L

   repeat(scrollCycles) {
       device.swipe(centerX, swipeContentDownEndY, centerX, swipeContentDownStartY, swipeSteps) // Scrolls content up
       SystemClock.sleep(pauseBetweenScrolls)
   }

   repeat(scrollCycles) {
       device.swipe(centerX, swipeContentDownStartY, centerX, swipeContentDownEndY, swipeSteps) // Scrolls content down
       SystemClock.sleep(pauseBetweenScrolls)
   }
}

Our macrobenchmark tests revealed substantial performance improvements when switching from SubcomposeLayout to the optimized Layout approach. The results demonstrate consistent gains across all performance percentiles:

Frame Duration Improvements

The most critical metric for user experience – frame rendering time – showed significant improvements:

  • P50 (Median): 5.9ms vs 6.3ms (6.7% improvement)
  • P90: 10.5ms vs 11.0ms (4.7% improvement)
  • P95: 12.3ms vs 12.9ms (4.8% improvement)
  • P99: 15.0ms vs 16.2ms (8% improvement)

While these improvements might seem modest in absolute terms, they represent meaningful gains in a chat interface where smooth 60fps scrolling is critical. The P99 improvement is particularly significant – those worst-case frame times that cause noticeable stuttering are reduced by nearly 8%.

Frame Overrun Analysis

Frame overruns occur when rendering takes longer than the 16.67ms budget for 60fps. Our optimized Layout implementation shows better performance characteristics:

  • Fewer severe overruns: The P99 frame overrun improved from 1.4ms to 0.2ms
  • Better consistency: More predictable frame timing across all percentiles
  • Reduced stuttering: Fewer instances of frames missing their vsync deadline

The frame overrun improvements are especially important for maintaining smooth scrolling during intensive user interactions like rapid scroll gestures or when the system is under memory pressure.

Key Lessons Learned

When to Avoid SubcomposeLayout

Our experience reveals specific scenarios where SubcomposeLayout’s flexibility comes at too high a performance cost:

  • High-Frequency Layouts: In scrolling lists where layout operations occur dozens of times per second, the overhead of subcomposition becomes prohibitive.
  • Simple Dynamic Positioning: When your layout requirements can be achieved through measurement and calculation rather than conditional composition, standard Layout is more efficient.
  • Performance-Critical UI: Chat interfaces, gaming UIs, or any context where frame drops directly impact user satisfaction warrant the optimization effort.

When SubcomposeLayout Still Makes Sense

SubcomposeLayout remains the right choice for:

  • Complex Conditional Composition: When you need to compose entirely different component trees based on measured content.
  • Infrequent Layout Operations: For dialogs, configuration screens, or other UI that doesn’t require high-frequency layout passes.
  • Prototyping: SubcomposeLayout'sflexibility makes it excellent for rapid iteration during development.

Performance Optimization Checklist

Based on our optimization journey, here’s a practical checklist for identifying and resolving similar performance bottlenecks:

Detection

  • Profile scroll performance using macrobenchmarks, not just microbenchmarks
  • Monitor frame timing metrics during realistic user interactions
  • Test on lower-end devices where performance differences are amplified
  • Measure at scale – test with hundreds of items, not just a few

Analysis

  • Identify composition frequency – how often are your custom layouts being measured?
  • Count measurement passes – are children being measured multiple times unnecessarily?
  • Check allocation patterns – are you creating temporary objects during layout?

Optimization

  • Prefer single-pass measurement when possible
  • Share measurement results between composition and layout phases efficiently
  • Minimize object allocation during layout operations
  • Validate with benchmarks – ensure optimizations provide measurable benefits

Conclusion

The key takeaway for Android developers building high-performance UIs: always measure your assumptions. What appears to be a minor implementation detail can have a substantial impact on user experience at scale. Invest in proper benchmarking infrastructure early, and don’t hesitate to revisit implementation choices as your app’s performance requirements evolve.

Happy coding!

Sign Up For Daily Newsletter

Be keep up! Get the latest breaking news delivered straight to your inbox.
By signing up, you agree to our Terms of Use and acknowledge the data practices in our Privacy Policy. You may unsubscribe at any time.
Share This Article
Facebook Twitter Email Print
Share
What do you think?
Love0
Sad0
Happy0
Sleepy0
Angry0
Dead0
Wink0
Previous Article Best Cheap Phones 2025: Our favourite affordable handsets tested and ranked
Next Article China shuts down AI tools during nationwide college exams
Leave a comment

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Stay Connected

248.1k Like
69.1k Follow
134k Pin
54.3k Follow

Latest News

How AI Helps Regular People Build Useful Businesses | HackerNoon
Computing
watchOS 26 finally brings Apple Intelligence to the Apple Watch… for better or worse | Stuff
Gadget
Apple introduces Liquid Glass to take on Material 3 Expressive
News
WWDC 2025: Apple Confirms iOS 26 is coming soon
Software

You Might also Like

Computing

How AI Helps Regular People Build Useful Businesses | HackerNoon

10 Min Read
Computing

China’s BYD, Geely offer big incentives in latest price war move · TechNode

1 Min Read
Computing

Six founders ask investors questions about venture capital

15 Min Read
Computing

Influencer Marketing for Enterprise: How to Scale Success

4 Min Read
//

World of Software is your one-stop website for the latest tech news and updates, follow us now to get the news that matters to you.

Quick Link

  • Privacy Policy
  • Terms of use
  • Advertise
  • Contact

Topics

  • Computing
  • Software
  • Press Release
  • Trending

Sign Up for Our Newsletter

Subscribe to our newsletter to get our newest articles instantly!

World of SoftwareWorld of Software
Follow US
Copyright © All Rights Reserved. World of Software.
Welcome Back!

Sign in to your account

Lost your password?