Levsha
Levsha is a fast HTML template engine and eDSL for Scala 2.12, 2.13 and Scala 3. Optimized templates works without additional memory allocation. Levsha supports changeset inference, which allows to use it as virtual-dom-like middleware.
Static rendering
You can use Levsha as a static HTML renderer.
// build.sbt
libraryDependencies += "com.github.fomkin" %% "levsha-core" % "1.2.0"
// In your code
import levsha.text.renderHtml
import levsha.dsl._
import html._
val features = Seq("Very fast", "Memory-effective")
val html = renderHtml {
optimize {
body(
div(
clazz := "title",
backgroundColor @= "red",
"Hello, I'm Levsha!"
),
ul(clazz := "list",
features map { feature =>
li(class := "item", feature)
}
)
)
}
}
println(html)
<body>
<div style="background-color: red" class="title">Hello, I'm Levsha!</div>
<ul class="list">
<li class="item">Very fast</li>
<li class="item">Memory-effective</li>
</ul>
</body>
Benchmarks
Benchmarks show that Levsha is really fast. Unlike Twirl, Levsha’s performance does not depend on template complexity.
Test | Engine | Ops/s |
---|---|---|
simpleHtml | levsha | 1336693,499 |
simpleHtml | scalatags | 533740,566 |
simpleHtml | twirl | 5950436,854 |
withConditionAndLoop | levsha | 1299646,768 |
withConditionAndLoop | scalatags | 531345,430 |
withConditionAndLoop | twirl | 239537,158 |
withVariables | levsha | 1140298,804 |
withVariables | scalatags | 483508,457 |
withVariables | twirl | 2146419,329 |
In your sbt shell.
bench/jmh:run .StaticRenderingComparision
As a virtual DOM
Levsha can be used as virtual-DOM-like middleware. Unlike other popular virtual DOM solutions, Levsha doesn’t allocate additional memory for construction of a new virtual DOM copy. Also it does not allocate memory in changes inferring phase. Levsha’s memory usage is constant.
// build.sbt
libraryDependencies += "com.github.fomkin" %%% "levsha-dom" % "1.2.0"
// In your code
import org.scalajs.dom._
import levsha.dom.render
import levsha.dom.event
import levsha.dsl._
import html._
case class Todo(id: String, text: String, done: Boolean)
def onSubmitClick() = {
val input = document
.getElementById("todo-input")
.asInstanceOf[html.Input]
val inputText = input.value
// Reset input
input.value = ""
val newTodo = Todo(
id = Random.alphanumeric.take(5).mkString,
text = inputText,
done = false
)
renderTodos(todos :+ newTodo)
}
def onTodoClick(todo: Todo) = {
renderTodos(
todos.updated(
todos.indexOf(todo),
todo.copy(done = !todo.done)
)
)
}
def renderTodos(todos: Seq[Todo]): Unit = render(document.body) {
optimize {
body(
div(clazz := "title", "Todos"),
ul(clazz := "list",
todos map { todo =>
li(
todo match {
case Todo(_, text, true) => strike(text)
case Todo(_, text, false) => span(text)
},
event("click")(onTodoClick(todo))
)
}
),
input(id := "todo-input", placeholder := "New ToDo"),
button("Submit", event("click")(onSubmitClick()))
)
}
}
val todos = Seq(
Todo("1", "Start use Levsha", done = false),
Todo("2", "Lean back and have rest", done = false)
)
renderTodos(todos)
Memory allocation model explanation
As noted below Levsha does not make additional memory allocations if template optimized. It is possible because optimized template, in compile-time rewrites into calls of RenderContext
methods (unlike other template engines which represent their templates as AST on-heap).
For example,
optimize {
div(clazz := "content",
h1("Hello world"),
p("Lorem ipsum dolor")
)
}
Will be rewritten to
Node { renderContext =>
renderContext.openNode(XmlNs.html, "div")
renderContext.setAttr(XmlNs.html, "class", "content")
renderContext.openNode(XmlNs.html, "h1")
renderContext.addTextNode("Hello world")
renderContext.closeNode("h1")
renderContext.openNode(XmlNs.html, "p")
renderContext.addTextNode("Lorem ipsum dolor")
renderContext.closeNode("p")
renderContext.closeNode("div")
}
In turn, RenderContext
(namely DiffRenderContext
implementation) saves instructions in ByteBuffer
to infer changes in the future.
Of course, Levsha optimizer does not cover all cases. When optimization can’t be performed Levsha just applies current RenderContext
to the unoptimized node.
optimize {
ul(
Seq(1, 2, 3, 4, 5, 6, 7).collect {
case x if x % 2 == 0 => li(x.toString)
}
)
}
// ==>
Node { renderContext =>
renderContext.openNode(XmlNs.html, "ul")
Seq(1, 2, 3, 4, 5, 6, 7)
.collect {
case x if x % 2 == 0 =>
Node { renderContext =>
renderContext.openNode(XmlNs.html, "li")
renderContext.addTextNode(x.toString)
renderContext.closeNode("li")
}
}
.foreach { childNode =>
childNode.apply(renderContext)
}
renderContext.closeNode("ul")
}
When you write your Levsha templates, keep in your mind this list of optimizations:
- Nodes and attrs in branches of
if
expression will be moved to currentRenderContext
- Same for cases of pattern matching
xs.map(x => div(x))
will be rewritten into awhile
loopmaybeX.map(x => div(x))
will be rewritten into anif
expressionvoid
will be removed
The third item of this list shows us how to rewrite previous example so that optimization could be performed.
optimize {
ul(
Seq(1, 2, 3, 4, 5, 6, 7)
.filter(x => x % 2 == 0)
.map { x => li(x.toString) }
)
}
// ==>
Node { renderContext =>
renderContext.openNode(XmlNs.html, "div")
val iterator = Seq(1, 2, 3, 4, 5, 6, 7)
.filter(x => x % 2 == 0)
.iterator
while (iterator.hasNext) {
val x = iterator.next()
renderContext.openNode(XmlNs.html, "li")
renderContext.addTextNode(x.toString)
renderContext.closeNode("li")
}
renderContext.closeNode("div")
}
Optimizer options
You can pass this options to SBT.
$ sbt -Doption=value
Option | Description | Possible values | Default |
---|---|---|---|
levsha.optimizer.logUnableToOptimize |
Write positions of unoptimized parts of code to a file. | true/false or file name | false |
levsha.optimizer.unableToSort.forceOptimization |
If tag node content couldn’t be sorted in compile time, optimizer will keep the code unoptimized (so content will be sorted in runtime). You can force optimizer to ignore unspecified Documents (that couldn’t be sorted) and optimize anyway, but keep in mind that node content should be ordered (styles, attrs, nodes). | true/false | false |
levsha.optimizer.unableToSort.warnings |
Warns that optimizer can’t sort tag content when optimization is forced. | true/false | true |