14 min read

Fresh vs. Next.js 15

I tried Fresh after another hydration bug. It shipped HTML, woke just the interactive islands, and my laptop sighed in relief. Next 15 felt heavier but battle-tested. My take: use Fresh for speed and clarity, Next when you need every plugin and playbook.
Fresh vs. Next.js 15
Photo by Maryam Sicard on Unsplash

SSR, DX, and real-world trade-offs


Your React build works, but the bundle feels heavy, and hydration bugs show up at the worst time. While hunting leaner options, I tried Fresh, a Deno-first framework that ships HTML by default and hydrates only the islands you declare. Next.js 15 likewise hydrates only components flagged 'use client', yet still includes a tiny RSC loader and React runtime. Here’s how the two compare on render flow, routing, and everyday DX.

TL;DR

  • Fresh sends only the JavaScript each interactive island needs.
  • Next.js offers richer plugins and a familiar React ecosystem.
  • Fresh feels simpler in dev: zero webpack, tiny node_modules.
  • Next.js scales safely on Vercel with battle-tested docs and community answers.

Read on for detailed SSR flow, routing patterns, and DX notes.

SSR and Hydration

Next.js’s approach

Next.js pioneered server-side rendering for React, but it evolved into a complex hybrid. In Next 15 (and recent versions), you can mix static generation and SSR, and with the App Router, React components can run on the server or client. By default, pages that use the new App Router hydrate only their client components; server components render to HTML and aren’t re-executed in the browser. This still adds a small React runtime (and the RSC loader) whenever the page contains client components; a page that’s 100% server components can ship just HTML. Consider a typical product page: Next will send a partially hydrated React app (only for its client components) to the browser, running React on the client for components that may never need interactivity. The upside is a dynamic React app, but the downside is heavier JavaScript payloads and potential hydration bugs.

Fresh’s approach

Fresh takes a different path. By default, every page is server-rendered HTML, and no JavaScript is sent to the browser unless needed. Fresh uses an “islands” architecture, inspired by frameworks like Astro. Essentially, Fresh delivers a mostly static HTML page and hydrates only interactive parts as islands of client-side Preact components. In our product page example, Fresh would ship static HTML for product descriptions and reviews, and activate interactive islands only for bits like the shopping cart or image carousel. This surgical hydration eliminates a huge chunk of unnecessary client JS that Next.js might send. The result is often faster load times and less CPU work on the client, since Fresh isn’t booting a full React app on every page load.

Another big distinction: Fresh doesn’t need a separate build step for SSR, yet Fresh 2.0 adds an optional deno task build that snapshots and compresses island code for production. There’s still no Webpack or Turbopack by default; Deno can JIT-compile on the fly, or you can pre-optimize via that one-command build. The Fresh documentation emphasizes this “zero build” design: when a request comes in, Fresh renders the page and sends HTML, and if an interactive island is present, only that island’s JS is sent to the browser. All code transformation (TSX to JS) is handled under the hood by Deno at runtime. This dramatically simplifies the SSR workflow.

What does this mean in practice? For one, performance under load can favor Fresh’s lean approach. Without needing to send or parse as much JS, Fresh can respond faster and handle more requests with the same resources. Next.js certainly isn’t slow, but its client-heavy nature means there’s more work happening on the browser side per request. It’s worth noting that Next.js 15 has taken steps to improve performance and reduce client-side bloat. The introduction of React Server Components in the App Router lets Next render more on the server and send less JS for purely static parts. Next 15 introduces Turbopack as an opt-in Rust dev bundler (next dev --turbopack), delivering much faster rebuilds and optimizing how code is delivered (e.g., up to ~46% faster initial compile in some cases). However, even with these improvements, a Next page with interactive components will still ship React's client runtime for those interactive components plus a small RSC loader script for hydration. Next’s App Router also sends a JSON “server-component payload” that describes the page tree for reconciliation, so the initial transfer is HTML plus that data blob. Fresh ships only HTML and the tiny island bundle(s), avoiding this duplicate payload. Fresh’s Preact-based islands remain lighter weight. Preact itself is tiny, and if your page has only one small interactive widget, that’s all the JavaScript the user gets. The philosophy differs: Next.js leans toward a rich client-side app by default (with an option to use server components for part of the tree), whereas Fresh defaults to an HTML-first, progressively enhanced page.

From the developer perspective, SSR in Fresh felt refreshingly straightforward. I didn’t have to configure webpack or worry about whether data was coming from a server component, route handler, or client-side fetch. Every Fresh page is inherently server-rendered on request, so you simply write your component and ensure any browser-only logic is isolated in an island. It’s a more old-school approach in some ways (closer to the multi-page apps of the past), yet combined with modern React-like components. Meanwhile, in Next, SSR can be magical but also puzzling: I’ve dealt with weird hydration mismatches and the mental overhead of “is this code running on server, client, or both?” Fresh’s model draws a clearer line: by default, code runs on the server, unless you explicitly opt into a client island. That clarity was actually quite liberating.

