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.
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!