Supercharging Vue.js with WebAssembly and Java

This article explores the integration of WebAssembly with Vue.js using Bytecoder, a powerful Java bytecode transpiler. It demonstrates how to bridge the gap between Java development and modern web frameworks by compiling Java code to WebAssembly while maintaining seamless integration with Vue.js. The article covers the technical aspects of WebAssembly as a compile target, introduces Bytecoder's capabilities, and provides practical examples of implementing Vue.js components using Java code that compiles to WebAssembly. This approach combines the benefits of Java's strong typing and development tools with Vue.js's reactive UI framework, all while achieving the performance benefits of WebAssembly.

5 Minutes reading time

Behold the masterpiece that AI hallucinated while reading this post:

"How Little Java Learned to Dance with Vue.js in the Web Browser"

(after I fed it way too many marketing blogs and memes)

Created using DALL-E 3

AI-Generated: How Little Java Learned to Dance with Vue.js in the Web Browser

State of the union

WebAssembly for starters is a standard for high-performance, size-optimized executable code for the Web. It was released in March 2017 by the WebAssembly working group as version 1.0, which is basically a MVP (Minimum Viable Product), but the initial development started in June 2017. WebAssembly is the successor asm.js and the Google Native Client (NaCL).

WebAssembly itself is merely a sandboxed runtime environment. It was designed as a compile target. High level languages such as C/C++, Rust, .NET or Java can be compiled to WebAssembly. As a compile target, it implements a set of binary opcodes, a module system and a stack machine making the opcodes executable. Modern compiler can translate and optimize high level languages to low level WebAssembly modules.

WebAssembly 1.0 is a MVP. As a Minimum viable product it is focused on an extensible core system. The core supports the mentioned instruction set, has support for modules and a virtual, linear memory and a limited set of data types such as integers and floating point numbers. It does not support high level structures such as arrays and objects. It also does not implement memory management. All those things must be implemented by the language emulation layer generated by the compiler. The MVP does also not support direct browser API interaction. This makes it tricky to write applications for the Web, as basically all known applications need some kind of user interaction, which is HTML and JavaScript. A compiler therefore needs to generate runtime linkage code, which emulates or delegates to the browser API and makes them available to the WebAssembly bytecode using the WebAssembly module system. In the following chapters I want to show you a compiler which can compile Java Bytecode to WebAssembly. In this short demo, I will also show you how user interaction can be implemented by WebAssembly based on a modern framework: vue.js. Say hello to Bytecoder!

Bytecoder

Bytecoder is a rich domain model for Java(JVM) bytecode and framework to interpret and transpile it to other languages such as JavaScript, OpenCL or WebAssembly. Its high level goals are:

  • Ability to cross-compile JVM bytecode to JavaScript, WebAssembly, OpenCL and other languages

  • Primary compile targets are JavaScript and WebAssembly

  • Allow integration with other UI-Frameworks such as vue.js

  • Use other tool chains such as Google Closure Compiler to further optimize generated code

  • Backed by OpenJDK 11 as JRE Classlib

The JVM bytecode is parsed and transformed into an intermediate representation. This intermediate representation is passed thru optimizer stages and sent to a backend implementation for target code generation.

The JavaScript backend transforms the intermediate representation into JavaScript.

The WebAssembly backend transforms the intermediate representation into WebAssembly text and binary code.

The OpenCL backend is used to compile single algorithms into OpenCL and execute them on the GPU. This backend is designed to enhance existing programs running on the JVM to utilize the vast power of modern GPUs.

Ok, Bytecoder can compile Java/JVM bytecode to WebAssembly. How can I do some user interaction, and can I use my favorite UI framework for this? Yes, you can! For this demo, I will use vue.js.

Vue.js Bytecoder integration

Bytecoder comes with a OpenJDK 11 classlib. Unfortunately there are two parts for successful vue.js and WebAssembly integration missing. We do not have a vue.js Java API, and we do now have a way to instruct the compiler to generate the required WebAssembly runtime linkage code to make vue.js available to the WebAssembly sandbox.