Of course, Fresh’s SSR model isn’t a silver bullet. If your page has many interactive islands, Fresh will load multiple small JS bundles instead of one big one, which could introduce its own overhead (lots of tiny requests). Next.js, by bundling, might send more JS upfront but fewer network requests. And not every app can be mostly static; some apps truly benefit from a rich client-side experience that React is designed for. The good news is, Fresh 2.0 is bringing further optimizations, the Deno team has reworked the core for better performance and even more flexibility. They’ve hinted that Fresh 2 is “much more extensible, faster, and easier to work with” than v1, with a simplified API that will feel familiar to web developers. In sum, on SSR and hydration, Fresh impresses with its simplicity and lean output, whereas Next.js offers a more full-blown React approach. Depending on your use case (minimalistic site vs. complex web app), you might favor one over the other.


Routing and Data Fetching

File-system routing

Both Next.js and Fresh use file-based routing conventions, but with slightly different flavors. In Next.js (since its inception), you create files in a pages/ directory (or now an app/ directory for the new router) and the framework automatically turns those into routes. Fresh does exactly the same with a routes/ folder: a file routes/about.tsx becomes the /about page, [id].tsx files denote dynamic routes, etc., just like Next. Coming from Next, I felt right at home setting up Fresh routes. One difference is that Fresh uses Preact components for rendering (JSX/TSX syntax), but Next uses React. In practice, writing a component in Fresh’s .tsx page felt identical to writing a React component in Next. The routing conventions (nested folders for nested routes, index files, etc.) were very comparable to Next’s Pages Router (and somewhat to the App Router structure as well). Fresh also supports nested layouts via _layout.tsx files in any subfolder, mirroring Next’s segment layouts.

Data fetching

In Next.js, data fetching traditionally involved special functions like getServerSideProps or getStaticProps for pages, or using API routes to supply data. In the newer Next App Router, that shifted: you now load data inside the server component, call route handlers (app/**/route.ts) for endpoint logic, or if you want a streamed pattern, use React's use()/Suspense helpers. Caching is controlled right on each fetch ({ cache: 'force-cache' }) or via a revalidate number. It’s powerful but takes time to understand: you choose per call whether to pre-render, revalidate, or keep it fully dynamic. Fresh simplifies this by leveraging the platform: every page is server-rendered on demand, so you can fetch directly in the component or via an optional handlers export (GET, POST, etc.) that passes typed props straight into the page. It feels natural. Akin to writing an Express route that renders a template, except here you’re in TypeScript JSX.

Note: Next.js streams HTML as it’s generated; Fresh still sends the response in one shot — streaming is slated for a post-2.0 release.

A particularly nice touch is Fresh’s built-in form handling. Next 15, though, now offers two native ways to do the same:

  1. Route Handler: Createapp/contact/route.ts, export a POST() function, and aim your form at /contact. The handler runs server-side and can return HTML, JSON, or a redirect.
  2. Server Action: In any component, mark a function with "use server" and pass it to action=. Next serializes FormData over the RSC channel and then re-renders as needed.

Fresh still leans harder into web standards: every page file can export a POST handler that calls await req.formData() and returns a Response (for example, a 303 redirect to /thanks). No extra abstractions, no legacy /api folder. Bottom line: both frameworks now respect default browser form posts out of the box. Fresh offers a single, always-on convention, while Next lets you choose between Route Handlers, Server Actions, or (if you’re still on the Pages Router) the older API-route pattern.

Next.js, of course, offers more advanced data-fetching patterns like Incremental Static Regeneration (rebuilding pages on the fly at intervals) and sophisticated caching strategies. As of writing, Fresh doesn’t have an out-of-the-box equivalent to ISR or a built-in caching mechanism for SSR responses. If you need caching, you’d implement it yourself or rely on a CDN in front. Fresh is more “real-time” by nature (render on every request), which is great for always-fresh data but less efficient if you want static caching. That said, because Fresh runs on Deno Deploy’s global edge, you can get very low latency and still add edge-cache headers manually. It’s just not an abstracted concept in the framework the way Next’s revalidate is.

API routes and backend logic

With Next.js, many projects still expose server-only logic via API Routes under/pages/api/*, but in the App Router (Next 13–15), the modern path is a Route Handler (app/**/route.ts) that sits beside your page and runs server-side. Fresh doesn’t have a separate “API routes” convention. Any page file’s handlers export can return JSON, HTML, or a redirect, so you don’t create a dedicated /api folder unless you want to. Alternatively, you can run Fresh alongside a Deno microservice if your backend grows complex. Deno’s standard library or third-party middleware (Oak, Hono, etc.) plug in easily because they share the same runtime.

