MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr πŸ”₯ tagline

Hey there πŸ‘‹ I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 β€” present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 β€” Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms β€” earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

Deno Fresh: Zero-Run-Time JavaScript Framework

Build with Fresh: island architecture, edge deployment, and Preact integration.

DenoFreshIslandsFrontend

By MinhVo

Introduction

Most modern web frameworks ship 100KB+ of JavaScript to the browser before rendering a single pixel. React sends its virtual DOM, Vue sends its reactivity system, Svelte sends its compiler output. Fresh takes a radically different approach: it ships zero JavaScript by default and only hydrates interactive componentsβ€”called islandsβ€”when explicitly marked.

This zero-runtime philosophy makes Fresh one of the fastest web frameworks in existence. A Fresh page loads as pure HTML, renders instantly, and achieves perfect Core Web Vitals scores without any optimization. When you need interactivityβ€”a search bar, a counter, a real-time widgetβ€”you create an island that hydrates independently, leaving the rest of the page as static server-rendered HTML.

Built on Deno and Preact, Fresh combines the simplicity of server-rendered pages with the component model developers love. It's the framework for developers who believe the web should be fast by default.

Zero JavaScript architecture

Understanding Fresh: Core Concepts

How Zero Runtime Works

When Fresh renders a page on the server, it converts Preact components to HTML strings using preact-render-to-string. This HTML is sent to the browser with no accompanying JavaScriptβ€”unless the page contains islands.

The browser receives:

<!-- Pure HTML - no JavaScript -->
<!DOCTYPE html>
<html>
  <head>
    <title>My Page</title>
    <!-- Fresh automatically adds critical CSS -->
    <style>
      .header { font-size: 2rem; font-weight: bold; }
      .content { max-width: 800px; margin: 0 auto; }
    </style>
  </head>
  <body>
    <div>
      <h1 class="header">Welcome</h1>
      <p class="content">This page has zero JavaScript.</p>
    </div>
  </body>
</html>

Compare this to a typical React SPA:

<!-- React SPA - 85KB+ of JavaScript before any content -->
<!DOCTYPE html>
<html>
  <body>
    <div id="root"></div>
    <script src="/react.production.min.js"></script>
    <script src="/react-dom.production.min.js"></script>
    <script src="/app.bundle.js"></script>
    <!-- Content appears only after JS loads and executes -->
  </body>
</html>

The Island Model

