I’m looking for an effective solution to create reusable components in kotlinx.html. I found three type of solutions, but each result in unwanted side effects, such as being 100x slower, or unwanted extra divs.
This is the code:
import kotlinx.html.*
import kotlinx.html.stream.appendHTML
import kotlinx.html.stream.createHTML
import java.util.UUID
import kotlin.system.measureTimeMillis
fun main() {
repeat(1000) {
val time = measureTimeMillis {
renderMultipleProducts(products)
}
println(time)
}
}
// Usecase A: rendering multiple products (With extra unwanted container div)
fun renderMultipleProducts(products: List<Product>): String {
return createHTML().div { // this div is unwanted
for (product in products) {
// version 1
productComponent1(product)()
// version 2, 100x slower than version 1 and 3
consumer.productComponent2(product)
// version 3
productComponent3(product)
}
}
}
// Usecase B: render single product (Without unwanted container div)
fun renderSingleProduct(product: Product): String {
return buildString { // no extra div needed
appendHTML().productComponent2(product)
}
}
// Render using returned lambda receiver
fun productComponent1(product: Product): FlowContent.() -> Unit = {
div {
h5 { +product.name }
div { +"$ ${product.price}" }
}
}
// Render using TagConsumer extension function
fun TagConsumer<*>.productComponent2(product: Product) {
div {
h5 { +product.name }
div { +"$ ${product.price}" }
}
}
// Render using FlowContent/Tag extension function
fun FlowContent.productComponent3(product: Product) {
div {
h5 { +product.name }
div { +"$ ${product.price}" }
}
}
// Data
val products = (1..5000).map {
Product(UUID.randomUUID().toString(), (Math.random() * 100000).toInt())
}
// Domain class
data class Product(val name: String, val price: Int)
Using fun TagConsumer<*>.productComponent2() allows me to render a reusable component without requiring additional container divs. But when I benchmarked it, I found that it was about 100 times slower then the two other solutions that I found.
The other solutions are fun productComponent1(product: Product): FlowContent.() -> Unit and fun FlowContent.productComponent3(product: Product), but they require the caller to wrap the calls in container tags, such as divs.
How are you benchmarking these results? Are you using something like kotlinx-benchmark? measureTimeMillis really isn’t that reliable, and you can’t benchmark multiple pieces of code at the same exact time. My educated guess would be that you should define an extension function that takes in the same receiver that the html block brings into scope, and inside of that you can call div however much you want.
Okay, I’ve done some benchmarking, and the results are… interesting.
Firstly, here’s the code to the initial benchmark, and the improved one (more on that later).
I benchmarked both for single item and 5000 items.Using your code, with some slight tweaks, I got these results (higher is better):
Clearly renderWithProductComponent2 is much much worse than the other functions at multiple products, but for single products the performance is basically the same.
Digging deeper into the root cause of that, we find that renderWithProductComponent2 uses a consumer, which, when you call an HTML dsl function on, “finalizes” the block (i.e. it converts it immediately to a string). This is what’s causing the terrible performance. It’s creating a separate string per product, while renderWithProductComponent1 and renderWithProductComponent3 aren’t.
Changing the code slightly to use buildString and Appendable.appendHtml (see the improved benchmark code above), we get these results:
As you can see, all of them have comparable performance. But renderWithProductComponent2 is much faster at single-items and has the advantage of not needing an outer div block. Instead, it needs an Appendable to add the string to.
We posted at the same time You basically need buildString to be on the outside so that all the products are added to it. This ensures that you don’t need thousands of String objects.
Edit: And we addressed it at the same time, but you were quicker Take your time!