How to Supercharge Web Apps with Go and WebAssembly
By Ege Aytin
You might’ve noticed the increasing chatter around WebAssembly (WASM) in the developer community. Its potential is vast, and at Permify, we’ve found it invaluable in enhancing our open source project. In this post, we’ll explore how integrating WebAssembly with Go elevated our web application’s performance and user experience.
I’m part of the team behind Permify — an open-source infrastructure that helps developers create and manage granular permissions throughout their applications. One of our key features is an interactive Playground used for building and testing authorization models. Our decision to integrate WebAssembly into the Permify Playground brought substantial benefits, which we’ll dive into here.
Why Use WebAssembly with Go?
WebAssembly (Wasm) enables fast, efficient code execution in web browsers. It acts as a bridge between high-level languages and near-native performance levels in web apps. Here are the core advantages:
- Universal browser support
- Close-to-native execution speed
Go’s static typing and concurrency strengths make it a solid backend choice, and WebAssembly allowed us to bring these strengths directly into the browser without depending on server-side processing. The result? A highly responsive, maintainable, and interactive Playground experience for our users.
The Benefits of WASM in the Permify Playground
Our key goals for the Playground experience included simplicity, high performance, and direct feedback from users. With WASM, we achieved:
- Zero server-side processing
- Instant, in-browser computation
- Better scalability and portability
- Positive developer feedback and engagement
Getting Started with Go and WebAssembly
Here’s how we implemented WebAssembly in the Playground using Go:
1. Compile Go to WebAssembly
Set the target platform:
GOOS=js GOARCH=wasm go build -o main.wasm main.go
Optimize the binary:
wasm-opt main.wasm --enable-bulk-memory -Oz -o play.wasm
2. Handle Events from JavaScript
Example Go code to register a button click:
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
}
Corresponding HTML:
<button onclick="window.handleClick()">Click me</button>
3. Initialize the WebAssembly 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>
4. DOM Interaction from Go
func updateDOMContent() {
document := js.Global().Get("document")
element := document.Call("getElementById", "myParagraph")
element.Set("innerText", "Updated content from Go!")
}
5. Use Goroutines in the Browser
func fetchData(url string, ch chan string) {
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)
}
Permify’s WASM Code Structure
1. WASM-Specific Build Tags
//go:build wasm
// +build wasm
Ensures this code is compiled only for WebAssembly targets.
2. Running JS-to-Go Functions
A function run
marshals JSON from JavaScript, processes it in Go, then returns a response JSON string, enabling two-way data exchange between JavaScript and Go.
3. Main Execution
func main() {
ch := make(chan struct{}, 0)
dev = development.NewContainer()
js.Global().Set("run", run())
<-ch
}
This keeps the WebAssembly module alive so it can be interacted with via JavaScript.
Embedding WASM in a React Application
1. Directory Structure
loadWasm/
├── index.tsx
├── wasm_exec.js
└── wasmTypes.d.ts
2. TypeScript Declarations
declare global {
export interface Window {
Go: any;
run: (shape: string) => any[];
}
}
export {};
3. Load WASM in React Component
async function loadWasm(): Promise<void> {
const goWasm = new window.Go();
const result = await WebAssembly.instantiateStreaming(fetch("play.wasm"), goWasm.importObject);
goWasm.run(result.instance);
}
Called within a React component’s useEffect
to load WASM when the component mounts.
4. Using the run Function
function Run(shape) {
return new Promise((resolve) => {
resolve(window.run(shape));
});
}
5. Button Component to Execute WASM
function RunButton({ shape, onResult }) {
const handleClick = async () => {
let result = await Run(shape);
onResult(result);
};
return <button onClick={handleClick}>Run WebAssembly</button>;
}
6. Main App Integration
const shapeContent = {
schema: "...",
relationships: [...],
attributes: [...],
scenarios: [...]
};
function App() {
const [result, setResult] = useState([]);
return (
<div>
<RunButton shape={JSON.stringify(shapeContent)} onResult={setResult} />
<ul>
{result.map((item, i) => <li key={i}>{item}</li>)}
</ul>
</div>
);
}
Conclusion
We explored the integration of Go and WebAssembly, providing both a high-level overview and a technical walkthrough. By combining the performance of Go with the efficiency of WebAssembly, we brought powerful server-side functionality into the browser. This approach has simplified our development process, improved responsiveness, and provided a flexible platform for modeling permissions with Permify’s Playground.
If you’re considering building web applications that require both performance and rich interactivity, leveraging Go with WebAssembly is a compelling choice.