Islands are independent Preact components that hydrate on the client. Each island:

  • Has its own JavaScript bundle (only includes what it needs)
  • Hydrates independently (doesn't block other islands)
  • Can communicate with the server through fetch/WebSocket
  • Can share state with other islands via signals
Page Layout (server-rendered HTML, 0 JS)
β”œβ”€β”€ Header (static HTML)
β”œβ”€β”€ Navigation (static HTML)
β”œβ”€β”€ Content Area
β”‚   β”œβ”€β”€ Blog Post (static HTML)
β”‚   β”œβ”€β”€ Comment Form (ISLAND - ships JS)
β”‚   └── Like Button (ISLAND - ships JS)
β”œβ”€β”€ Sidebar
β”‚   β”œβ”€β”€ Recent Posts (static HTML)
β”‚   └── Search (ISLAND - ships JS)
└── Footer (static HTML)

The total JavaScript shipped is only the sum of the island bundlesβ€”typically 5-20KB instead of 100KB+.

Preact as the Foundation

Fresh uses Preact, which provides:

  • 3KB gzipped (vs. React's 42KB)
  • Same JSX syntax and hooks API as React
  • Signals for fine-grained reactivity without virtual DOM diffing
  • Full TypeScript support with excellent type inference
// This Preact component looks identical to React
import { useState, useEffect } from "preact/hooks";
 
function Timer() {
  const [seconds, setSeconds] = useState(0);
 
  useEffect(() => {
    const interval = setInterval(() => setSeconds((s) => s + 1), 1000);
    return () => clearInterval(interval);
  }, []);
 
  return <p>Elapsed: {seconds}s</p>;
}

Preact component model

Architecture and Design Patterns

Project Structure

A Fresh project follows a strict directory structure:

my-fresh-app/
β”œβ”€β”€ deno.json              # Deno configuration
β”œβ”€β”€ dev.ts                 # Development server entry
β”œβ”€β”€ main.ts                # Production server entry
β”œβ”€β”€ fresh.gen.ts           # Auto-generated route manifest
β”œβ”€β”€ import_map.json        # Import aliases
β”œβ”€β”€ components/            # Server-rendered components (no JS)
β”‚   β”œβ”€β”€ Header.tsx
β”‚   β”œβ”€β”€ Footer.tsx
β”‚   └── Layout.tsx
β”œβ”€β”€ islands/               # Interactive components (ship JS)
β”‚   β”œβ”€β”€ Counter.tsx
β”‚   β”œβ”€β”€ SearchBar.tsx
β”‚   └── ThemeToggle.tsx
β”œβ”€β”€ routes/                # File-based routing
β”‚   β”œβ”€β”€ index.tsx
β”‚   β”œβ”€β”€ about.tsx
β”‚   β”œβ”€β”€ blog/
β”‚   β”‚   β”œβ”€β”€ index.tsx
β”‚   β”‚   └── [slug].tsx
β”‚   └── api/
β”‚       └── hello.ts
β”œβ”€β”€ static/                # Static assets
β”‚   β”œβ”€β”€ styles.css
β”‚   └── images/
└── utils/                 # Shared utilities
    └── posts.ts

Route Handlers and Data Loading

Every route file can export a handler for server-side data loading:

// routes/blog/[slug].tsx
import { Handlers, PageProps } from "$fresh/server.ts";
import { Head } from "$fresh/runtime.ts";
import { loadPost } from "../../utils/posts.ts";
import LikeButton from "../../islands/LikeButton.tsx";
 
interface Post {
  title: string;
  content: string;
  date: string;
  slug: string;
  likes: number;
}
 
export const handler: Handlers<Post> = {
  async GET(req, ctx) {
    const post = await loadPost(ctx.params.slug);
    if (!post) {
      return new Response("Not Found", { status: 404 });
    }
    return ctx.render(post);
  },
};
 
export default function BlogPost({ data }: PageProps<Post>) {
  return (
    <>
      <Head>
        <title>{data.title} | My Blog</title>
        <meta property="og:title" content={data.title} />
        <meta property="og:type" content="article" />
      </Head>
 
      <article class="max-w-3xl mx-auto p-6">
        <header class="mb-8">
          <h1 class="text-4xl font-bold">{data.title}</h1>
          <time class="text-gray-500">{data.date}</time>
        </header>
 
        {/* Static content - no JS */}
        <div
          class="prose"
          dangerouslySetInnerHTML={{ __html: data.content }}
        />
 
        {/* Island - ships ~2KB of JS */}
        <footer class="mt-8 pt-4 border-t">
          <LikeButton slug={data.slug} initialLikes={data.likes} />
        </footer>
      </article>
    </>
  );
}

Middleware Stack

Fresh supports middleware for cross-cutting concerns:

// routes/_middleware.ts
import { MiddlewareHandlerContext } from "$fresh/server.ts";
 
// Timing middleware
export async function handler(req: Request, ctx: MiddlewareHandlerContext) {
  const start = performance.now();
  const response = await ctx.next();
  const duration = performance.now() - start;
 
  response.headers.set("Server-Timing", `total;dur=${duration.toFixed(2)}`);
  return response;
}
 
// routes/api/_middleware.ts
import { MiddlewareHandlerContext } from "$fresh/server.ts";
 
// CORS middleware for API routes
export async function handler(req: Request, ctx: MiddlewareHandlerContext) {
  if (req.method === "OPTIONS") {
    return new Response(null, {
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE",
        "Access-Control-Allow-Headers": "Content-Type, Authorization",
      },
    });
  }
 
  const response = await ctx.next();
  response.headers.set("Access-Control-Allow-Origin", "*");
  return response;
}

Step-by-Step Implementation

Creating Your First Fresh App

# Generate a new Fresh project
deno run -A https://fresh.deno.dev my-app
 
# Start development server
cd my-app
deno task dev
 
# Open http://localhost:8000

Building a Landing Page

// routes/index.tsx
import { Head } from "$fresh/runtime.ts";
import Counter from "../islands/Counter.tsx";
 
export default function Home() {
  return (
    <>
      <Head>
        <title>Fresh Landing Page</title>
        <meta name="description" content="A blazing fast landing page" />
      </Head>
 
      {/* Hero section - pure HTML */}
      <section class="bg-gradient-to-r from-blue-600 to-purple-600 text-white py-20">
        <div class="max-w-4xl mx-auto text-center px-6">
          <h1 class="text-5xl font-bold mb-6">
            Build Fast Websites
          </h1>
          <p class="text-xl mb-8">
            Fresh ships zero JavaScript by default. Your pages load instantly.
          </p>
          <a href="/docs" class="bg-white text-blue-600 px-8 py-3 rounded-lg font-semibold">
            Get Started
          </a>
        </div>
      </section>
 
      {/* Features section - pure HTML */}
      <section class="py-20">
        <div class="max-w-4xl mx-auto px-6 grid md:grid-cols-3 gap-8">
          <div class="text-center">
            <h3 class="text-xl font-bold mb-2">Zero JS</h3>
            <p>Pages ship no JavaScript by default. Only islands hydrate.</p>
          </div>
          <div class="text-center">
            <h3 class="text-xl font-bold mb-2">Edge Ready</h3>
            <p>Deploy to Deno Deploy for global edge performance.</p>
          </div>
          <div class="text-center">
            <h3 class="text-xl font-bold mb-2">TypeScript First</h3>
            <p>Native TypeScript support with no build step.</p>
          </div>
        </div>
      </section>
 
      {/* Interactive section - island */}
      <section class="py-20 bg-gray-100">
        <div class="max-w-md mx-auto text-center">
          <h2 class="text-3xl font-bold mb-6">Try It Live</h2>
          <Counter initial={0} />
        </div>
      </section>
    </>
  );
}

The Counter Island

// islands/Counter.tsx
import { useState } from "preact/hooks";
 
interface CounterProps {
  initial: number;
}
 
export default function Counter({ initial }: CounterProps) {
  const [count, setCount] = useState(initial);
 
  return (
    <div class="flex items-center justify-center gap-4">
      <button
        class="w-12 h-12 bg-red-500 text-white rounded-full text-xl"
        onClick={() => setCount(count - 1)}
      >
        -
      </button>
      <span class="text-4xl font-bold w-20 text-center">{count}</span>
      <button
        class="w-12 h-12 bg-green-500 text-white rounded-full text-xl"
        onClick={() => setCount(count + 1)}
      >
        +
      </button>
    </div>
  );
}

Form Handling with Islands

// islands/ContactForm.tsx
import { useState } from "preact/hooks";
 
interface FormState {
  name: string;
  email: string;
  message: string;
}
 
export default function ContactForm() {
  const [form, setForm] = useState<FormState>({ name: "", email: "", message: "" });
  const [status, setStatus] = useState<"idle" | "sending" | "success" | "error">("idle");
 
  async function handleSubmit(e: Event) {
    e.preventDefault();
    setStatus("sending");
 
    try {
      const res = await fetch("/api/contact", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(form),
      });
 
      if (res.ok) {
        setStatus("success");
        setForm({ name: "", email: "", message: "" });
      } else {
        setStatus("error");
      }
    } catch {
      setStatus("error");
    }
  }
 
  return (
    <form onSubmit={handleSubmit} class="space-y-4 max-w-lg mx-auto">
      <input
        type="text"
        placeholder="Your name"
        value={form.name}
        onInput={(e) => setForm({ ...form, name: (e.target as HTMLInputElement).value })}
        class="w-full p-3 border rounded"
        required
      />
      <input
        type="email"
        placeholder="Your email"
        value={form.email}
        onInput={(e) => setForm({ ...form, email: (e.target as HTMLInputElement).value })}
        class="w-full p-3 border rounded"
        required
      />
      <textarea
        placeholder="Your message"
        value={form.message}
        onInput={(e) => setForm({ ...form, message: (e.target as HTMLTextAreaElement).value })}
        class="w-full p-3 border rounded h-32"
        required
      />
      <button
        type="submit"
        disabled={status === "sending"}
        class="w-full p-3 bg-blue-600 text-white rounded disabled:opacity-50"
      >
        {status === "sending" ? "Sending..." : "Send Message"}
      </button>
      {status === "success" && <p class="text-green-600">Message sent!</p>}
      {status === "error" && <p class="text-red-600">Failed to send. Try again.</p>}
    </form>
  );
}

