GoUI – Server-driven UI framework for Go

Hello everyone,

I’d like to share a project I’ve been working on called GoUI.

GoUI is an experimental server-driven UI framework for Go that aims to simplify web application development by keeping the application logic on the server while delivering modern, interactive user interfaces.

Key features include:

• Server-driven rendering
• Stateful components
• Real-time UI updates via WebSockets
• SEO-friendly HTML output
• Minimal JavaScript
• Component-based architecture
• Blade-inspired template engine for Go

The project is still in its early stages, and my primary goal is to gather feedback from experienced Go developers.

I’d really appreciate your thoughts on:

  • The overall architecture
  • API design
  • Developer experience
  • Performance considerations
  • Potential use cases
  • Any limitations or concerns you see

GitHub:

I’m open to any criticism or suggestions. Thanks for taking the time to check it out!

1 Like

It looks like you are serving minimal HTML like <app id=""> to the client and then trigger rendering with javascript, which connects to the server via websocket to retrieve the initial rendering for statefull components?

If this is true, this is not very SEO or first page load friendly. Usually SSR provides the benefit of the browser getting the final HTML & CSS immediately without any javascript processing or additional round-trips on first load. In this way the browser can start layouting and rendering the final page in a streaming fashion, while still receiving the HTML content.

You should prerender the page to provide a complete stable view on first pageload and then hydrate with javascript to add interactivity.

You’re right that this is a real problem for the default mode — but it’s already handled, not overlooked.

GoUI actually has three page modes, chosen per-route:

  • ModeLive (what you’re describing) — empty #app shell, WS connects and does the initial render. This is intentional for internal/authenticated views (admin panels, dashboards) where SEO and first-paint don’t matter and you want the server to own 100% of the render from the first frame.

  • ModeSEO — exactly what you’re asking for: full HTML + <head> meta (title, description, OG tags) is sent on the first response, so the browser can layout/paint immediately with zero JS. The WebSocket then connects in the background and hydrates the existing DOM in place — no re-render, no flash, no extra round-trip before content is visible.

  • ModeStatic — full HTML, no client script, no WebSocket at all. For pages with zero interactivity (legal, about, etc.), it’s a plain HTTP response, as cheap as a static file.

So the mode you’re pushing back on is the deliberate trade-off for stateful internal UIs, not the framework’s general behavior. Public/SEO-sensitive routes use ModeSEO specifically to get the SSR-then-hydrate behavior you described. Same component model, different delivery strategy per route — set with a single parameter on registration.

The “ModeLive” sounds unnecessary, because the server will “own 100% of the render” either way. The only difference between pre-rendering is, that the first rendering happens synchronously on the server and not asynchronously with a separate call.

It is just slower and additional noise with additional network round trips and layout shifts in the browser.

Thanks — this is a fair point, and we agree.

You’re right that the server owns rendering either way. Making ModeLive default to an empty shell + a second async round-trip was mostly extra latency, extra network noise, and layout shift.

We’ve adjusted the product accordingly:

  • With the page renderer, ModeLive now does the first render synchronously on the HTTP response (SSR), then hydrates over WebSocket.

  • Empty-shell ModeLive is still available, but opt-in via DeferFirstRender.

  • ModeLive vs ModeSEO is mainly Head/crawler metadata, not “sync vs async” first paint.

Really appreciate you calling this out — it helped us tighten the defaults. Shipped in v1.1.0.

Calling a project with 5 commits version 1.1.0 probably isn’t going to gain a lot of traction in the go community. The fact that you just changed core functionality shows that this is not at v1. Not to mention stuff like this.

Otherwise, an interesting idea. Also - have you actually dealt with websockets in production? I think it’s going to be a long haul to an actual v1. And in certain environments (Google Cloud Run for example), websockets were such a pain I ended up ripping them out and going back to long-polling HTTP requests. If I were you, I’d try building / deploying / running an application with this. Tools that see actual use tend to solve actual problems in my experience and be more stable.

2 Likes