"Zero JavaScript by default" has been Fresh's pitch since day one. The honest fine print: even a page with no islands and no partials still shipped a small client entry, somewhere between 14 and 22 KB gzipped depending on the project. Not a scandal, but not zero either. Fresh 2.3, announced on the Deno blog, closes that gap for real.
Actually zero this time
There is nothing to configure. After upgrading, Fresh checks at render time whether a page actually uses islands or partials. If it uses neither, the response contains no script tags, no module preload headers, and no client bundle. A marketing page, a blog post, a docs page, plain HTML over the wire, the way it renders it.
If you want to see it yourself, upgrade and open the network tab on a static route. The difference is easy to measure: where 2.2 loaded the client entry on every page, 2.3 loads nothing until the first island shows up.
View Transitions with one attribute
Fresh already had client-side navigation through its partials system (f-client-nav). Version 2.3 wires the browser's View Transitions API into it. Opt in by adding one attribute next to the one you already have:
<body f-client-nav f-view-transition>
Partial navigations are now wrapped in document.startViewTransition(), and you style the animation in plain CSS:
::view-transition-old(root) {
animation: fade-out 150ms ease-out;
}
::view-transition-new(root) {
animation: fade-in 150ms ease-in;
}
Chrome 111+, Edge 111+, and Safari 18+ run the animations natively. Firefox doesn't yet, it simply falls back to a normal partial update, so there is no penalty for opting in early.
WebSockets without a sidecar
Until now, real-time features in a Fresh app meant either wiring Deno.upgradeWebSocket by hand or running a separate server. Fresh 2.3 makes WebSockets first-class. The quickest version lives on the App instance:
app.ws("/ws", {
open(ctx) {
console.log("client connected");
},
message(ctx, event) {
ctx.send(`echo: ${event.data}`);
},
close(ctx) {
console.log("client gone");
},
});
File-based routes get the same capability through ctx.upgrade() inside a GET handler. And if you need to manage connections yourself, a chat room keeping a Set of sockets, say, calling ctx.upgrade() with no handler object returns the raw WebSocket so you own the lifecycle.
The rest of the release
Three smaller additions are worth knowing about:
- CSP nonces. Fresh can now inject a per-request nonce into the scripts and styles it emits, which makes a strict
Content-Security-Policypractical without hand-maintaining hashes. - Temporal in islands. All eight Temporal types can be passed as island props and serialize correctly across the server/client boundary, useful now that Temporal is stabilizing across runtimes.
- Prerendering. Mark a route with
prerender: trueand Fresh renders it to static HTML at build time; dynamic routes can enumerate their paths. Combined with zero-JS pages, Fresh quietly becomes a capable static site generator.
Fresh 2.3 is a deno update away, and with Deno 2.7+ new projects scaffold via deno create. For a framework whose whole identity is restraint, this is the release where the restraint stops being approximate.