Edge deployment architecture

Real-World Use Cases

Use Case 1: Documentation Site

// routes/docs/[...slug].tsx
import { Handlers, PageProps } from "$fresh/server.ts";
import TableOfContents from "../../islands/TableOfContents.tsx";
 
interface DocPage {
  title: string;
  content: string;
  headings: Array<{ id: string; text: string; level: number }>;
}
 
export const handler: Handlers<DocPage> = {
  async GET(req, ctx) {
    const slug = ctx.params.slug || "index";
    const doc = await loadDoc(slug);
    if (!doc) return new Response("Not Found", { status: 404 });
    return ctx.render(doc);
  },
};
 
export default function DocPage({ data }: PageProps<DocPage>) {
  return (
    <div class="flex max-w-6xl mx-auto">
      {/* Sidebar - static HTML */}
      <nav class="w-64 shrink-0 p-6 border-r">
        <a href="/docs/introduction" class="block py-2">Introduction</a>
        <a href="/docs/getting-started" class="block py-2">Getting Started</a>
        <a href="/docs/components" class="block py-2">Components</a>
      </nav>
 
      {/* Main content */}
      <main class="flex-1 p-6">
        <h1>{data.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: data.content }} />
      </main>
 
      {/* Table of contents - island */}
      <aside class="w-48 shrink-0 p-6">
        <TableOfContents headings={data.headings} />
      </aside>
    </div>
  );
}

