What's the Fastest Way to Create JavaScript Types in Scala.js? :: Feb 10, 2020
In Scala.js, we cannot just write
class C(val a: String, val b: Boolean, val c: Int)
import scala.scalajs.js.JSON
JSON.stringify(C("hello world", true, 69))
// (this post uses Scala 3 syntax)
It's because JSON.stringify takes a native JavaScript parameter of type `js.Any`. So, what's the fastest way to create a native JavaScript type in Scala.js?
Option 1: Subclass of js.Object.
import scala.scalajs.js
class C(val a: String, val b: Boolean, val c: Int) extends js.Object
JSON.stringify(C("hello world", true, 69))
Option 2: Dynamic.literal with tupled arguments.
val literal: js.Dynamic = js.Dynamic.literal("a" -> "hello world", "b" -> true, "c" -> 69)
JSON.stringify(literal)
Option 3: `Dynamic.literal` with varargs.
val literal: js.Dynamic = js.Dynamic.literal(a = "hello world", b = true, c = 69)
JSON.stringify(literal)
Option 4: `Dynamic.literal` with [Dynamic::updateDynamic](https://www.scala-js.org/api/scalajs-library/latest/scala/scalajs/js/Dynamic.html#updateDynamic(name:String)(value:scala.scalajs.js.Any):Unit).
val literal = js.Dynamic.literal()
literal.updateDynamic("a")("hello world")
literal.updateDynamic("b")(true)
literal.updateDynamic("c")(69)
JSON.stringify(literal)
Option 5: `js.Object` with `Dynamic::updateDynamic`
val dynamicObj = js.Object().asInstanceOf[js.Dynamic]
dynamicObj.updateDynamic("a")("hello world")
dynamicObj.updateDynamic("b")(true)
dynamicObj.updateDynamic("c")(69)
JSON.stringify(dynamicObj)
Guess now before scrolling!
This result makes sense to me after looking at the codegen. Subclasses have initialization, `updateDynamic` is intrinsically translated into bracket access like `literal['a'] = 'hello world'`, and `Dynamic.literal()` does not have a non-varargs overload unlike `js.Object()`.
Full Benchmark Source:
import scala.scalajs.js
import scala.scalajs.js.JSON
import scala.util.Random
class C(val a: String, val b: Boolean, val c: Int) extends js.Object
@main def bench(): Unit =
for _ <- 1 to 20 do loop()
def loop(): Unit =
val limit = 1000000
val res = Array.ofDim[String](limit)
var i = 0
var start = js.Date.now()
while i < limit do
res(i) = JSON.stringify(C("hello world", true, 69))
i += 1
var end = js.Date.now()
def randomJson: String = res(Random.nextInt(res.size))
println(s"ms to make $randomJson classes: ${end - start}")
i = 0
start = js.Date.now()
while i < limit do
res(i) = JSON.stringify(js.Dynamic.literal("a" -> "hello world", "b" -> true, "c" -> 69))
i += 1
end = js.Date.now()
println(s"ms to make $randomJson literals via tuples: ${end - start}")
i = 0
start = js.Date.now()
while i < limit do
res(i) = JSON.stringify(js.Dynamic.literal(a = "hello world", b = true, c = 69))
i += 1
end = js.Date.now()
println(s"ms to make $randomJson literals: ${end - start}")
i = 0
start = js.Date.now()
while i < limit do
val literal = js.Dynamic.literal()
literal.updateDynamic("a")("hello world")
literal.updateDynamic("b")(true)
literal.updateDynamic("c")(69)
res(i) = JSON.stringify(literal)
i += 1
end = js.Date.now()
println(s"ms to make $randomJson literals via updateDynamic: ${end - start}")
i = 0
start = js.Date.now()
while i < limit do
val dynamicObj = js.Object().asInstanceOf[js.Dynamic]
dynamicObj.updateDynamic("a")("hello world")
dynamicObj.updateDynamic("b")(true)
dynamicObj.updateDynamic("c")(69)
res(i) = JSON.stringify(dynamicObj)
i += 1
end = js.Date.now()
println(s"ms to make $randomJson objects with js.Object() + updateDynamic: ${end - start}")
println()
Why does any of this matter? Next blog we will see!