Next.js is more batteries-included here: API Routes (legacy), Route Handlers (App Router), and edge middleware all ship in the box. Fresh stays lean but not rigid; it already supports folder-scoped middleware viaroutes/_middleware.ts, and Fresh 2 adds an Express-style plugin system (app.use() hooks) for fine-grained control. In short, Next bundles multiple endpoint patterns, while Fresh keeps one simple convention and lets plugins or Deno middleware handle the rest.

In summary, routing in Fresh felt intuitive and minimal, essentially just file = route, and use Deno’s Request/Response API for any custom needs. There’s no heavy lifting to get data into your page; you just fetch or query as needed when the request comes in. Next.js’s routing (especially with the two router modes currently in play) is more feature-rich but also more complex. I’ll admit, when working on large Next.js apps, I sometimes struggle to trace where data is coming from (is it a server component loading it? A client component calling an API route? A static prop from build time?). With Fresh, the data flow was easier to follow: the request hits the route’s handler, you prepare data, then render the component. That straightforwardness has its charm, especially for small to mid-sized applications.


DX

Here’s where I felt a real emotional shift: developer experience. After years in the Next.js ecosystem, I’ve grown used to a certain amount of build-tool overhead. Next has come a long way since manual webpack configs, yet a big project still drags along a huge node_modules folder, dozens of plugins, and the occasional cache meltdown. Working with Fresh was, well, felt opening the window. Fresh air. The scaffold ships with a tiny deno.json (plus an optional import_map.json), no package.json, no Babel, no bundler. Deno itself supplies the TypeScript compiler, linter, formatter, and runtime. I randeno task start, watched it fetch deps once, and from then on, every save hot-reloaded near instantly.

Turbopack vs Deno dev server

  1. Next 15: Turbopack is an opt-in Rust bundler (next dev --turbopack) that has become the recommended dev default and claims up to 96% faster code updates on Vercel-sized apps. Real-world results vary: some teams still see incremental refreshes taking several seconds, and memory footprints of 3–8 GB are common in large codebases.
  2. Fresh: Deno recompiles only the changed file and its few dependents. CPU and RAM stay low (often <500 MB). There’s no mega-bundle rebuild, so my machine stayed cool to the touch.

Subjectively, working in Fresh felt snappier and lighter; working in a large Next app still felt powerful but heavy.

Another DX aspect is debugging and error handling. Next.js has good error overlay and stack traces in development, but when things break deep in the build process (say, a weird webpack issue or a React hydration bug), the errors can be arcane. In Fresh, I noticed errors were usually just plain stack traces referencing my code or the Deno runtime, generally easier to decipher. Part of this is because Fresh is using the platform APIs; a failure often looks like a thrown error from a fetch or a database call, which I know how to handle. In Next, I’ve chased issues where the error was coming from inside Next’s internals or an obscure webpack loader, which is frustrating. That said, Next is battle-tested: you’re less likely to hit unknown bugs in the framework itself because so many others have tread that path. Fresh is newer, so I kept an eye out for rough edges. Meanwhile, Next’s maturity means most DX issues you’ll find on StackOverflow or GitHub already with solutions.

TypeScript support is stellar in both, but Fresh takes the crown for convenience. Deno is TypeScript-first, so everything in Fresh is naturally typed, no config needed. In Next, TypeScript works but requires a tsconfig.json and one-time setup (which create-next-app usually handles for you). I appreciate that Next has tightened its TS integration over the years, yet I still occasionally run into type issues where I need to tweak the config or figure out the types of Next’s App Router props (e.g., params/searchParams passed to a Page or Route Handler). In Fresh, the types (for things like the Handlers or the PageProps to a page) were simple and well-documented. I definitely felt a bit more “at ease” writing TS in Fresh, not worrying about build versus type pipelines.

Perhaps the biggest DX differentiator is the simplicity. Fresh’s motto could be “less tooling, more coding.” No more fiddling with webpack plugins, Babel configs, or obscure Next.js settings to get things working. This was echoed by other developers too. Early Fresh adopters often gush about the developer happiness angle: not fighting build tools or massive dev dependencies. One author aptly said Fresh’s most compelling advantage isn’t raw performance, it’s that it eliminates the need to babysit the toolchain. After spending some time with Fresh, I tend to agree. My brain was freed up to think about features rather than bundlers or performance tweaks. It reminded me of working with simpler stacks in the past, but with modern React-like ergonomics. In Next.js, I sometimes feel the weight of the “Next ecosystem”, it’s powerful but you have to know its conventions, its gotchas (like “oh, you can’t use window in a server component” or “this library doesn’t support React 19 features yet causing a build issue”). That’s not to say Fresh is devoid of learning (I had to learn how islands work, for instance), but it felt closer to baseline web development knowledge.

