Seamless k6.io Performance Testing in Scala Projects Using Scala.js
Modern systems rarely fail because of a single bug. More often, they degrade under real-world pressure: sudden traffic spikes, chatty cross-service calls, slow leaks that only surface after hours of load. Performance issues now emerge continuously in production, not during a dedicated test phase. Cloud environments and SRE-centric observability pushed load validation out of the controlled lab and into the delivery pipeline. Relying on auto-scaling and dashboards means discovering problems only once they are already impacting users in production.
K6 addresses part of these problems by making performance test code: versioned, automated, and integrated directly into CI. But it is a JavaScript-only scripting model. It is misaligned with teams that are building distributed backends in Scala. You lose type safety, FP patterns*, and the ability to reuse domain logic and shared libraries that drive the system’s behaviour.
In this article, we explore a different path: writing k6 tests in Scala via Scala.js. Same runtime model, same k6 ecosystem. We get compile-time guarantees, functional composition, and direct integration into Scala-based projects. Treat performance testing as a first-class software concern, not something done as a last piece of a puzzle, often by a separate team. We can tighten feedback loops, which can be a complement to the ubiquitous "shift-right" approach promoted by DevOps and SRE.
Background
k6 has become one of the most popular tools for creating performance tests. Developers and QA engineers value it for its simplicity, readability, and ease of integration.
As the documentation states:
Grafana k6 is an open-source, developer-friendly, and extensible load testing tool. k6 allows you to prevent performance issues and proactively improve reliability.
k6 is a lightweight, compiled binary written in GO, embedding the Goja JavaScript interpreter. This design reflects a fundamental trade-off: leveraging the maturity and familiarity of the JavaScript ecosystem while maintaining the low resource consumption and efficiency of the Go language. As a result, test scripts are written in JavaScript. It is a practical choice since the language is well-known among both developers and QA engineers.
You can execute tests in your local environment or in a cloud environment with minimal boilerplate code. K6.io integrates well with Grafana, enabling near real-time monitoring of test execution. Its extension API allows you to use existing community extensions or develop your own. For example, to add support for custom network protocols.
Why look beyond JavaScript?
While JavaScript is the first choice for many engineers, especially those working in software quality, it's not the only option worth considering. Scala, despite its reputation for complexity, offers strong interoperability and a powerful feature set that can be valuable in performance testing scenarios. Teams that already have their codebase written in Scala could benefit from it significantly.
In this article, we will highlight both the advantages and disadvantages that Scala brings to the table, helping you evaluate whether it might be a good fit for your use case. We will also outline how to integrate it with projects using a few Scala build tools: sbt, mill, and Gradle.
But first, we need to say a few words about Scala.js, which makes it possible.
Scala.js as a bridge to k6
Scala.js allows us to write applications in Scala that compile down to JavaScript and run in any modern browser or JavaScript runtime. This means you can leverage the static typing and advanced features of Scala - such as functional programming constructs, powerful collections, and expressive syntax - while still targeting the JavaScript ecosystem.
By doing so, Scala.js can help improve code quality and maintainability through stronger type safety and better compiler guarantees. Another advantage is the ability to reuse existing Scala libraries (limited to some degree) as well as integrate seamlessly with the extensive ecosystem of JavaScript libraries. This makes it a powerful bridge between the JVM world and the JavaScript runtime.
With this foundation in place, we can now show how to build a k6 library facade in Scala.js and begin prototyping our target solution.
The Journey: From Idea to Implementation
The idea of using Scala.js with k6 started from a simple observation: while k6 offers an elegant way to write performance tests in JavaScript, it felt disconnected from projects that were already written in Scala. We wanted a way to reuse Scala’s type system and tooling, while still taking advantage of k6’s flexibility and ecosystem.
To verify if it is possible, we have done a simple scala-js project using Scala CLI:
//> using scala "3.5.0"
//> using platform scala-js
package example
import scala.scalajs.js
import scala.scalajs.js.annotation.*
@js.native
object k6 extends js.Object {
def check(response: Response, checks: js.Dictionary[Boolean]): Boolean = js.native
def sleep(response: Int): Unit = sjs.native
}
@js.native
object http extends js.Object {
def get(url: String): Response = js.native
}
@js.native
trait Response extends js.Object {
def body: String = js.native
def status: Int = js.native
}
@JSExportTopLevel(JSImport.Default)
object K6ScalaExample {
@JSExport
def main(): Unit = {
val response = Http.get("https://quickpizza.grafana.com/")
}
}Having proof that it will work, we have created a k6 Grafana facade using ScalaJS, which is much simpler:
//> using scala "3.5.0"
//> using platform scala-js
//> using dep "org.scala.js::k6-scala::0.0.1-SNAPSHOT"
package example
import scala.scalajs.js
import scala.scalajs.js.annotation.*
import org.scala.js.k6.http.Http
@JSExportTopLevel(JSImport.Default)
object K6ScalaExample {
@JSExport
def main(): Unit = {
val response = Http.get("https://quickpizza.grafana.com/")
}
}Integration with Build Tools
It looks nice. Pretty neat implementation. In some cases, even sufficient if we want to have standalone tests, but the true benefits would be, as we stated earlier, if we could integrate it with existing projects. In the Scala world, there are three most significant build tools: sbt, mill, and Gradle. In all cases, we need k6 Grafana installed.
To check if it is feasible, we have created a simple project with appropriate tooling. For SBT, we have created a simple task:
lazy val root = Project("root", file("."))
.enablePlugins(ScalaJSPlugin)
.settings(
scalaVersion := "3.5.0",
publish / skip := true,
scalaJSLinkerConfig ~= {
_.withModuleSplitStyle(ModuleSplitStyle.FewestModules).withModuleKind(ModuleKind.ESModule)
},
runK6example := {
val jsTargetDir = (Compile / fullOptJS / crossTarget).value
val name = (Compile / fullOptJS / moduleName).value
val jsOutput = jsTargetDir / s"${name}-fastopt.js"
val log = streams.value.log
log.info(s"Using JS output: ${jsOutput.getAbsolutePath()}")
log.info(s"Running k6 test...")
val exitCode = Process(Seq("k6", "run", jsOutput.getAbsolutePath)).!
if (exitCode != 0) {
log.error("k6 failed!")
}
}
)Similarly, for mill a command was created:
import mill._
import mill.api.Loose
import mill.scalalib._
import mill.scalajslib._
import mill.scalajslib.api._
val projectScalaVersion = "3.5.0"
object helloworld extends ScalaJSModule {
override def scalaVersion = projectScalaVersion
override def scalaJSVersion = "1.18.1"
override def ivyDeps: Target[Loose.Agg[Dep]] = super.ivyDeps() ++ Agg(
ivy"org.scalajs::k6-scala::0.0.1-SNAPSHOT".withDottyCompat(projectScalaVersion)
)
override def moduleKind = ModuleKind.ESModule
override def moduleSplitStyle = ModuleSplitStyle.FewestModules
override def sources = T.sources {
Seq(millSourcePath / "src").map(PathRef(_))
}
def runK6test() = T.command {
val jsFilePath: os.Path = fastLinkJS().dest.path / "main.js"
println(s"Using JS output: $jsFilePath")
val exitCode = os.proc("k6", "run", jsFilePath).call(stdout = os.Inherit, stderr = os.Inherit).exitCode
if (exitCode != 0) {
throw new Exception("k6 failed!")
}
}
}All projects with additional Gradle-based projects can be found in VirtusLab's GitHub project for k6.io faced.
Performance and Metrics
Having a working solution and integrations with build tools in place, we wanted to measure the potential time overhead compared to "regular" JavaScript. To do this, we used the hyperfine tool, which offers a very simple syntax:
hyperfine -r 100 -p 'rm -rf .scala-build/ example.js example.js.map' 'scala-cli --power package example.scala --js --js-emit-source-maps --js-module-kind esmodule -f --js-no-opt'
hyperfine -r 100 'k6 run example.js'
hyperfine -r 100 'k6 run pure.js'
hyperfine -r 100 -p 'mill clean' 'mill helloworld.fastOpt'
hyperfine -r 100 -p 'sbt clean' 'sbt fastOptJS'
hyperfine -r 100 -p './gradlew clean' './gradlew link'
hyperfine -r 100 'sbt runK6test'
hyperfine -r 100 'mill runK6test'
hyperfine -r 100 './gradlew runK6test'Assumptions:
- We have tested compilation and execution times separately.
- Compilation is done on a clean workspace,
- The script is changed so there are 100 iterations, but no actual requests.
- Showing only the average value from 100 repetitions.
| technology stack | compilation time | execution time |
|---|---|---|
| k6.io native | N/A | 358.5 ms |
| scala-js with scala-cli | 2.895 s | 467.0 ms |
| scala-js with sbt | 15.214 s | 8.735 s |
| scala-js with mill | 7.402 s | 752.1 ms |
| scala-js with gradle | 10.277 s | 4.313 s |
As we can see, even in a clean workspace, the compilation step introduces some overhead compared to the total test execution time. While this example is very simple, in real-world scenarios, the compilation time could be higher. On the other hand, incremental compilation will typically be used in most cases, which can significantly reduce this overhead.
When it comes to execution time, we do observe a difference. This may be attributed to the boilerplate code generated in the Scala.js version and its impact on k6's interpretation performance. Building tools based on executions gives even higher overhead. However, since execution time in typical load testing scenarios is measured in minutes, not seconds, this overhead should be negligible.
Pros and Cons of Writing k6 Tests in Scala.js
The performance results give us a clear picture of the runtime overhead. However, execution time is only one factor to consider when deciding whether Scala.js is a good fit for k6 tests. Let’s take a step back and look at the broader set of pros and cons.
Pros of Writing k6 Tests in Scala.js
- Type-safety and IDE support
- Integration with Scala (and possibly Java) based projects
- Possibility to reuse domain logic
- Easier maintenance for teams already using Scala.
Cons of Writing k6 Tests in Scala.js
- No native support from Grafana k6.io
- Compilation and execution time overhead
- Lack of traceability between test failures and actual tests
- Smaller community and ecosystem
- Learning curve for Scala.js (if new to it)
- No way to debug tests, but this is a general Grafana k6.io concern.
Summary
There's no doubt - JavaScript is the first-choice language for k6. It's the native runtime, widely adopted, and extremely well-documented. For most teams, especially those just getting started with performance testing, JavaScript offers the fastest path to results with minimal setup.
But when does Scala.js make sense?
As mentioned earlier, Scala becomes a compelling option when you're already working within a Scala ecosystem - particularly in large, type-safe codebases where performance testing needs to be aligned closely with existing models or logic.
Beyond that, Scala.js can also be a great fit if you:
- Value type safety to catch issues early and improve test reliability
- Prefer a functional programming approach to test composition and logic
- Want to reuse domain logic or data models directly in your performance tests
That said, it’s important to acknowledge the current limitations: This approach is still in its early stages. While promising, it requires further development to become fully production-ready - including full API coverage, proper packaging and publishing, tooling improvements, and seamless runtime integration.
Links and resources
- Scala.js k6 facade - initial project and some examples
- Scala.js documentation
- Scala CLI documentation
- Grafana k6
* JavaScript exposes a few functional primitives, but without static types, they remain conventions, not guarantees.
