Union types are erased to their upper bound, so there’s no boxing (except where necessary).
You can use
:settings -Vprint:erasure in the REPL to show the output of the “erasure” phase, which happens “just” before the java bytecode is emitted.
Here are some examples:
def f(x: String | Seq[String]) = () /* becomes f(x: java.lang.Object) */
def f(x: Int | Long) = () // becomes f(x: java.lang.Object) too, so here the parameter is boxed
class C1 extends Parent
class C2 extends Parent
def f(x: C1 | C2) = () // becomes f(x: Parent)
edit: To conclude, choosing to use union types or not is not a matter of performance. It depends on what you’re trying to achieve Using union types allows to put more restriction on the types in a very handy way.