But there is rescue. Bytecoder has a OpaqueReferenceType API. This API is a set of Java interfaces to model the interaction with browser APIs such as the DOM or third party libraries such as vue.js. The OpaqueReferenceType API is based on the WebAssembly Reference Types Proposal, which might be integrated someday into the next official WebAssembly release. So now, show me the money! How can be write a program that can be compiled to WebAssembly with effective vue.js binding? Well, here it is!

import de.mirkosertic.bytecoder.api.OpaqueProperty; (1)
import de.mirkosertic.bytecoder.api.vue.Vue;
import de.mirkosertic.bytecoder.api.vue.VueBuilder;
import de.mirkosertic.bytecoder.api.vue.VueEventListener;
import de.mirkosertic.bytecoder.api.vue.VueInstance;
import de.mirkosertic.bytecoder.api.web.ClickEvent;

public class VueDemo {

    public interface MyVueInstance extends VueInstance {

        @OpaqueProperty
        void welcomemessage(String aNewMessage);
    }

    public static void main(String[] args) { (2)

        VueBuilder theBuilder = Vue.builder(); (3)
        theBuilder.bindToTemplateSelector("#vuetemplate"); (4)
        theBuilder.data().setProperty("welcomemessage", "hello world!"); (4)
        theBuilder.addEventListener("clicked", new VueEventListener() { (5)
            @Override
            public void handle(MyVueInstance instance, ClickEvent event) {
                instance.welcomemessage("Timestamp is " + System.currentTimeMillis());
            }
        });
        MyVueInstance instance = theBuilder.build(); (6)
    }
}
1We need to import the Bytecoder OpaqueReference API
2The main method is the entry point for the WebAssembly program
3A VueBuilder is used to create one or more vue.js component instances
4The builder is bound to a HTML template and initialized with some core data properties
5We can also do callbacks from the vue.js API to WebAssembly. The binding code is generated by the compiler.
6This will summon the full vue.js magic

This program also needs a HTML template:

<style>
    [v-cloak] {
        display: none;
    }
</style>
<div v-cloak id="vuetemplate">  (1)
    <h1>Hello, this is a vue.js instance running with WebAssembly</h1>
    <span>Current message : {{welcomemessage}}</span>
    <button v-on:click="clicked">Click me to change the message!</button> (2)
</div>
1HTML element with id "vuetemplate" is referenced in the Java code
2Click listener "clicked" was registered in the Java code

The Bytecoder API also needs the implementation logic for the VueBuilder interface:

bytecoder.imports.vue = {
    builder : function() {
        var builder = {
            config : {
                data: {
                    setProperty: function(name, value) {
                        this[name] = value;
                    }
                },
                methods: {
                },
            },
            bindToTemplateSelector: function(aSelectorStr) {
                this.config.el = aSelectorStr;
            },
            data: function() {
                return this.config.data;
            },
            addEventListener: function(eventName,listenerFunction) {
                this.config.methods[eventName] = function() {
                    var args = Array.prototype.slice.call(arguments);
                    args.unshift(this);
                    listenerFunction.apply(this, args);
                }
            },
            build: function() {
                var v = new Vue(this.config);
                v.setProperty = function(name, value) {
                    v[name] = value;
                };
                return v;
            }
        };
        return bytecoder.toBytecoderReference(builder);
    }
};

This import is an example of the WebAssembly module system. WebAssembly allows us to import functionality from outside of its sandbox and call it at runtime.

Conclusion

Bytecoder makes it easy to use a wide spread high level language and compile it to high efficiency runtime environments like WebAssembly. The OpaqueReferenceType API combined with modern frameworks like vue.js allows us to fill the missing parts of the WebAssembly core specification. The Bytecoder transpiler is a great tool to keep developer productivity high by using high level languages and also keep runtime costs low. WebAssembly rocks!

Git revision: 2e692ad