I've found this message <https://kotlinlang.slack....
# compose-web
a
I've found this message https://kotlinlang.slack.com/archives/C01F2HV7868/p1623689375233100?thread_ts=1623686048.230900&amp;cid=C01F2HV7868 from @Oleksandr Karpovich [JB] related to the js-framework-benchmark and Compose HTML. I've implemented the benchmark and ran it but the performance is... well, horrible. Takes a few seconds to run operations that take no more than a second in react (and react is known for being quite slow, if we compare it to SolidJS it looks much worse for Compose). Seems like there haven't been performance optimizations (or not as much as I've hoped). Quite a big deal for lower end devices imo 😞
âž• 1
r
Can you share your implementation of the benchmark?
a
Sure, it's quite similar to the react version (not the most idiomatic thing but good enough to test imo
Copy code
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import org.jetbrains.compose.web.dom.*
import org.jetbrains.compose.web.renderComposable

var idCounter = 1;

data class Row(val id: Int, val label: String)

fun main() {
    var data by mutableStateOf(arrayOf<Row>())
    var selected by mutableStateOf<Row?>(null)

    renderComposable(rootElementId = "root") {
        Div({ classes("container") }) {
            Div({ classes("jumbotron") }) {
                Div({ classes("row") }) {
                    Div({ classes("col-md-6") }) {
                        H1 { Text("Compose HTML") }
                    }
                    Div({ classes("col-md-6") }) {
                        Div({ classes("row") }) {
                            Button(
                                {
                                    id("run")
                                    onClick {
                                        println("Click create 1000 rows")
                                        data = buildData(1000)
                                    }
                                }
                            ) { Text("Create 1,000 rows") }

                            Button(
                                {
                                    id("runlots")
                                    onClick {
                                        println("Click create 10_000 rows")
                                        data = buildData(10_000)
                                    }
                                }
                            ) { Text("Create 10,000 rows") }

                            Button(
                                {
                                    id("add")
                                    onClick {
                                        println("Click append 1000 rows")
                                        data += buildData(1000)
                                    }
                                }
                            ) { Text("Append 1,000 rows") }

                            Button(
                                {
                                    id("run")
                                    onClick {
                                        println("Click update every 10th row")

                                        val newData = data.copyOf()

                                        for (i in data.indices step 10) {
                                            val oldData = newData[i]
                                            newData[i] = oldData.copy(label = "${oldData.label} !!!")
                                        }

                                        data = newData
                                    }
                                }
                            ) { Text("Update every 10th row") }

                            Button(
                                {
                                    id("run")
                                    onClick {
                                        println("Click clear")
                                        data = emptyArray()
                                    }
                                }
                            ) { Text("Clear") }

                            Button(
                                {
                                    id("swaprows")
                                    onClick {
                                        println("Click swap rows")

                                        if(data.size > 998) {
                                            val newData = data.copyOf()
                                            newData[1] = data[998]
                                            newData[998] = data[1]

                                            data = newData
                                        }
                                    }
                                }
                            ) { Text("Swap Rows") }
                        }
                    }
                }
            }
            Table({ classes("table", "table-hover", "table-striped", "test-data") }) {
                for (item in data) {
                    Tr({ if (selected?.id == item.id) classes("danger") }) {
                        Td({ classes("col-md-1") }) { Text(item.id.toString()) }
                        Td({ classes("col-md-4") }) {
                            A(attrs = {
                                onClick {
                                    println("Clicked item $item")
                                    selected = item
                                }
                            }) { Text(item.label) }
                        }
                        Td({ classes("col-md-1") }) {
                            A(attrs = { onClick { } }) {
                                Span({
                                    classes(
                                        "glyphicon",
                                        "glyphicon-remove"
                                    )
                                })
                            }
                        }
                        Td({ classes("col-md-6") })
                    }
                }
            }

            Span({ classes("preloadicon", "glyphicon", "glyphicon-remove") })
        }
    }
}

fun buildData(count: Int): Array<Row> {
    return Array<Row>(count, init = {
        Row(
            idCounter++,
            "${adjectives.random()} ${colours.random()} ${nouns.random()}"
        )
    })
}


val adjectives = arrayOf(
    "pretty",
    "large",
    "big",
    "small",
    "tall",
    "short",
    "long",
    "handsome",
    "plain",
    "quaint",
    "clean",
    "elegant",
    "easy",
    "angry",
    "crazy",
    "helpful",
    "mushy",
    "odd",
    "unsightly",
    "adorable",
    "important",
    "inexpensive",
    "cheap",
    "expensive",
    "fancy"
)
val colours = arrayOf("red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", "orange")
val nouns = arrayOf(
    "table",
    "chair",
    "house",
    "bbq",
    "desk",
    "car",
    "pony",
    "cookie",
    "sandwich",
    "burger",
    "pizza",
    "mouse",
    "keyboard"
)
I've tried also using
mutableStateListOf
rather than
mutableStateOf
+
Array
but it didn't make any difference (at least not obvious)
r
Have you managed to integrate the whole compose/kotlin build with the benchmark tooling or just compiled this kotlin code to JS and used that JS file directly?
a
I didn't bother trying running the benchmark itself, I built a "production" version and tested it in the browser itself. I'll try in the benchmark tooling (using a compiled kotlin to JS), I'll be back in a few mins
uh so some benchmarks ended up failing after running for a while 😞 I may have done something wrong but w/e, just the execution of the tests took forever, enough for me to check its performance
a
The problem is that we don’t know if the largest time is spent in the compose runtime or maybe in some of your state data structures. Js data structures are hiiiiighly optimized and the V8 runtime even optimized them further. So when using Kotlin collections they don’t leverage these benefits (yet). Although I believe Array maps to the Js array. Needless to say, a flamegraph would be nice. But besides that. It really depends on what you seek in developing a web app. For some of our internal apps we use Kotlin for HTML with great succes but we don’t require ultra performance but we really enjoy the fact we can use common codebases in front and backend. Although this is possible with TS as well, I find working with statically typed languages eliminates a whole class of errors
h
There is also another thing to keep in mind, react uses a virtual dom to manage many html nodes, while compose html does not. Depending on the benchmark this could make a huge difference when testing many elements.
r
@hfhbd Virtual DOMs are widely considered to be the cause of poor performance as opposed to better performance. For example, SolidJS is way faster than React and does not use a VDOM. I think Compose should (eventually) have the edge against frameworks that use VDOMs.
a
VDOM is a, clever, hack of working around the complexity of updating multiple DOM nodes. Direct updates of DOM elements will always be quicker, since eventually that is what the VDOM reconciliation boils down to.
a
The problem is that we don’t know if the largest time is spent in the compose runtime or maybe in some of your state data structures. Js data structures are hiiiiighly optimized and the V8 runtime even optimized them further. So when using Kotlin collections they don’t leverage these benefits (yet). Although I believe Array maps to the Js array.
Even clicking clear to remove all the rows seems to take much longer than it should (it only creates an empty array to replace the previous one), so it's quite likely to be a compose runtime issue
t
I think this is a compose runtime issue, 1000 items is small and
buildData
's runtime is almost 0ms.
for (item in data)
also finishes long before the 1st item on the list is visible.