JetBrains / compose-multiplatform

Compose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.
https://jetbrains.com/lp/compose-multiplatform
Apache License 2.0
16.03k stars 1.16k forks source link

how to reduce the time for First Meaning Paint #2645

Closed guoguo338 closed 1 week ago

guoguo338 commented 1 year ago

Hi,

Is there any way to shrink the time for "First Meaning Paint" in compose framework side, i.e., the time for the first page rendering. For example, maybe we can delay the number of modifiers until 2nd frame? Maybe the slot-table for first frame could be captured and loaded in advanced next time?

igordmn commented 1 year ago

The whole process of showing an application includes:

  1. Initialization of Compose. Happens only when we start an application.

    • Loading Java and native libraries
    • Initialization of various frameworks used by Compose (Swing, Skia, Native platform framework)
    • Initialization of Compose itself
  2. Preparing the application. Happens every time we open a new window/dialog, or just change the content of it.

    • composition (construction tree of UI components)
    • layout (deciding position and size of UI components)
    • paint (drawing UI components on Canvas)
    • all drawing calls during paint are performed by GPU

On every step we can do some optimization on the framework level, but it is not always possible to optimize it completely, and developer of the application should care of optimization of the second step (preparing the application).

If it is not possible to do all the work fast in the first frame, the application developer should schedule some work to the next frames. For desktop it can be achieved this way:

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.WindowScope
import androidx.compose.ui.window.singleWindowApplication
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent

fun main() = singleWindowApplication {
    val isWindowVisible by isWindowVisibleState()

    Box(Modifier.fillMaxSize().background(Color.LightGray)) {
        if (!isWindowVisible) {
            LightUI()
        } else {
            HeavyUI()
        }
    }
}

@Composable
private fun WindowScope.isWindowVisibleState(): State<Boolean> {
    val isVisible = remember { mutableStateOf(false) }

    DisposableEffect(Unit) {
        val listener = object : ComponentAdapter() {
            override fun componentShown(e: ComponentEvent?) {
                isVisible.value = window.isVisible
            }
        }
        window.addComponentListener(listener)
        onDispose {
            window.removeComponentListener(listener)
        }
    }

    return isVisible
}

@Composable
fun LightUI() {
    Row {
        repeat(3) {
            Box(
                Modifier
                    .width(200.dp)
                    .fillMaxHeight()
                    .border(1.dp, Color.Gray)
            )
        }
    }
}

@Composable
fun HeavyUI() {
    Row {
        repeat(3) {
            Column(
                Modifier
                    .width(200.dp)
                    .fillMaxHeight()
                    .border(1.dp, Color.Black)
                    .verticalScroll(rememberScrollState())
            ) {
                repeat(50) {
                    var text by remember { mutableStateOf("") }
                    TextField(text, { text = it })
                }
            }
        }
        Column { 
            // loading images asynchronously
        }
    }
}

(maybe we should provide a similar check in Compose itself, for all platforms)

Anyway, the startup of Compose application can be very slow (2-4 seconds), so we need:

  1. Optimize some of the steps (where we can):

    • reduce framework initialization time
    • do some initialization in parallel
    • support parallel composition/layout/paint
    • postpone some work, and show the content faster, if possible (not sure that it is achievable on the framework level)
  2. Provide tools to do additional optimizations on the application level:

    • ability of preparing content (compose, layout, paint) asynchronously in background
    • serialize some of the work, and just load it from the disk next time
  3. Provide tools to measure performance of each step (in IDE, or just in console)

  4. Describe them in our documentation

delay the number of modifiers until 2nd frame

Good idea, but not sure that is possible in general case. This work should be done in each modifier - we should look what modifiers are slow, and postpone some work in them.

the slot-table for first frame could be captured and loaded in advanced next time

It is not possible to do on the framework level, because we can't know which part can be loaded next time, that should be decided by application developer. Also, if we talk about serialization of some part (storing it on a disk), it is also not possible on the framework level, as the slot table contain a lot of non-serializable data. But anyway, we can provide tools, so developers can easily choose parts which they decide can be postponed or/and serialized.

Related issue: https://github.com/JetBrains/compose-jb/issues/2517

orangy commented 1 year ago

Just a small hint: one thing that helps (at least on macOS) is loading fonts in parallel, if you have a main menu. Launch a coroutine on a background dispatcher even before you call application { … } and call this magic: UIManager.getFont("Panel.font").fontName. While Compose app is loading classes, initializing, etc, at least part of the font system would be ready.

okushnikov commented 1 month ago

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.