Go's ecosystem, respected
Routing is net/http’s own ServeMux — same patterns, wildcards, and
precedence — and handlers stay plain http.HandlerFunc, so the middleware,
httptest tests, and http.Handlers you already have keep working.

Two aims hold this framework together. The first is to make the naive
implementation the fast and safe one: write the obvious net/http handler and
get the performance and the security posture you would otherwise have to go back
and add. The second is to make the web frontend a first-class thing to build
in Go, rather than the half of an application that leaves for the JavaScript
ecosystem at the JSON boundary.
Both rest on the unit Go already gives you — one http.Handler that binds a
request, runs application logic, and writes a complete response through ordinary
navigation and form semantics. No component graph, patch protocol, hydration
step, or browser runtime, which is also what keeps the result a small, practical
TinyGo target.
Go's ecosystem, respected
Routing is net/http’s own ServeMux — same patterns, wildcards, and
precedence — and handlers stay plain http.HandlerFunc, so the middleware,
httptest tests, and http.Handlers you already have keep working.
One command, then typed Go
pw init scaffolds a complete project — handler, typed page, document
shell, SQL, migration, error pages, Devbox — and pw dev watches it. Behind
both, pw generate compiles templates, SQL, request binding, and OpenAPI
into typed Go, so a renamed template argument or a SELECT that no longer
matches its result type is a build error. Nothing is rediscovered by
reflection at request time, which is what lets the same code run on
TinyGo.
The frontend, written in Go
A .pw.html file compiles to a component
with typed parameters and scoped styles, Tailwind is one pw add tailwind
away, and a route can be a directory. Async and live rendering stream a page
around the query still running, and a same-origin link is answered with only
the regions whose markup differs — all of it server-rendered HTML.
Modern web standards, preset
Validated security headers and CSRF, WebAuthn passkeys, negotiated zstd and gzip compression, and OpenTelemetry traces reaching each SQL statement — every one a switch in configuration rather than a stack to assemble.
Need richer interactions later? An opt-in server-driven UI layer extends the
same net/http foundation rather than replacing it. If an application does not
import that layer, it does not pay for it.
examples/todo
holds one todo list written twice against one PostgreSQL table — once with
net/http, html/template, encoding/json and pgx, once with Popcorn Web.
Same five routes, same rows, and the harness refuses to measure them if their
JSON bodies disagree.
Throughput first, k6 at 20 VUs against the same seeded 50 rows, medians of five alternating passes. The framework result depends almost entirely on where the session lives:
| requests/s | HTML p95 | |
|---|---|---|
net/http + pgx |
~14,700 | 3.15–3.39 ms |
| Popcorn Web, cookie session | ~13,700 | 2.90–3.21 ms |
| Popcorn Web, server-side session | ~15,800 | 2.46–2.66 ms |
Same application code in both framework rows, and the framework is quicker to
first byte in both. A cookie session puts the whole record on the wire twice per
request — 631 bytes of Set-Cookie against 299 — and that, not the AES-GCM
sealing it, is what costs the throughput: the crypto measures 0.5 µs.
Per request the framework spends 203 µs of CPU against the baseline’s 221, while also running a session, a CSRF check, security headers and a request ID the hand-written service does not have. Its whole middleware chain is 2.7 µs, against 37 µs for one loopback query. Performance has the full breakdown and the four other settings worth tuning.
All of that has to be linked in, and the binary is where you would expect to pay for it. Go 1.26.5 and TinyGo 0.41.1 on darwin/arm64:
| Build | net/http + pgx |
Popcorn Web |
|---|---|---|
host go build |
18.98 MB | 19.66 MB |
host -ldflags="-s -w" |
13.06 MB | 13.57 MB |
| TinyGo native | does not build | 6.13 MB |
TinyGo native -no-debug |
does not build | 6.13 MB |
TinyGo wasip1 |
does not build | does not build |
Read the stripped row as the price list. Sessions, CSRF, security headers, negotiated compression, OpenTelemetry spans down to each SQL statement, the generated templates and the generated queries together come to 0.51 MB over a service that has none of them — 3.9%, for everything the throughput table above was already running.
The TinyGo rows are where the table stops being a comparison. pgx does not
compile there at all — it reaches for tls.Conn, tls.X509KeyPair and
net.Resolver, none of which TinyGo has — so the framework’s rebuilt driver
stack is what makes the column exist, and it more than halves the binary against
a stripped host build.
Neither service reaches wasip1, and for the framework that is a platform
limit rather than missing work: the PostgreSQL engine requires TinyGo’s threaded
scheduler, because under the cooperative one a blocking socket call holds the
runtime and a query outlives its context deadline in silence. WASI preview 1 has
no threads, so the engine refuses to build there — by way of a deliberate
compile error whose identifier names the fix.
Why Popcorn Web takes both aims in full: what the naive implementation gets for free, and what it takes for frontend work to belong in Go.