Use Case 2: E-Commerce with Cart

// islands/ProductCard.tsx
import { useSignal } from "@preact/signals";
 
export default function ProductCard({ product }: { product: Product }) {
  const quantity = useSignal(1);
  const adding = useSignal(false);
 
  async function addToCart() {
    adding.value = true;
    await fetch("/api/cart", {
      method: "POST",
      body: JSON.stringify({ productId: product.id, quantity: quantity.value }),
    });
    adding.value = false;
    // Trigger cart count update
    window.dispatchEvent(new CustomEvent("cart-updated"));
  }
 
  return (
    <div class="border rounded-lg overflow-hidden">
      <img src={product.image} alt={product.name} />
      <div class="p-4">
        <h3>{product.name}</h3>
        <p class="text-xl font-bold">${product.price}</p>
        <div class="flex items-center gap-2 mt-2">
          <button onClick={() => quantity.value = Math.max(1, quantity.value - 1)}>-</button>
          <span>{quantity.value}</span>
          <button onClick={() => quantity.value++}>+</button>
        </div>
        <button
          onClick={addToCart}
          disabled={adding.value}
          class="mt-4 w-full bg-blue-600 text-white py-2 rounded"
        >
          {adding.value ? "Adding..." : "Add to Cart"}
        </button>
      </div>
    </div>
  );
}

