Skip to content

Reducing KotlinJS bundle size using ES2015

The different versions of the JavaScript language are standardized via the EcmaScript standard. By default, Kotlin generates JavaScript code that follows the ES5 standard (2009). Since Kotlin 2.0, the compiler has been able to generate ES2015 code.

Comparing ES5 and ES2015

By default, the Kotlin compiler emits anonymous objects to represent classes. However, in ES2015, the compiler is able to generate proper JavaScript classes.

class Point(
    val x: Int,
    val y: Int,
) {

    override fun toString() = "Point($x, $y)"
}
initMetadataForClass(Point, 'Point');
// …
function Point(x, y) {
    this.h_1 = x;
    this.i_1 = y;
}
protoOf(Point).toString = function () {
    return 'Point(' + this.h_1 + ', ' + this.i_1 + ')';
};
class Point {
  constructor(x, y) {
    this.h_1 = x;
    this.i_1 = y;
  }
  toString() {
    return 'Point(' + this.h_1 + ', ' + this.i_1 + ')';
  }
}

While this doesn't change much in terms of bundle size, it does improve the development experience for mixed Kotlin/JavaScript projects.

ES2015 also adds support for lambda expressions.

More importantly, the Kotlin compiler is able to use ES2015 generator expressions to implement the suspend keyword, which is used by coroutines and sequences. To compare, here is a simple sequence generator:

fun fibonacci() = sequence {
    var a = 0
    var b = 1
    while (true) {
        val tmp = a + b
        a = b
        b = tmp
        yield(tmp)
    }
}
// …
function fibonacci$slambda(resultContinuation) {
  CoroutineImpl.call(this, resultContinuation);
}
protoOf(fibonacci$slambda).c3 = function ($this$sequence, $completion) {
  var tmp = this.d3($this$sequence, $completion);
  tmp.d1_1 = Unit_instance;
  tmp.e1_1 = null;
  return tmp.j1();
};
protoOf(fibonacci$slambda).a2 = function (p1, $completion) {
  return this.c3(p1 instanceof SequenceScope ? p1 : THROW_CCE(), $completion);
};
protoOf(fibonacci$slambda).j1 = function () {
  var suspendResult = this.d1_1;
  $sm: do
    try {
      var tmp = this.b1_1;
      switch (tmp) {
        case 0:
          this.c1_1 = 3;
          this.z2_1 = 0;
          this.a3_1 = 1;
          this.b1_1 = 1;
          continue $sm;
        case 1:
          if (!true) {
            this.b1_1 = 4;
            continue $sm;
          }

          this.b3_1 = this.z2_1 + this.a3_1 | 0;
          this.z2_1 = this.a3_1;
          this.a3_1 = this.b3_1;
          this.b1_1 = 2;
          suspendResult = this.y2_1.d2(this.b3_1, this);
          if (suspendResult === get_COROUTINE_SUSPENDED()) {
            return suspendResult;
          }

          continue $sm;
        case 2:
          this.b1_1 = 1;
          continue $sm;
        case 3:
          throw this.e1_1;
        case 4:
          return Unit_instance;
      }
    } catch ($p) {
      var e = $p;
      if (this.c1_1 === 3) {
        throw e;
      } else {
        this.b1_1 = this.c1_1;
        this.e1_1 = e;
      }
    }
   while (true);
};
protoOf(fibonacci$slambda).d3 = function ($this$sequence, completion) {
  var i = new fibonacci$slambda(completion);
  i.y2_1 = $this$sequence;
  return i;
};
// …
// …
function *_generator_invoke__zhh2q8($this, $this$sequence, $completion) {
    var a = 0;
    var b = 1;
    while (true) {
        var tmp = a + b | 0;
        a = b;
        b = tmp;
        var tmp_0 = $this$sequence.a3(tmp, $completion);
        if (tmp_0 === get_COROUTINE_SUSPENDED())
            tmp_0 = yield tmp_0;
    }
    return Unit_instance;
}
function fibonacci$slambda_0() {
    var i = new fibonacci$slambda();
    var l = ($this$sequence, $completion) => i.n3($this$sequence, $completion);
    l.$arity = 1;
    return l;
}
// …

In this small example, the final bundle is already smaller by 0.74 kB, over a single function. In coroutines-heavy applications, for example if you're using Ktor, this difference grows continuously.

Configuration

If you haven't configured Vite for your project yet, start with the initial setup.

To enable ES2015, configure the Kotlin compiler options. The Vite for Kotlin plugin will automatically pick it up.

build.gradle.kts
// …

kotlin {
    js {
        browser()
        binaries.executable()

        compilerOptions {
            target.set("es2015")
        }
    }

    // …
}