WebAssembly with Go: Taking Web Apps to the Next Level
Ege Aytin
Posted on October 17, 2023
Author: Tolga Ozen
Introduction
You might've noticed the increasing chatter around WebAssembly (WASM) in the dev community. Its potential is vast, and we've found it invaluable in enhancing our open source project!
Hi everyone, I'm part of the team behind Permify, an open-source infra that helps developers to create and manage granular permissions throughout their applications.
In this article, I'll demonstrate why and how we integrated WebAssembly (WASM) into our Playground and gained benefits from its collaboration with Golang.
What does this playground do? Well, without diving too deep, its a interactive module of Permify which used for creating and testing authorization models.
Throughout this post, I'll be sharing:
- A brief explanation of WASM and the benefits of using it with Go.
- A peek into what spurred our choice to integrate WASM in Permify.
-
WASM Implementation, including
- Quick Warm Up: WASM Implementation with Go
- Deeper Dive: Permify's WASM Code Breakdown
- Frontend: Steps to Embed Go WASM in a React Application
By the end, you should have a clearer understanding of why and how we leveraged WASM's capabilities for our project.
Understanding WebAssembly
WebAssembly (Wasm) has established itself as a pivotal technology, enabling quick and efficient code execution in web browsers and forming a robust bridge between web applications and the high-performance typically associated with native applications.
1. Unveiling WebAssembly:
Wasm acts as a low-level virtual machine, executing a compact binary code that’s translated from high-level languages.
Primary Advantages:
- Universal Browser Support: Thanks to its support from all major browsers, Wasm delivers consistent performance across diverse platforms.
- Near-Native Performance: Intentionally designed to execute binary code at a speed akin to native applications, Wasm enhances the responsiveness of web applications considerably.
In our open-source project, Permify, we strategically incorporated Go (also known as Golang) into its foundational core, selecting it for its widely recognized static typing, concurrency handling, and performance optimization. When the development journey led us to craft the Permify Playground, WebAssembly stepped into the spotlight as a crucial element.
2. Blending Go & WebAssembly:
- Characteristics of Go: Celebrated for its optimal performance and concurrency handling capabilities, Go has carved a sturdy standing within the developer community.
- Synergy with WebAssembly: The translation of Go code into WebAssembly enables developers to effectively utilize Go’s robust performance and concurrency management directly within the browser, propelling the creation of powerful, efficient, and scalable web applications.
Our journey isn't just about melding Go and WebAssembly. Moving forward, we'll unearth why Wasm was pinpointed as the technology of choice for the Permify Playground development and what significant benefits were reaped from this decision.
Why WebAssembly?
The inception of the Permify Playground brought with it a key question: How to showcase our capabilities without being entwined in the complexities and maintenance woes of traditional server architectures? WebAssembly appeared as a shining answer. Adopting this binary instruction format allowed us to:
- Execute In-Browser: Permify's playground could operate straight within the browser, sidestepping server maintenance overheads and repetitive API calls, and notably, making ongoing maintenance a breeze in comparison to older server-based approaches.
- Achieve Peak Performance: Employing WebAssembly ensures that our Go application operates with a level of performance that competes with native applications, enhancing user interactions and bolstering response times.
Harvesting Technical Benefits and Gathering User Feedback
Utilizing WebAssembly in our Permify Playground led us down a path of discernible technical advantages and an embrace from the community:
- Swift Execution: By side-stepping server interactions and deploying WebAssembly in-browser, we've been able to deliver ultra-fast response times.
- Uncomplicated User Interface: Centralizing our playground in the browser, we've dispelled complexities associated with multi-tool workflows, delivering a clean and straightforward user experience.
- Community Validation: The affirming feedback and positive reception from the developer community stand as validation of our technological choices and implementations.
Join us in the following sections as we delve deeper into the technicalities, feedback, and learnings from our adventure, providing a thorough exploration of our endeavours with WebAssembly.
WASM Implementation with Go
Before we explore Permify's use of WebAssembly (WASM) and Go, let's understand their combination in a sample app. What follows is a step-by-step guide to bringing them together, setting the stage for our deeper dive into Permify's implementation.
1. Transforming Go into WebAssembly:
- Steps:
- To get started, ensure you've set the WebAssembly build target in Go:
GOOS=js GOARCH=wasm go build -o main.wasm main.go
- Next, apply optimizations to reduce the file size and enhance performance:
wasm-opt main.wasm --enable-bulk-memory -Oz -o play.wasm
- Handling Events:
Suppose you want your Go function to react to a button click from your web page:
package main
import "syscall/js"
func registerCallbacks() {
js.Global().Set("handleClick", js.FuncOf(handleClick))
}
func handleClick(this js.Value, inputs []js.Value) interface{} {
println("Button clicked!")
return nil
}
In your HTML, after loading your WebAssembly module:
<button onclick="window.handleClick()">Click me</button>
2. Integrating with Web Pages:
- Initializing Wasm:
Ensure you have the wasm_exec.js
script linked, then instantiate your Wasm module:
<script src="wasm_exec.js"></script>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(fetch("play.wasm"), go.importObject).then((result) => {
go.run(result.instance);
});
</script>
- Interacting with the DOM:
Accessing and modifying web elements is fundamental. For instance, changing the content of a paragraph element from Go would look something like this:
func updateDOMContent() {
document := js.Global().Get("document")
element := document.Call("getElementById", "myParagraph")
element.Set("innerText", "Updated content from Go!")
}
3. The Gains: Efficiency & Speed:
- Go’s Goroutines in the Browser:
Imagine having multiple data fetch operations that can run simultaneously without blocking the main thread:
func fetchData(url string, ch chan string) {
// Simulate data fetch.
ch <- "Data from " + url
}
func main() {
ch := make(chan string)
go fetchData("<https://api.example1.com>", ch)
go fetchData("<https://api.example2.com>", ch)
data1 := <-ch
data2 := <-ch
println(data1, data2)
}
Navigating through Go and WebAssembly (WASM) showcases a powerful union, merging Go's concurrent processing with WASM's rapid client-side execution.
The depth explored in our sample app lights the way forward into Permify, where we apply these technological strengths into a scalable, real-world authorization system.
Deeper Dive: Permify's WASM Code Breakdown
Let's dive a bit deeper into the heart of our WebAssembly integration by exploring the key segments of our Go-based WASM code.
1. Setting up the Go-to-WASM Environment
involves preparing and specifying our Go code to be compiled for a WebAssembly runtime.
// go:build wasm
// +build wasm
These lines serve as directives to the Go compiler, signaling that the following code is designated for a WebAssembly runtime environment. Specifically:
-
//go:build wasm
: A build constraint ensuring the code is compiled only for WASM targets, adhering to modern syntax. -
// +build wasm
: An analogous constraint, utilizing older syntax for compatibility with prior Go versions.
In essence, these directives guide the compiler to include this code segment only when compiling for a WebAssembly architecture, ensuring an appropriate setup and function within this specific runtime.
2. Bridging JavaScript and Go with the run
Function
package main
import (
"context"
"encoding/json"
"syscall/js"
"google.golang.org/protobuf/encoding/protojson"
"github.com/Permify/permify/pkg/development"
)
var dev *development.Development
func run() js.Func {
// The `run` function returns a new JavaScript function
// that wraps the Go function.
return js.FuncOf(func(this js.Value, args []js.Value)
interface{} {
// t will be used to store the unmarshaled JSON data.
// The use of an empty interface{} type means it can hold any type of value.
var t interface{}
// Unmarshal JSON from JavaScript function argument (args[0]) to Go's data structure (map).
// args[0].String() gets the JSON string from the JavaScript argument,
// which is then converted to bytes and unmarshaled (parsed) into the map `t`.
err := json.Unmarshal([]byte(args[0].String()), &t)
// If an error occurs during unmarshaling (parsing) the JSON,
// it returns an array with the error message "invalid JSON" to JavaScript.
if err != nil {
return js.ValueOf([]interface{}{"invalid JSON"})
}
// Attempt to assert that the parsed JSON (`t`) is a map with string keys.
// This step ensures that the unmarshaled JSON is of the expected type (map).
input, ok := t.(map[string]interface{})
// If the assertion is false (`ok` is false),
// it returns an array with the error message "invalid JSON" to JavaScript.
if !ok {
return js.ValueOf([]interface{}{"invalid JSON"})
}
// Run the main logic of the application with the parsed input.
// It’s assumed that `dev.Run` processes `input` in some way and returns any errors encountered during that process.
errors := dev.Run(context.Background(), input)
// If no errors are present (the length of the `errors` slice is 0),
// return an empty array to JavaScript to indicate success with no errors.
if len(errors) == 0 {
return js.ValueOf([]interface{}{})
}
// If there are errors, each error in the `errors` slice is marshaled (converted) to a JSON string.
// `vs` is a slice that will store each of these JSON error strings.
vs := make([]interface{}, 0, len(errors))
// Iterate through each error in the `errors` slice.
for _, r := range errors {
// Convert the error `r` to a JSON string and store it in `result`.
// If an error occurs during this marshaling, it returns an array with that error message to JavaScript.
result, err := json.Marshal(r)
if err != nil {
return js.ValueOf([]interface{}{err.Error()})
}
// Add the JSON error string to the `vs` slice.
vs = append(vs, string(result))
}
// Return the `vs` slice (containing all JSON error strings) to JavaScript.
return js.ValueOf(vs)
})
}
Within the realm of Permify, the run
function stands as a cornerstone, executing a crucial bridging operation between JavaScript inputs and Go's processing capabilities. It orchestrates real-time data interchange in JSON format, safeguarding that Permify's core functionalities are smoothly and instantaneously accessible via a browser interface.
Digging into run
:
- JSON Data Interchange: Translating JavaScript inputs into a format utilizable by Go, the function unmarshals JSON, transferring data between JS and Go, assuring that the robust processing capabilities of Go can seamlessly manipulate browser-sourced inputs.
- Error Handling: Ensuring clarity and user-awareness, it conducts meticulous error-checking during data parsing and processing, returning relevant error messages back to the JavaScript environment to ensure user-friendly interactions.
-
Contextual Processing: By employing
dev.Run
, it processes the parsed input within a certain context, managing application logic while handling potential errors to assure steady data management and user feedback. - Bidirectional Communication: As errors are marshaled back into JSON format and returned to JavaScript, the function ensures a two-way data flow, keeping both environments in synchronized harmony.
Thus, through adeptly managing data, error-handling, and ensuring a fluid two-way communication channel, run
serves as an integral bridge, linking JavaScript and Go to ensure the smooth, real-time operation of Permify within a browser interface. This facilitation of interaction not only heightens user experience but also leverages the respective strengths of JavaScript and Go within the Permify environment.
3. Main Execution and Initialization
// Continuing from the previously discussed code...
func main() {
// Instantiate a channel, 'ch', with no buffer, acting as a synchronization point for the goroutine.
ch := make(chan struct{}, 0)
// Create a new instance of 'Container' from the 'development' package and assign it to the global variable 'dev'.
dev = development.NewContainer()
// Attach the previously defined 'run' function to the global JavaScript object,
// making it callable from the JavaScript environment.
js.Global().Set("run", run())
// Utilize a channel receive expression to halt the 'main' goroutine, preventing the program from terminating.
<-ch
}
-
ch := make(chan struct{}, 0)
: A synchronization channel is created to coordinate the activity of goroutines (concurrent threads in Go). -
dev = development.NewContainer()
: Initializes a new container instance from the development package and assigns it todev
. -
js.Global().Set("run", run())
: Exposes the Gorun
function to the global JavaScript context, enabling JavaScript to call Go functions. -
<-ch
: Halts themain
goroutine indefinitely, ensuring that the Go WebAssembly module remains active in the JavaScript environment.
In summary, the code establishes a Go environment running within WebAssembly that exposes specific functionality (run
function) to the JavaScript side and keeps itself active and available for function calls from JavaScript.
Building the Go Code into a WASM Module
Before we delve into Permify's rich functionalities, it's paramount to elucidate the steps of converting our Go code into a WASM module, priming it for browser execution.
For enthusiasts eager to delve deep into the complete Go codebase, don't hesitate to browse our GitHub repository: Permify Wasm Code.
1. Compiling to WASM
Kickstart the transformation of our Go application into a WASM binary with this command:
GOOS=js GOARCH=wasm go build -o permify.wasm main.go
This directive cues the Go compiler to churn out a .wasm
binary attuned for JavaScript environments, with main.go
as the source. The output, permify.wasm
, is a concise rendition of our Go capabilities, primed for web deployment.
2. WASM Exec JS
In conjunction with the WASM binary, the Go ecosystem offers an indispensable JavaScript piece named wasm_exec.js
. It's pivotal for initializing and facilitating our WASM module within a browser setting. You can typically locate this essential script inside the Go installation, under misc/wasm
.
However, to streamline your journey, we've hosted wasm_exec.js
right here for direct access: wasm_exec.
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
Equipped with these pivotal assets - the WASM binary and its companion JavaScript - the stage is set for its amalgamation into our frontend.
Steps to Embed Go WASM in a React Application
1. Setting Up the React Application Structure
To kick things off, ensure you have a directory structure that clearly separates your WebAssembly-related code from the rest of your application. Based on your given structure, the loadWasm
folder seems to be where all the magic happens:
loadWasm/
│
├── index.tsx // Your main React component that integrates WASM.
├── wasm_exec.js // Provided by Go, bridges the gap between Go's WASM and JS.
└── wasmTypes.d.ts // TypeScript type declarations for WebAssembly.
To view the complete structure and delve into the specifics of each file, refer to the Permify Playground on GitHub.
2. Establishing Type Declarations
Inside the wasmTypes.d.ts
, global type declarations are made which expand upon the Window interface to acknowledge the new methods brought in by Go's WebAssembly:
declare global {
export interface Window {
Go: any;
run: (shape: string) => any[];
}
}
export {};
This ensures TypeScript recognizes the Go
constructor and the run
method when called on the global window
object.
3. Preparing the WebAssembly Loader
In index.tsx
, several critical tasks are accomplished:
- Import Dependencies: First off, we import the required JS and TypeScript declarations:
import "./wasm_exec.js";
import "./wasmTypes.d.ts";
-
WebAssembly Initialization: The asynchronous function
loadWasm
takes care of the entire process:
async function loadWasm(): Promise<void> {
const goWasm = new window.Go();
const result = await WebAssembly.instantiateStreaming(
fetch("play.wasm"),
goWasm.importObject
);
goWasm.run(result.instance);
}
Here, new window.Go()
initializes the Go WASM environment. WebAssembly.instantiateStreaming
fetches the WASM module, compiles it, and creates an instance. Finally, goWasm.run
activates the WASM module.
-
React Component with Loader UI: The
LoadWasm
component uses theuseEffect
hook to asynchronously load the WebAssembly when the component mounts:
export const LoadWasm: React.FC<React.PropsWithChildren<{}>> = (props) => {
const [isLoading, setIsLoading] = React.useState(true);
useEffect(() => {
loadWasm().then(() => {
setIsLoading(false);
});
}, []);
if (isLoading) {
return (
<div className="wasm-loader-background h-screen">
<div className="center-of-screen">
<SVG src={toAbsoluteUrl("/media/svg/rocket.svg")} />
</div>
</div>
);
} else {
return <React.Fragment>{props.children}</React.Fragment>;
}
};
While loading, SVG rocket is displayed to indicate that initialization is ongoing. This feedback is crucial as users might otherwise be uncertain about what's transpiring behind the scenes. Once loading completes, children components or content will render.
4. Calling WebAssembly Functions
Given your Go WASM exposes a method named run
, you can invoke it as follows:
function Run(shape) {
return new Promise((resolve) => {
let res = window.run(shape);
resolve(res);
});
}
This function essentially acts as a bridge, allowing the React frontend to communicate with the Go backend logic encapsulated in the WASM.
5. Implementing the Run Button in React
To integrate a button that triggers the WebAssembly function when clicked, follow these steps:
- Creating the Button Component
First, we'll create a simple React component with a button:
import React from "react";
type RunButtonProps = {
shape: string;
onResult: (result: any[]) => void;
};
function RunButton({ shape, onResult }: RunButtonProps) {
const handleClick = async () => {
let result = await Run(shape);
onResult(result);
};
return <button onClick={handleClick}>Run WebAssembly</button>;
}
In the code above, the RunButton
component accepts two props:
-
shape
: The shape argument to pass to the WebAssemblyrun
function. -
onResult
: A callback function that receives the result of the WebAssembly function and can be used to update the state or display the result in the UI.
- Integrating the Button in the Main Component
Now, in your main component (or wherever you'd like to place the button), integrate the RunButton
:
import React, { useState } from "react";
import RunButton from "./path_to_RunButton_component"; // Replace with the actual path
function App() {
const [result, setResult] = useState<any[]>([]);
// Define the shape content
const shapeContent = {
schema: `|-
entity user {}
entity account {
relation owner @user
relation following @user
relation follower @user
attribute public boolean
action view = (owner or follower) or public
}
entity post {
relation account @account
attribute restricted boolean
action view = account.view
action comment = account.following not restricted
action like = account.following not restricted
}`,
relationships: [
"account:1#owner@user:kevin",
"account:2#owner@user:george",
"account:1#following@user:george",
"account:2#follower@user:kevin",
"post:1#account@account:1",
"post:2#account@account:2",
],
attributes: [
"account:1$public|boolean:true",
"account:2$public|boolean:false",
"post:1$restricted|boolean:false",
"post:2$restricted|boolean:true",
],
scenarios: [
{
name: "Account Viewing Permissions",
description:
"Evaluate account viewing permissions for 'kevin' and 'george'.",
checks: [
{
entity: "account:1",
subject: "user:kevin",
assertions: {
view: true,
},
},
],
},
],
};
return (
<div>
<RunButton shape={JSON.stringify(shapeContent)} onResult={setResult} />
<div>
Results:
<ul>
{result.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
</div>
);
}
In this example, App
is a component that contains the RunButton
. When the button is clicked, the result from the WebAssembly function is displayed in a list below the button.
Conclusion
Throughout this exploration, the integration of WebAssembly with Go was unfolded, illuminating the pathway toward enhanced web development and optimal user interactions within browsers.
The journey involved setting up the Go environment, converting Go code to WebAssembly, and executing it within a web context, ultimately giving life to the interactive platform showcased at play.permify.co.
This platform stands not only as an example but also as a beacon, illustrating the concrete and potent capabilities achievable when intertwining these technological domains.
Posted on October 17, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.