Use Case 3: Real-Time Dashboard

// islands/Dashboard.tsx
import { useEffect, useState } from "preact/hooks";
 
interface Metrics {
  visitors: number;
  pageViews: number;
  bounceRate: number;
  avgSession: number;
}
 
export default function Dashboard() {
  const [metrics, setMetrics] = useState<Metrics | null>(null);
 
  useEffect(() => {
    const ws = new WebSocket(`wss://${location.host}/api/ws/metrics`);
 
    ws.onmessage = (e) => {
      setMetrics(JSON.parse(e.data));
    };
 
    return () => ws.close();
  }, []);
 
  if (!metrics) return <div class="animate-pulse">Loading...</div>;
 
  return (
    <div class="grid grid-cols-4 gap-4">
      <MetricCard title="Visitors" value={metrics.visitors} />
      <MetricCard title="Page Views" value={metrics.pageViews} />
      <MetricCard title="Bounce Rate" value={`${metrics.bounceRate}%`} />
      <MetricCard title="Avg Session" value={`${metrics.avgSession}s`} />
    </div>
  );
}
 
function MetricCard({ title, value }: { title: string; value: number | string }) {
  return (
    <div class="bg-white p-6 rounded-lg shadow">
      <p class="text-gray-500 text-sm">{title}</p>
      <p class="text-3xl font-bold">{value}</p>
    </div>
  );
}

Best Practices for Production

  1. Audit island count: Run ls islands/ regularly. Each island adds JavaScript to your page. If you have more than 5-7 islands on a single page, consider restructuring.

  2. Use signals for shared state: Preact signals provide fine-grained reactivity without re-rendering entire component trees. Use useSignal() instead of useState() for frequently changing values.

  3. Lazy-load heavy islands: Use preact/compat's lazy() to defer loading of islands that aren't immediately visible (below the fold).

  4. Implement proper error boundaries: Wrap islands in error boundaries so a client-side crash doesn't break the entire page.

  5. Use Head component for SEO: Every page should have a unique <title> and <meta description>. Fresh renders these server-side, so search engines see them without JavaScript.

  6. Deploy to Deno Deploy: Fresh is optimized for Deno Deploy's edge runtime. The V8 isolate model provides sub-5ms cold starts.

  7. Use Tailwind CSS: Fresh has built-in Tailwind support. Configure it in deno.json and use utility classes for all styling.

  8. Test with JavaScript disabled: Open your site with JavaScript disabled in the browser. Everything should still work except island components.

Common Pitfalls and Solutions

PitfallImpactSolution
Using browser APIs in componentsSSR crashesGuard with IS_BROWSER from $fresh/runtime.ts
Forgetting islands directoryComponent renders but doesn't hydrateMove interactive components to islands/
Large island bundlesDefeats zero-JS purposeKeep islands small; use dynamic imports
Not handling SSR dataHydration mismatchEnsure server and client render the same initial state
Using dangerouslySetInnerHTML with user inputXSS vulnerabilitySanitize HTML with DOMPurify before rendering
Missing Head componentPoor SEOAlways include <Head> with title and meta tags

Performance Optimization

// Enable Tailwind CSS with Fresh
// deno.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "preact"
  },
  "imports": {
    "$fresh/": "https://deno.land/x/fresh@1.6.0/",
    "preact": "https://esm.sh/preact@10.19.3",
    "preact/": "https://esm.sh/preact@10.19.3/",
    "preact-render-to-string": "https://esm.sh/preact-render-to-string@6.3.1",
    "@preact/signals": "https://esm.sh/@preact/signals@1.2.2"
  }
}
// Optimize static asset caching
// main.ts
import { start } from "./server.ts";
 
