The Zero-JS Journey: Optimizing Site Load Times
When I set out to rebuild this server from scratch, I had exactly one goal in mind: speed.
Modern web development has a bad habit. Frameworks like Next.js, Nuxt, and SvelteKit are fantastic for complex applications with highly interactive state, but they have infected the blogosphere. Why send a megabyte of React DOM hydration down the wire for a page that is statically reading text?
The Problem with Hydration
Hydration is the process of sending static HTML across the wire, followed immediately by sending a JavaScript bundle that attaches event listeners to everything. This hurts TTI (Time to Interactive) significantly.
“A document should be readable the moment the HTML arrives, not two seconds later after the vendor bundle parses.” — Me, yesterday.
Take a look at a typical SPA blog setup:
<!-- The hydration problem -->
<html>
<head>
<script defer src="/assets/framework.123.js"></script>
<script defer src="/assets/vendor.456.js"></script>
<script defer src="/assets/hydration-root.789.js"></script>
</head>
<body>
<div id="app"><!-- Content loads here eventually --></div>
</body>
</html>
The browser must download, parse, compile, and execute JS before the user can effectively use the page or click links.
The Solution: SSG (Static Site Generation)
By switching my stack to purely static generation alongside a bare-bones Go server or Nginx proxy, we solve this immediately.
The strategy:
- Parse Markdown files.
- Render Syntax Highlighting using Shiki (Astro does this beautifully) on the Node backend during build time.
- Render Math using KaTeX to static HTML.
- Output raw
.htmlfiles.
There is zero client-side JS used on this website. Don’t believe me? Disable JavaScript in your browser settings right now and reload. Everything still works, including code highlighting and math rendering.
Performance Comparisons
Here is a performance comparison table of my old payload vs my new payload:
| Metric | Old (React SPA) | New (Static) | Improvement |
|---|---|---|---|
| First Contentful Paint | 1.8s | 0.2s | 88% |
| Largest Contentful Paint | 2.1s | 0.3s | 85% |
| Total Blocking Time | 150ms | 0ms | 100% |
| JavaScript Payload | 250KB | 0KB | 100% |
Before and After: The Visual Proof
It’s one thing to show numbers, but looking at a network waterfalls tells a better story.
An abstract representation of data analytics tracking peak performance.
The Magic of Astro
Astro made this possible by allowing me to still author components using modern tools (like .astro files, or even React/Svelte if needed) but compiling them completely away. By default, Astro strips all JS. You have to explicitly use client:load to hydrate a component. I simply never use it.
---
// This is an Example.astro component.
// Everything here runs at BUILD TIME only.
const response = await fetch('https://api.example.com/site-stats');
const stats = await response.json();
---
<div class="stats-card">
<h2>Site Overview</h2>
<p>Total Posts: {stats.posts.length}</p>
<p>Last Build: {new Date().toISOString()}</p>
</div>
The output contains no fetch polyfills, no state management, just pure, fast HTML.
Speed is a feature. Build faster sites.