Go vs Next.js: Why We Never Fell for the Hype
Developers were sold a lie. They've been told, over and over, that Next.js is the modern choice. That JavaScript and TypeScript are the default. That if you're not building on a JS framework, you're living in the past.
This lie is old. It dates back to the PHP era — a long time ago — when JavaScript frameworks genuinely were a step up. PHP powered 70% of the web by 2004. It was slow, messy, and painful to maintain. React, Vue, and the ecosystem around them gave you better developer experience, better tooling, better structure. The lesson everyone internalized was: JavaScript frameworks are better. End of story.
But times changed. The lie didn't.
If you listen to the hype, the answer to "Go or Next.js?" is obvious: Next.js. It's modern. It's JavaScript. It's what everyone uses. So I decided to audit that answer — not with opinions, but with research. I looked at what we actually use, and I looked at the data. The standard Next.js advantages — frontend DX, talent pool, SEO, streaming SSR, component reuse — I checked each one against a real Go tech stack and against published benchmarks.
The sold answer was wrong. Not because Next.js is bad — it's a fine framework — but because the comparison was happening in the abstract, instead of against what real Go projects actually use and what the research actually shows.
So let me walk through each claim honestly.
Claim 1: "Next.js is better for SEO"
Here's the mental model that was wrong:
Next.js → server-rendered → good for SEO
Go → ??? → maybe worse?
But the Go templates are also server-rendered. They render full HTML on the server, every request. This isn't a client-side SPA that Google has to execute JavaScript to see. It's complete HTML, right out of the response.
So the real comparison is:
Next.js → server-rendered HTML → good for SEO
Go → server-rendered HTML → good for SEO
Same thing. And on raw speed — time to first byte — Go is faster. No V8 startup, no React reconciliation overhead, no hydration mismatch risk. Go controls every byte.
The SEO advantage Next.js has isn't over Go — it's over client-side rendering (CSR). And CSR isn't just "less good for SEO" — it's an active disadvantage for any public-facing website. Here's why that matters in 2026:
Googlebot renders JS with: 9 second median delay
AI crawlers (GPTBot, etc): do NOT render JS at all
Server-rendered HTML: indexed immediately, no delay
If your site renders content client-side, Google sees an empty page for 9 seconds before it can index anything. AI crawlers like GPTBot and ClaudeBot don't render JavaScript at all — they see only the HTML response. A CSR site is invisible to them. That means zero citations in ChatGPT, zero visibility in AI search, zero traffic from the fastest-growing discovery channel. For a public-facing website, CSR isn't a tradeoff — it's a liability.
Server-rendered HTML — whether from Next.js or Go — is what both Google and AI crawlers need. Go was always server-rendered. Next.js had to add SSR to catch up to what server-rendered languages always did.
Next.js does have a nice metadata API. That's convenient. But Go already hand-rolls all of that. The Next.js version saves some boilerplate. It doesn't give us a capability Go lacks.
Verdict: a wash, or slight Go edge.
Claim 2: "Next.js has streaming SSR"
Next.js has React Suspense. You can stream HTML chunks to the browser as data becomes available. That's a real feature!
But Go has http.Flusher. You can flush the response writer
whenever you want. The reason we don't use it is simpler: Go pages
render in milliseconds. There's nothing to stream. The whole page is
done before you'd finish setting up the first Suspense boundary.
And when you do need real-time streaming, Go has first-class WebSocket
support in the standard library. gorilla/websocket and
nhooyr.io/websocket are mature, battle-tested, and used
in production at scale. No extra runtime, no framework abstraction,
no socket.io layer. Just a goroutine per connection and
a channel. That's the Go way.
Verdict: a solution to a problem Go doesn't have.
Claim 3: "Next.js has hot reload"
Go uses air. It watches the .go,
.html, .css, and .js files,
rebuilds the binary, and restarts the server. Takes about a second.
Air has 20,000+ GitHub stars and is the standard hot-reload tool for
Go.
Verdict: equal.
Claim 4: "Next.js has a bigger talent pool"
True. Next.js has a bigger pool. But bigger isn't better. The JavaScript talent pool is flooded with developers who can wire up React components but lack basic computer science fundamentals — heap vs stack, pointers, memory allocation, concurrency models. They know the framework. They don't know what's underneath it. When something breaks at runtime, when memory leaks, when the event loop stalls, they can't debug it because they've never been below the abstraction.
And low quality has a compounding cost. Once a project grows beyond a few components, that lack of fundamentals turns into a large bowl of spaghetti code — props drilled ten levels deep, state scattered across contexts, effects triggering effects, no clear ownership of data flow. Nobody can refactor it because nobody understands the full picture. The framework was supposed to impose structure. Instead it became the structure, and the structure is spaghetti.
Go developers tend to come from systems backgrounds. They understand goroutines, channels, memory layout, and garbage collection because Go makes those concepts explicit. The language teaches you to think about them. A smaller pool, but deeper.
Verdict: a bigger pool makes good developers impossible to find. More noise, more resumes, more interviews to filter through. The signal-to-noise ratio is worse, not better. And the good developers — the ones who saw through the hype early — already moved on. They're writing Go, Rust, or something else. They're not in the Next.js pool anymore.
Claim 5: "Next.js has a metadata API"
As I mentioned above — we already hand-roll meta tags, canonical URLs, and JSON-LD. It works. The Next.js version is a convenience wrapper around the same thing.
Verdict: convenience, not capability.
Claim 6: "Next.js has components"
This is the one I was most sure about. The argument: Next.js gives you React components — typed, composable, reusable. Go just concatenates HTML strings.
Except modern Go doesn't concatenate strings. Go uses libraries like HB — a Go HTML builder with a fluent API. Here's what a blog card component looks like in our actual codebase:
card := hb.Div().
Class("blog-card h-100").
Child(postImage).
Child(hb.Div().
Class("blog-card-content").
Child(postTitle).
Child(postSummary)).
Child(hb.Div().
Class("blog-card-meta").
Child(postPublished))
link := hb.Hyperlink().
Class("blog-card-link").
Href(postURL).
Child(card)
That IS a component system. It's typed. It's composable. It's testable. Its reusable across the entire codebase. And it's the same language as the backend — no serialization boundary, no prop drilling, no hydration step.
The mental model comparison:
Next.js: build component tree → serialize to HTML → hydrate on client → reconcile
Go + HB: build component tree → render to HTML → done
One language. One process. One mental model. The server builds the tree, renders it, and sends it.
And HB is not alone. The Go ecosystem now has multiple mature component systems:
templ-components: 97 server-rendered components, templ + HTMX + Tailwind
gomponents: HTML components in pure Go
GMX: Vue-style single-file components compiled to Go
All: type-safe, server-rendered, no Node.js build step
Verdict: Go already has components.
Claim 7: "But what about rich interactivity?"
Fair point. Server-rendered components are great for content pages, but some parts of an application need real client-side interactivity. A dashboard, for example.
There are many options for client-side interactivity in Go. Some prefer HTMX and Alpine.js for simple, declarative interactivity. I prefer Vue.js for more complex, stateful interactions. for those parts. Not the whole site — just where it matters. I load Vue from a CDN — no build step, no npm dependency, no bundler. The rest of the site is server-rendered Go, and even the pages that use Vue are still multipage, not a single-page application.
Here's the architecture:
Content pages → Go server-rendered HTML (fast, simple, cacheable)
Interactive UI → Vue.js via CDN (rich, stateful, no build step)
This is actually a better architecture than a full Next.js app. You get the simplicity and speed of server rendering where it works, and the interactivity of a client framework where it matters. No SPA, no build pipeline, no client-side bundle for every route.
Verdict: already solved, with a better architecture.
The Concrete Difference: Docker
This is where the comparison stops being abstract and gets tangible.
Our Go Dockerfile:
FROM golang:1.27 AS builder
WORKDIR /app
COPY website/go.* ./
RUN go mod download
COPY . ./
WORKDIR /app/website
RUN CGO_ENABLED=0 GOOS=linux go build -v -o server ./cmd/server
FROM alpine:3
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /app/website/server /server
CMD ["/server"]
A typical Next.js Dockerfile:
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]
A 2026 benchmark built the same app in both stacks and measured:
On Google Cloud Run, where we deploy, cold starts tell the same story:
CGO_ENABLED=0 gives us a fully static, native binary.
No C dependencies. No runtime. No interpreter. No virtual machine.
Just compiled machine code that runs directly on the CPU. It runs
anywhere — Alpine, scratch, whatever. And there are no runtime
CVEs to patch, because there's no runtime.
And here's the part the hype doesn't mention: Go compiles fast. Not
"fast for a compiled language" — fast. A full build of this
project takes seconds, not minutes. The compiler is a single tool
with no configuration, no plugins, no presets. You run
go build and you get a binary. That's it.
For us, running on cloud infrastructure with cold starts, this is a real cost and performance difference. Every cold start, the Go binary is ready in milliseconds. The Next.js server has to boot Node, load the module graph, and warm up. Every time.
And it's not just Cloud Run. We also deploy to self-hosted instances — small VPS boxes, edge servers, even a Raspberry Pi. That's where the size and memory difference really pays off. A 15 MB Go binary using 10 MB of RAM means you can run a dozen apps on a $5 box. A 200 MB Next.js image using 80 MB of RAM means you get one, maybe two, before the box chokes. Smaller containers and less memory means more apps on the same hardware. That's not a micro-optimization — it's the difference between $5/month and $50/month, or between one server and five.
But the real deployment difference isn't the image size. It's the
artifact. Go deploys one file. A single binary. You
scp it to the server, run it, and you're done. No
node_modules tree with thousands of packages. No
package-lock.json. No npm ci step. No
transpilation. No bundling. No minification. No tree-shaking. No
polyfills. One file, compiled, ready to run.
Next.js deploys thousands. The node_modules directory
alone for a typical Next.js project contains hundreds to thousands
of packages — each one a potential attack surface, a potential
bug, a potential breaking change on the next npm install.
Every dependency is a dependency you didn't write, don't fully
understand, and can't fully audit. Go's standard library covers HTTP,
JSON, crypto, compression, templating, testing — you need
fewer dependencies, and the ones you do have are fewer and more
auditable.
Security: The Part Nobody Talks About
The hype says JavaScript is safe because it's mature. The data says otherwise.
By Q2 2026, Sonatype logged 1.8 million malicious packages across open source ecosystems. npm accounted for 96.6% of them:
The Shai-Hulud worm, first seen in September 2025, automated the compromise and redistribution of npm packages. A single worm event in May 2026 generated 226 malicious packages in one month. This is not theoretical.
Here's why Go is structurally safer:
Go npm
Install scripts: None. go get runs no scripts. preinstall/postinstall run arbitrary code
Integrity: Merkle-tree checksum log Lockfile hashes (weaker)
Registry: Decentralized (source URLs) Central accounts (one phish = many packages)
Version resolution: Explicit upgrades Semver ranges (auto-resolve to bad patches)
Then there are the runtime CVEs:
With Go, you recompile with a patched toolchain and the binary is clean. There is no runtime to patch. With Node.js, the runtime is in your Docker image, in production, for the lifetime of the application.
Go's scratch image has zero attack surface: no shell, no package manager, no SSH. Nothing for an attacker to pivot from.
The Final Scorecard
| Next.js selling point | What we have | Verdict |
|---|---|---|
| SSR / SEO | Go server-rendered templates (faster TTFB, AI-crawler friendly) | Wash, slight Go edge |
| Hot reload | air (20,000+ stars, ~1s rebuild) | Equal |
| Components | HB + templ/gomponents/GMX ecosystem | Already solved |
| Rich interactivity | Vue.js via CDN where it matters | Better architecture |
| Metadata API | Hand-rolled, already works | Convenience, not capability |
| Talent pool | Smaller pool, deeper fundamentals (goroutines, memory, concurrency) | Worse signal-to-noise ratio |
| Streaming SSR | http.Flusher if we ever need it |
Problem we don't have |
| Supply chain security | No install scripts, Merkle-tree integrity, 0 CVEs exploited | Go wins decisively |
| Docker image | ~15-25 MB vs ~150-300 MB | Go wins (14x smaller) |
| Deployment artifact | 1 binary vs thousands of npm packages | Go wins decisively |
| Native binary | Compiled machine code vs interpreted JS on V8 | Go wins |
| Build speed | Seconds, no config vs minutes, bundlers/transpilers | Go wins |
| Memory | 10-15 MB idle vs 30-80 MB | Go wins (50-70% less) |
| Cold start | 60-80ms vs 300-400ms on Cloud Run | Go wins (5x faster) |
And we keep what Next.js can't give us: a single binary, the
dracory/* stack — roughly 50 reusable modules
representing years of accumulated leverage — goroutine-level
concurrency, no Node runtime, no build pipeline, zero runtime CVEs, and
full control over every byte.
The Verdict
There's no case for Next.js. Every advantage the industry repeats was either wrong (SEO), already solved (hot reload, components, metadata), or irrelevant (talent pool). When you audit each point against what you actually use — and against the research — Go wins on every axis that matters.
The hype says: JavaScript frameworks are modern, everything else is legacy. The reality says: the PHP era ended a long time ago. The alternatives caught up and then pulled ahead. Go gives you a native binary, fast compilation, faster developer experience, better security, simpler deployment, and a component system that doesn't need a build step or a runtime. The research is clear: 14x smaller Docker images, 16x less memory, 5x faster cold starts, one binary vs thousands of npm packages, structural immunity to the supply chain attacks that dominate npm.
The lesson here is simple: framework comparisons are written for a generic audience. They compare features in the abstract. But you don't build in the abstract — you build with a specific stack, on a specific platform, with specific constraints. The only honest comparison is the one that checks each claim against what you have.
The lie is strong. The research is stronger.