start({
  port: 8000,
  // Cache static assets aggressively
  assetCacheControl: {
    "text/css": "public, max-age=31536000, immutable",
    "application/javascript": "public, max-age=31536000, immutable",
    "image/*": "public, max-age=86400",
  },
});

Comparison with Alternatives

FeatureFreshNext.jsAstroSvelteKit
Default JS0 bytes~85KB0 bytes~20KB
HydrationIslandsRSC + HydrationIslandsFull
Bundle Size (island)~3KB~45KB~3KB~15KB
SSRDeno DeployVercel/NodeAnyNode/Edge
TypeScriptNativeConfigConfigConfig
CSSTailwind built-inAnyAnyScoped
Learning CurveLowMediumLowMedium

Advanced Patterns

Dynamic Island Loading

// routes/index.tsx
import { lazy, Suspense } from "preact/compat";
 
// Only load the heavy chart island when needed
const Chart = lazy(() => import("../islands/Chart.tsx"));
 
export default function Home() {
  return (
    <div>
      <h1>Dashboard</h1>
 
      {/* Static content renders immediately */}
      <p>Overview of your metrics</p>
 
      {/* Chart loads asynchronously */}
      <Suspense fallback={<div class="h-64 bg-gray-100 animate-pulse" />}>
        <Chart />
      </Suspense>
    </div>
  );
}

Custom Error Pages

// routes/_500.tsx
import { ErrorPageProps } from "$fresh/server.ts";
 
export default function Error500({ error }: ErrorPageProps) {
  return (
    <main class="min-h-screen flex items-center justify-center">
      <div class="text-center">
        <h1 class="text-6xl font-bold text-red-600">500</h1>
        <p class="text-xl mt-4">Something went wrong</p>
        <p class="text-gray-500 mt-2">{error?.message}</p>
        <a href="/" class="mt-6 inline-block text-blue-600 underline">
          Go home
        </a>
      </div>
    </main>
  );
}

Testing Strategies

// tests/handler.test.ts
import { assertEquals } from "https://deno.land/std/assert/mod.ts";
 
Deno.test("Home page returns 200", async () => {
  const resp = await fetch("http://localhost:8000/");
  assertEquals(resp.status, 200);
 
  const html = await resp.text();
  assertEquals(html.includes("Welcome"), true);
});
 
Deno.test("Blog post returns 404 for missing slug", async () => {
  const resp = await fetch("http://localhost:8000/blog/nonexistent");
  assertEquals(resp.status, 404);
});
 
Deno.test("API returns JSON", async () => {
  const resp = await fetch("http://localhost:8000/api/hello");
  assertEquals(resp.status, 200);
  assertEquals(resp.headers.get("content-type"), "application/json");
 
  const data = await resp.json();
  assertEquals(data.message, "Hello from Fresh!");
});
 
// Run with: deno test --allow-net

Future Outlook

Fresh is evolving toward:

  • Partial hydration: Automatic island detection based on interactivity analysis
  • Server components: React Server Components-like patterns for data-heavy pages
  • Streaming SSR: Progressive HTML delivery for faster perceived performance
  • Built-in image optimization: Automatic responsive images with modern formats
  • Enhanced routing: Parallel routes and route groups for complex applications

The framework is becoming the standard choice for Deno-based web development, with growing adoption for documentation sites, blogs, and content-driven applications.

Conclusion

Fresh proves that web frameworks don't need to ship megabytes of JavaScript to provide a great developer experience. By defaulting to zero runtime JavaScript and using islands for targeted interactivity, Fresh achieves performance levels that traditional SPA frameworks can't match.

Key takeaways:

  1. Zero JS by default means perfect Core Web Vitals without optimization
  2. Islands architecture provides interactivity only where needed
  3. Preact delivers React-like DX at 3KB
  4. Deno native means TypeScript, testing, and deployment all in one toolchain
  5. Edge-first deployment to Deno Deploy for global performance

For content-driven websites where performance matters, Fresh is the framework to beat.