rjaros / kvision

Object oriented web framework for Kotlin/JS
https://kvision.io
MIT License
1.2k stars 67 forks source link

Optimize styling #450

Closed reubenfirmin closed 1 year ago

reubenfirmin commented 1 year ago

I moved styling into code, since that works better in dev mode (changes to styling in code are picked up, whereas changes to css are not always picked up.) However, this introduced a major performance penalty.

When I profile my application, almost all the time in UI interactions is now spent styling. See attached profile.

2022-11-14_04-27

When I drill into the trace, most of the time seems to be spent on the joinToString in styleVNodes in Root.kt.

2022-11-14_04-51

Granted that styling in code means that styles need to be computed; however, most of the time it will be re-computing styles that were previously generated. Can you work on a way to avoid recalculating styles when not needed? Maybe it's as simple as a cache that avoids regenerating if the same widget type with the same style state is rendered as previously.

rjaros commented 1 year ago

Can you give some example code where you use Style objects?

reubenfirmin commented 1 year ago

Here's the previous version of the class I pasted in #451:

package com.example.model

import com.example.*
import com.example.App.Companion.debug
import com.example.view.canvas.CardView
import io.kvision.core.*
import io.kvision.core.Color.Companion.hex
import io.kvision.core.UNIT.*
import io.kvision.html.H1
import io.kvision.html.H2
import io.kvision.utils.em
import io.kvision.utils.px

object CardStyle {
class CardStyle(val theme: Theme) {

    private val foundationCardStyle = Style(selector = "card_base") {
        color = Color.hex(APP_FONTCOLOR)
        color = hex(theme.cardFontColor)
        position = Position.RELATIVE
        whiteSpace = WhiteSpace.NORMAL
    }

    private val baseCardStyle =  inheritingStyle(parentStyle = foundationCardStyle) {
        margin = 10.px
        fontSize = 16.px
        lineHeight = 1.5.em
        opacity = 100.0
        padding = 10.px
        background = Background(Color.hex(APP_ITEM_BACKGROUND))
        background = Background(hex(theme.cardBackground))
    }

    private val imageCardStyle = inheritingStyle(parentStyle = foundationCardStyle) {
        paddingRight = 15 to px
        paddingLeft = 15 to px
        paddingTop = 10 to px
        paddingBottom = 0 to px
        margin = 0 to px
    }

    private val roundedCardStyle = inheritingStyle(parentStyle = baseCardStyle) {
        // TODO now supported
        borderRadius(10 to px)
        // TODO now supported
        boxShadow(0, 8, 14, 0, Color.rgba(0,0,0,45))
    }

    private val linkCardStyle = inheritingStyle(parentStyle = roundedCardStyle) {
        textAlign = TextAlign.CENTER
        fontSize = 14 to px
        cursor = Cursor.POINTER
    }

    private val outlineCardStyleBlue = inheritingStyle(parentStyle = linkCardStyle) {
        border = Border(2 to px, BorderStyle.SOLID, Color.hex(APP_CARD_STRIPE_FOCUSED))
        border = Border(2 to px, BorderStyle.SOLID, hex(theme.cardStripeFocused))
    }

    private val outlineCardStyleGreen = inheritingStyle(parentStyle = linkCardStyle) {
        border = Border(2 to px, BorderStyle.SOLID, Color.hex(APP_CARD_OUTLINE_GREEN))
        border = Border(2 to px, BorderStyle.SOLID, hex(theme.cardStripeDone))
    }

    private val decoratedCardStyle = inheritingStyle(parentStyle = roundedCardStyle) {
        borderLeft = Border(10 to px, BorderStyle.SOLID, Color.hex(APP_CARD_STRIPE))
        borderLeft = Border(10 to px, BorderStyle.SOLID, hex(theme.cardStripe))
    }

    private val decoratedCardStyleHover = inheritingStyle(parentStyle = decoratedCardStyle, pClass = PClass.HOVER) {
        background = Background(Color.hex(APP_ITEM_FOCUSED_BACKGROUND))
        borderLeft = Border(10 to px, BorderStyle.SOLID, Color.hex(APP_CARD_STRIPE_FOCUSED))
        background = Background(hex(theme.cardFocusedBackground))
        borderLeft = Border(10 to px, BorderStyle.SOLID, hex(theme.cardStripeFocused))
    }

    private val headingCard = Style {
        margin = 10 to px
        borderLeft = Border(0 to px)
        background = Background(Color.hex(APP_BACKGROUND))
        background = Background(hex(theme.appBackground))
    }

