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!