To be fair, Next.js still offers a smoother on-ramp for most React devs. If you know React, you basically know 90% of Next.js, and Vercel’s polish in the DX (like their CLI, the dev errors and hints, etc.) is top-notch. Fresh being new means the docs are smaller (though quite clear), and you won’t find as many blog posts or StackOverflow answers for tricky issues. One has to rely more on official docs and the questions from the developers, whereas with Next, usually a quick search lands on an existing solution.


Ecosystem and Deployment

Any framework is only as good as its ecosystem. Next.js has an eight-year head start: auth (NextAuth), CMS bridges, UI kits (Material UI, Chakra, shadcn/ui), image/SEO helpers — the works. If it runs in React, it almost certainly runs in Next. Fresh is catching up fast. Fresh 1.x already let you import "npm:lodash"; Fresh 2 and Deno 2 finish the job with built-in Node polyfills and a smarter npm cache, so most pure-JS packages Just Work. Heavy React-specific libs are hit-or-miss because Fresh ships Preact by default. Preact’s compat layer covers many hooks and components; still, anything deep in React internals (some devtools, complex drag-drop kits) may need tweaking, and most libraries still require import and jsx aliases that map react and react-dom to preact/compat. On the upside, Fresh brings Preact Signals for ultra-cheap reactivity (something React still lacks) so tiny islands can update without re-rendering the world.

Deployment

  • Next.js 15: The sweet spot is Vercel. Push to GitHub, get global edge + serverless by default. You can self-host on Node (ECS, Docker, Heroku, etc.), but you lose the zero-config niceties.
  • Fresh: Tuned for Deno Deploy (edge isolates). You can container-run deno task start anywhere, but you’ll need Deno in the image. Traditional Node-only hosts won’t run it without that layer.

Lock-in worries cut both ways: Next’s fanciest features shine on Vercel; Fresh’s nicest DX shines on Deno Deploy. Both are OSS. You can eject any time, but optimal hosting gravitates toward the parent company’s cloud.

Edge vs serverless

Next has pushed into edge territory: Route Handlers and Middleware can deploy to Vercel Edge or Cloudflare Workers with the Web-standard Request/Response API. But pages that depend on Node APIs (fs, crypto, large image transforms) still fall back to Node serverless. Fresh was edge-first from day one, so every handler, page, or island runs fine in a Worker context without extra flags.

Community & stability

Next’s user base is an order of magnitude larger; answers pop up on Stack Overflow instantly, and majors have codemods for breaking changes. Fresh’s Discord is smaller but active. Fresh 2.0 will introduce a few breaking simplifications, such as a new app.use() plugin/middleware system that replaces today’s separate hooks, so early adopters should expect minor refactors. If you need “nothing moves” stability, Next still feels safer; if you want to ride next-gen ideas early, Fresh’s churn is the entry fee.


A Fresh Perspective

Writing this as a long-time Next.js developer, I was surprised by how much plain fun came from working with a simpler stack. Fresh’s no-build, server-first model let me focus on features instead of wrestling configs. Next.js, meanwhile, still covers every edge case under the sun and scales without flinching. It’s a bit like pitting a feather-light roadster against a trusty SUV: each wins in different conditions.

So, should a React/Next dev give Fresh a spin? Yes. Even if you never migrate, ideas such as islands and on-demand rendering will sharpen how you think about performance. Next itself is moving (edge rendering, streaming HTML, smaller bundles), pushed along by challengers like Fresh. For hobby apps or anything where lean speed matters, Fresh feels refreshing (pun very much intended). When you need every plugin, every template, and ten years of community know-how, Next 15 remains a safe call.

Personally, I’m glad to keep both tools on deck. Fresh made me rethink habits I’d accepted as “the React way.” It proves that even the basics (how we ship JavaScript, where we run it) can be reimagined. I’ll keep tinkering with Fresh 2.0 and watching how Next.js answers back.

Reflection

Will Fresh’s developer-first simplicity push it into mainstream use, or stay a favorite among performance nerds? And what matters more in your day-to-day work, stable familiarity or the buzz of trying something new? Either way, sampling both stacks widened my perspective. Give the newer framework a weekend; you might return to your main project with sharper instincts and a few lighter patterns in mind.


Enjoyed this piece?

If this piece was helpful or resonated with you, you can support my work by buying me a Coffee!

Click the image to visit Alvis’s Buy Me a Coffee page.
Subscribe to our newsletter.

Become a subscriber receive the latest updates in your inbox.