Deno Fresh 2.3: Zero JavaScript by Default, View Transitions, Temporal Support

BafS1 pts0 comments

Fresh 2.3: Zero JS by default, View Transitions, and Temporal support | DenoSkip to main contentIntroducing Claw Patrol: an open-source security firewall for agents<br>Read the post-><br>Dismiss

Fresh 2.3: Zero JS by default, View Transitions, and Temporal support<br>April 24, 2026<br>Bartek Iwańczuk<br>Product Update<br>Fresh

Fresh 2.3 is out, with over 100 commits from 20 contributors. This release makes<br>the "zero JavaScript by default" promise actually hold, adds View Transitions<br>support, pre-compiles middleware chains, and rounds out a long list of Vite<br>integration fixes.

You can start a new project with:

deno create @fresh/init<br>or update an existing one with:

deno run -Ar jsr:@fresh/update<br>Zero JavaScript by default

Fresh has always said that pages ship no JavaScript unless they need to, but<br>that wasn’t strictly true. Every page ended up with a small client-entry<br>script to bootstrap the island reviver and partials engine, even when neither<br>was used.

Thanks to Jeroen Akkerman in<br>#3696, Fresh now checks whether<br>the page actually uses islands or partials (f-client-nav) before injecting<br>anything. If it doesn’t, the page ships with no tag, no module<br>preload headers, and no client-side bundle at all.

Fresh 2.2<br>Fresh 2.3

JavaScript (raw)<br>~14–22 KB<br>0 KB

JavaScript (gzip)<br>~5–9 KB<br>0 KB

Module preload headers<br>1+

Inline boot

There is nothing to configure. Static pages will just stop shipping JavaScript<br>after upgrading.

View Transitions

The<br>View Transitions API<br>lets browsers animate between DOM states natively. Fresh 2.3 wires it up to the<br>existing partials system, so you can opt in by adding one attribute:

body f-client-nav f-view-transition>

body><br>Partial navigations will then be wrapped in document.startViewTransition() and<br>you can customize the animation with regular CSS:

::view-transition-old(root) {<br>animation: fade-out 0.2s ease-in;<br>::view-transition-new(root) {<br>animation: fade-in 0.2s ease-out;<br>Or target individual elements:

.sidebar {<br>view-transition-name: sidebar;

View Transitions between two pages in a Fresh app.

Browsers without support fall back to normal partial updates. See the<br>View Transitions docs<br>for more.

First-class WebSocket support

Fresh now has built-in WebSocket support<br>(#3774). The quickest way to add<br>a WebSocket endpoint is app.ws():

main.ts<br>const app = new App()<br>.ws("/ws", {<br>open(socket) {<br>console.log("Client connected");<br>},<br>message(socket, event) {<br>socket.send(`Echo: ${event.data}`);<br>},<br>close(socket) {<br>console.log("Client disconnected");<br>},<br>});<br>For file-based routes, use ctx.upgrade() inside a GET handler. In managed<br>mode, pass handlers and get the response back directly:

routes/api/ws.ts<br>export const handlers = define.handlers({<br>GET(ctx) {<br>return ctx.upgrade({<br>message(socket, event) {<br>socket.send(`Echo: ${event.data}`);<br>},<br>});<br>},<br>});<br>There’s also a bare mode. Call ctx.upgrade() without arguments to get the raw<br>WebSocket object, useful when you need to store sockets in a shared structure<br>like a chat room:

routes/api/chat.ts<br>const clients = new SetWebSocket>();

export const handlers = define.handlers({<br>GET(ctx) {<br>const { socket, response } = ctx.upgrade();

socket.onopen = () => clients.add(socket);<br>socket.onmessage = (event) => {<br>for (const client of clients) {<br>if (client.readyState === WebSocket.OPEN) {<br>client.send(event.data);<br>};<br>socket.onclose = () => clients.delete(socket);

return response;<br>},<br>});<br>Non-WebSocket requests to a WebSocket route automatically get a 400 response.<br>See the<br>WebSocket documentation for<br>the full API including idleTimeout and protocol options.

Vite integration improvements

A lot of this cycle went into making the Vite integration more robust,<br>especially around npm package compatibility.

CJS-to-ESM transforms and process.env replacements are now handled by Vite<br>directly, so we could drop two Babel passes from the build.

CJS handling in SSR dev mode has been improved, React compat aliasing works<br>properly now, and resolve.alias is applied before Deno resolution. Packages<br>like Radix UI work out of the box.

optimizeDeps.exclude is set up so Vite no longer creates a duplicate Preact<br>instance during pre-bundling.

Vite asset URLs now include a cache-bust query param so immutable caching does<br>the right thing (#3761).

Temp files are ignored by the Vite watcher, so editor swap files no longer<br>crash the dev server (#3763).

Work in this area is continuing.<br>#3767 removes the rest of the<br>Babel transforms (~2,050 lines) and lets Vite handle CJS packages natively end<br>to end. That should land shortly after 2.3.

CSP nonces and IP filtering

Two new security middleware ship with this release.

CSP nonce injection<br>(#3709) generates a unique<br>nonce per request and adds it to every inline and tag.<br>The corresponding Content-Security-Policy header uses 'nonce-{value}'<br>instead of 'unsafe-inline', so only scripts and styles that Fresh rendered are<br>allowed to execute.

main.ts<br>import { csp } from "fresh";

app.use(csp({ useNonce: true }));<br>User-supplied CSP directives now...

fresh view socket client transitions vite

Related Articles