    private val decoratedCardAllDoneStyle = inheritingStyle(parentStyle = decoratedCardStyle) {
        borderLeft = Border(10 to px, BorderStyle.SOLID, Color.hex(APP_CARD_OUTLINE_GREEN))
        borderLeft = Border(10 to px, BorderStyle.SOLID, hex(theme.cardStripeDone))
    }

    private val selectedStyle = Style {
        background = Background(Color.hex(APP_ITEM_FOCUSED_BACKGROUND))
        background = Background(hex(theme.cardFocusedBackground))
    }

    private val decoratedSelectedStyle = inheritingStyle(parentStyle = selectedStyle) {
        borderLeft = Border(10 to UNIT.px, BorderStyle.SOLID, Color.hex(APP_CARD_STRIPE_FOCUSED))
        borderLeft = Border(10 to UNIT.px, BorderStyle.SOLID, hex(theme.cardStripeFocused))
    }

    private val strikeThrough = Style {
        textDecoration = TextDecoration(TextDecorationLine.LINETHROUGH)
        color = Color.hex(APP_CARD_HEADING_DONE)
        color = hex(theme.cardHeadingDone)
    }

    fun styleCard(card: Card, view: CardView, model: Model) {
        when {
            card.headingOnly() -> {
                view.addCssStyle(headingCard)
                if (view.selected) {
                    view.addCssStyle(selectedStyle)
                } else {
                    view.removeCssStyle(selectedStyle)
                }
            }
            card.infoOnly() -> {
                view.addCssStyle(roundedCardStyle)
                if (view.selected) {
                    view.addCssStyle(selectedStyle)
                } else {
                    view.removeCssStyle(selectedStyle)
                }
                view.addCssClass("info")
            }
            else -> {
                if (card.done()) {
                    view.addCssStyle(decoratedCardAllDoneStyle)
                    view.getChildren().filter { it is H1 || it is H2 }.forEach { child ->
                        child.addCssStyle(strikeThrough)
                    }
                } else {
                    view.addCssStyle(decoratedCardStyle)
                    view.addCssStyle(decoratedCardStyleHover)
                }
                view.addCssClass("task")
                if (view.selected) {
                    debug("selecting")
                    view.addCssStyle(decoratedSelectedStyle)
                } else {
                    view.removeCssStyle(decoratedSelectedStyle)
                }
            }
        }

        if (card.isCanvasLink()) {
            applyCanvasLinkStyle(model, view)
        } else if (card.isImageLink()) {
            view.addCssStyle(imageCardStyle)
        }
    }

    fun applyCanvasLinkStyle(model: Model, view: CardView) {
        if (model.exists(view.card.linkedCanvas()!!)) {
            view.removeCssStyle(outlineCardStyleGreen)
            view.addCssStyle(outlineCardStyleBlue)
        } else {
            view.removeCssStyle(outlineCardStyleBlue)
            view.addCssStyle(outlineCardStyleGreen)
        }
    }

    private fun Style.borderRadius(curve: CssSize) {
        setStyle("border-radius", "${curve.first}${curve.second.name}")
    }

    private fun Style.boxShadow(hOffset: Int, vOffset: Int, blurRadius: Int, spreadRadius: Int, color: Color) {
        setStyle("box-shadow", "${hOffset}px ${vOffset}px ${blurRadius}px ${spreadRadius}px ${color.asString()}")
    }

    data class Transition(val property: String, val time: Double, val timingFunction: String? = null, val delay: Double? = null)

    private fun Style.transition(transitions: List<Transition>) {
        setStyle("transition",
            transitions.joinToString(",") { trans ->
                "${trans.property} ${trans.time}s ${trans.timingFunction ?: ""} ${trans.delay ?: ""}"
            })
    }
}
rjaros commented 1 year ago

I've added simple cache which should resolve performance problems (unless you generate new style objects all the time ;-) )

reubenfirmin commented 1 year ago

Great, I'll test it out once you're ready and let you know.

One more data point here - I moved most of my addCssStyle to addCssClass in the way mentioned in #451 , and that resulted in significant rendering speedups. On the current (precache) version though, I still chew up a lot of time initializing components with default styles; e.g. here's loading a page of cards where the time goes into creating styles for buttons. The caching should for sure fix this.

2022-11-15_04-19

rjaros commented 1 year ago

Could you perhaps show me your CardView class?

reubenfirmin commented 1 year ago

Ignore this latter comment of mine here - I found where I was adding an on the fly style {...} in buttons in CardView, which was causing all of this render time. I followed the approach we discussed in the last couple comments #451, which really made the app snappy.

rjaros commented 1 year ago

That's exactly what I presumed looking at the performance snapshots. Glad you have found the issue.

rjaros commented 1 year ago

Released with 5.18.0