Guide / 11 β€” Isomorphic Wasm

goscript client

The <goscript client> block contains Go logic compiled into WebAssembly via TinyGo. It executes within the browser, without any server requests.

⚠️ Note on Compilation Performance

Unlike server-side code, which is compiled almost instantaneously, WebAssembly compilation by TinyGo can take between 1 and 5 seconds depending on the complexity of your scripts and imports.

In development mode (stew run dev), it is normal to experience a slight delay between saving your file and the changes taking effect in the browser. If your modifications don't seem to be reflected immediately, wait a few seconds for the Wasm binary to be ready.

Basic Syntax (Signals)

<goscript client>
    import "stew/state" // Import the Signals package

    // Reactive variables (Signals)
    count := state.New(0)

    // Computed / Effect: Automatically recalculated (Auto-Tracking)
    state.Effect(func() {
        console.Log("The counter is at:", count.Get())
    })
</goscript>

<div bind:content={{ count.Get() }}>0</div>
<button on:click={{ count.Set(count.Get() + 1) }}>+</button>
<button on:click={{ count.Set(count.Get() - 1) }}>-</button>
Note: Thanks to stew/state, the DOM only refreshes when a specific Signal is updated, greatly optimizing CPU performance.

What the Compiler Generates

From the page above, Stew generates a temporary file /tmp/stew_wasm/stew_main_wasm_pages_page.go:

package main

import (
    "github.com/ZiplEix/stew/sdk/wasm"
    "github.com/ZiplEix/stew/sdk/wasm/state"
)

func main() {
    // Code from the <goscript client> block
    count := state.New(0)

    state.Effect(func() {
        console.Log("The counter is at:", count.Get())
    })

    // Automatically generated bindings
    wasm.BindBlock("stew-bind-page-0", func() string { return fmt.Sprint(count.Get()) })
    wasm.OnEvent("stew-bind-page-1", "click", func() { count.Set(count.Get() + 1) })

    wasm.StartReactivityLoop() // Keeps the Wasm main thread open

    _ = count
}

This file is then compiled with:

tinygo build -target wasm -no-debug -o ./static/wasm/pages_page.wasm ...

Wasm Binary Naming

The .wasm file is named after the relative path of the source .stew file to avoid collisions:

Source File Generated Wasm Binary
pages/@page.stewstatic/wasm/pages_page.wasm
pages/blog/@page.stewstatic/wasm/pages_blog_page.wasm
pages/users/__id__/@page.stewstatic/wasm/pages_users___id___page.wasm

Retrieving Server Data (DX)

To avoid JSON unmarshaling boilerplate, Stew offers a virtual import stew/data which automatically exposes a global data variable.

<goscript client>
    import "stew/data"

    console.Log("Page URL:", data.URL)
    userId := data.Params["id"]
</goscript>
πŸ“š Full Reference: Check the Wasm SDK guide for a complete list of available fields and impact on binary size.

Performance and Reactivity

Push-Based Architecture

Unlike some frameworks that check 60 times per second if your variables have changed (Dirty Checking / Polling), Stew's Signal system (stew/state) is completely inert and passive.

  • The DOM is only repainted when a .Set() is called.
  • Absolutely 0% CPU is wasted at rest.
  • Partial diff calls (via Stew's internal Idiomorph integration) are granular and limited to the blocks impacted by a specific Signal.

Isolation and Scopes

Stew hermetically separates server-side execution (SSR) from client-side execution (Wasm). Here are the golden rules:

πŸ–₯️ goscript server (SSR)

  • Binary: Native (Linux/macOS/Windows)
  • Capabilities: File access, DB, full network access
  • Lifecycle: Executed once per request
  • Scope: Not accessible by on: / bind: bindings

🌐 goscript client (Wasm)

  • Binary: WebAssembly (W3C Standard)
  • Capabilities: Sandboxed, DOM, Navigation/IO SDKs
  • Lifecycle: Infinite reactivity loop
  • Scope: The exclusive scope for bindings

Why are my server variables "undefined"?

If you declare user := ... in a server block and try to use on:click={{ user.Save() }}, the Wasm compilation will fail. The bind: and on: bindings are compiled directly into the Wasm binary. They can only see what is imported or declared within the <goscript client> block.

βœ… Recommendation: Communication

To pass data from the server to a client-side binding:
1. Store it in data.Store on the server side.
2. Retrieve it via import "stew/data" on the client side.
3. Bind the resulting client variable to your HTML.