← Back to blog

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:

  1. Parse Markdown files.
  2. Render Syntax Highlighting using Shiki (Astro does this beautifully) on the Node backend during build time.
  3. Render Math using KaTeX to static HTML.
  4. Output raw .html files.

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:

MetricOld (React SPA)New (Static)Improvement
First Contentful Paint1.8s0.2s88%
Largest Contentful Paint2.1s0.3s85%
Total Blocking Time150ms0ms100%
JavaScript Payload250KB0KB100%

Before and After: The Visual Proof

It’s one thing to show numbers, but looking at a network waterfalls tells a better story.

Lighthouse Score showing 100 on Performance, Accessibility, Best Practices, and SEO 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.