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!