<!-- Canonical: https://lucioduran.com/es/blog/deno-2-node-compat-fresh-framework
Language: es
Content-SHA256: 22db2eb50ca91c05170198efe83cf8ea1d35afa4eb9104f500029306820c0185
-->

JavaScript Runtimes

# Deno 2: Compatibilidad Total con Node, Fresh 2.0 y Benchmarks Comparativos Contra Node 22 y Bun

2025-11-098 min lectura

Lucio Durán
Engineering Manager & AI Solutions Architect

También disponible en: [English](<https://lucioduran.com/blog/deno-2-node-compat-fresh-framework>)

## La Capa de Compatibilidad con Node.js

La compatibilidad de [Deno](<https://deno.com/>) 2 con Node.js se construye sobre tres pilares:

### 1. npm Specifiers

Es posible importar cualquier paquete npm directamente:

```typescript
import express from "npm:express@4";
import { PrismaClient } from "npm:@prisma/client";
import chalk from "npm:chalk@5";
```

Por debajo, Deno descarga el paquete a un cache global (` ~/.cache/deno/npm/ `) y reescribe el import map en tiempo de resolución. La diferencia clave con Node: **no hay carpeta ` node_modules ` por defecto.** Deno resuelve dependencias desde el cache global, deduplicando entre proyectos.

Esto realmente resuelve uno de los puntos de dolor más viejos de Node. Si teners 10 proyectos que todos usan ` lodash@4.17.21 `, Node almacena 10 copias. Deno almacena una.

```bash
# Ver dónde se resuelve un paquete
$ deno info npm:express@4
local: /home/user/.cache/deno/npm/registry.npmjs.org/express/4.21.1/
```

### 2. Polyfills de Módulos Built-in de Node

Deno implementa el protocolo ` node: ` para módulos built-in:

```typescript
import { readFile } from "node:fs/promises";
import { createServer } from "node:http";
import { join } from "node:path";
import { Buffer } from "node:buffer";
```

La cobertura es impresionante pero no completa. En Deno 2.1, la realidad es así:

```
Totalmente implementados: node:fs, node:path, node:url, node:buffer,
 node:crypto, node:stream, node:events,
 node:util, node:os, node:process, node:http,
 node:https, node:net, node:tls, node:zlib,
 node:assert, node:child_process, node:worker_threads

Parcialmente implementados: node:cluster (sin sticky sessions),
 node:dgram (solo UDP básico),
 node:dns (sin resolveNs/resolveSrv),
 node:vm (sin runInContext)

No implementados: node:v8 (heap snapshots de V8),
 node:trace_events,
 node:perf_hooks (parcial)
```

Para la mayoría de las aplicaciones web, la lista de completamente implementados cubre todo lo que se necesita. Donde surgen problemas es en paquetes nicho que dependen de ` node:vm ` para sandboxing o ` node:v8 ` para análisis de heap.

### 3. Soporte de package.json

Deno 2 lee ` package.json ` y respeta sus campos:

```json
{
 "name": "my-app",
 "scripts": {
 "dev": "deno task dev",
 "build": "deno task build"
 },
 "dependencies": {
 "hono": "^4.0.0",
 "drizzle-orm": "^0.30.0"
 }
}
```

```bash
# Deno lee package.json e instala dependencias
$ deno install
# O simplemente ejecutar, Deno auto-instala en el primer uso
$ deno run -A server.ts
```

Esta es la feature que hace la migración realista. Es posible tomar un proyecto Node.js existente, apuntar Deno a él, y la mayoría de las cosas simplemente funcionan.

## Fresh 2.0: El Framework que Hace Islands Bien

[Fresh](<https://fresh.deno.dev/>) es el framework web full-stack de Deno, y la versión 2.0 es un replanteamiento significativo. La idea core no cambió, rendering server-side por defecto con "islands" de interactividad, pero la implementación es mucho más refinada.

### Cómo Funcionan los Islands

```
SPA Tradicional:
 Servidor renderiza HTML → Cliente descarga toda la app React →
 Hidrata todo → La página se vuelve interactiva

Fresh Islands:
 Servidor renderiza HTML → Cliente descarga SOLO JS de islands →
 Hidrata SOLO islands → Resto de la página es HTML estático (cero JS)
```

A continuación se muestra cómo se ve en la práctica:

```tsx
// routes/products/[id].tsx, Ruta renderizada en servidor (cero JS cliente)
import { Handlers, PageProps } from "$fresh/server.ts";
import { ProductDetails } from "../components/ProductDetails.tsx";
import AddToCartIsland from "../islands/AddToCart.tsx";
import ReviewsIsland from "../islands/Reviews.tsx";

export const handler: Handlers<Product> = {
 async GET(_req, ctx) {
 const product = await db.products.findUnique({
 where: { id: ctx.params.id },
 });
 if (!product) return ctx.renderNotFound();
 return ctx.render(product);
 },
};

export default function ProductPage({ data }: PageProps<Product>) {
 return (
 <div class="max-w-4xl mx-auto">
 {/* Esto se renderiza en el servidor. Cero JS al cliente. */}
 <ProductDetails product={data} />

 {/* Esto es un island. Solo el JS de este componente se envía. */}
 <AddToCartIsland productId={data.id} price={data.price} />

 {/* Otro island. Se carga independientemente. */}
 <ReviewsIsland productId={data.id} />
 </div>
 );
}
```

```tsx
// islands/AddToCart.tsx, Esto envía JS al cliente
import { useSignal } from "@preact/signals";

export default function AddToCart({ productId, price }: Props) {
 const quantity = useSignal(1);
 const loading = useSignal(false);

 async function handleAddToCart() {
 loading.value = true;
 await fetch("/api/cart", {
 method: "POST",
 body: JSON.stringify({ productId, quantity: quantity.value }),
 });
 loading.value = false;
 }

 return (
 <div class="flex items-center gap-4">
 <select
 value={quantity.value}
 onChange={(e) => quantity.value = Number(e.currentTarget.value)}
 >
 {[1, 2, 3, 4, 5].map((n) => <option value={n}>{n}</option>)}
 </select>
 <button
 onClick={handleAddToCart}
 disabled={loading.value}
 class="bg-blue-600 text-white px-4 py-2 rounded"
 >
 Agregar al Carrito, ${(price * quantity.value).toFixed(2)}
 </button>
 </div>
 );
}
```

El componente ` ProductDetails ` envía cero JavaScript. El island ` AddToCart ` envía quizás 3KB de JS (runtime de señales Preact + componente). Para una página de producto que podría ser 50KB de HTML, se está enviando 3KB de JS en vez de los 150KB+ que una SPA React requeriría.

### Partials: Navegación SPA Sin SPAs

Fresh 2.0 introdujo Partials, una forma de actualizar secciones de la página sin navegación completa:

```tsx
import { Partial } from "$fresh/runtime.ts";

export function ProductList({ products, page }) {
 return (
 <Partial name="product-list">
 <div class="grid grid-cols-3 gap-4">
 {products.map((p) => (
 <a href={`/products?page=${page}`} f-partial="/partials/products">
 <ProductCard product={p} />
 </a>
 ))}
 </div>
 <Pagination page={page} />
 </Partial>
 );
}
```

Clickear un link con atributo ` f-partial ` fetches el nuevo fragmento HTML del servidor y intercambia solo esa sección del DOM. Sin librería de routing client-side. Sin state management de React. Solo HTML-over-the-wire con un runtime minúsculo (~2KB).

## Benchmarks: Análisis Contextual

Este es el punto donde una perspectiva contraria es justificada. Todos benchmarkean runtimes con servidores HTTP de "hello world". Ese es benchmarks son inútiles para aplicaciones del mundo real. Esto es lo que realmente importa.

### Setup de Tests

Todos los tests en la misma máquina (AMD Ryzen 9 7950X, 32GB RAM, Ubuntu 24.04):

- **Node 22.11.0** con Fastify 5.0
- **Deno 2.1.4** con Deno.serve()
- **Bun 1.1.38** con Bun.serve()

### Hello World HTTP (requests/sec)

```
Node 22 (Fastify): 128,000 req/s
Deno 2.1 (Deno.serve): 152,000 req/s
Bun 1.1 (Bun.serve): 167,000 req/s
```

Deno supera a Node por ~19%. Bun supera a Deno por ~10%. Estos números coinciden con el marketing. Pero la cosa es esta, este benchmark está midiendo la implementación del servidor HTTP, no el runtime.

### API JSON con Acceso a Base de Datos

Más realista, fetch de SQLite, serializar JSON, retornar:

```
Node 22 (Fastify + better-sqlite3): 24,500 req/s
Deno 2.1 (Deno.serve + deno-sqlite): 22,800 req/s
Bun 1.1 (Bun.serve + bun:sqlite): 26,100 req/s
```

La brecha se achica dramáticamente. Ahora estamos pasando la mayor parte del tiempo en V8 (los tres lo usan) y SQLite. La capa HTTP del runtime apenas importa.

### Server-Side Rendering (Preact/JSX)

Renderizar una página de producto con 50 items:

```
Node 22 (Fastify + preact-render-to-string): 3,800 req/s
Deno 2.1 (Fresh, SSR): 3,650 req/s
Bun 1.1 (Elysia + preact-render-to-string): 3,900 req/s
```

Esencialmente idénticos. SSR es enteramente CPU-bound en V8. El runtime no importa.

### Tiempo de Startup

Este es el punto donde las diferencias son marcadas:

```
Node 22 (script vacío): 35ms
Node 22 (servidor Fastify): 120ms
Deno 2.1 (script vacío): 45ms
Deno 2.1 (con type-check): 280ms
Deno 2.1 (--no-check): 50ms
Bun 1.1 (script vacío): 8ms
Bun 1.1 (servidor Elysia): 22ms
```

Bun destruye absolutamente a Node y Deno en startup. Para funciones serverless donde el cold start importa, esta es la característica más destacada de Bun.

### Uso de Memoria

Servidor HTTP idle:

```
Node 22 (Fastify): 45MB RSS
Deno 2.1 (Deno.serve): 38MB RSS
Bun 1.1 (Bun.serve): 32MB RSS
```

## Cuándo Elegir Qué

Basándose en el uso productivo de los tres runtimes en diferentes proyectos, esta es una evaluación honesta:

**Elegir Deno 2 cuando:**

- Se desea TypeScript built-in sin paso de build
- La seguridad importa (el modelo de permise es es único y valioso)
- Se está desplegando a Deno Deploy (su runtime edge es excelente)
- Se está arrancando un proyecto nuevo y se desea la arquitectura de islands de Fresh
- Se desea una sola herramienta para formateo, linting, testing, y ejecución

**Elegir Node 22 cuando:**

- Teners un codebase Node.js existente
- Se depende de addons nativos (node-gyp)
- Tu equipo conoce Node y el ecosistema profundamente
- Se necesita la compatibilidad de paquetes más amplia posible

**Elegir Bun cuando:**

- El tiempo de cold start es crítico (serverless, herramientas CLI)
- Se desea la experiencia de desarrollo más rápida posible
- Se necesita SQLite built-in
- Se está dispuesto a aceptar un runtime menos maduro por ganancias de velocidad

La verdad incómoda que ni el equipo de marketing de Deno ni el de Bun te van a decir es esta: **para la mayoría de las aplicaciones web, el runtime no importa.** Tu cuello de botella son las queries a la base de datos, el I/O de red, y la lógica de negocio. La diferencia entre 128K y 167K requests por segundo en un benchmark de hello world nunca va a ser relevante en producción.

Lo que sí importa es la experiencia de desarrollo, la compatibilidad del ecosistema, y la historia de deployment. En esas dimensiones, Deno 2 dio un salto masivo hacia adelante. Ya no te está pidiendo que abandones el ecosistema npm, te está dejando usarlo mientras ofrece algo mejor para código nuevo.

Ese es un pitch mucho más fuerte que "somos X% más rápidos sirviendo respuestas HTTP vacías."

## Preguntas frecuentes

### ¿Puedo usar cualquier paquete npm con Deno 2?

La mayoría de los paquetes funcionan out of the box con specifiers npm:. Paquetes que dependen de addons nativos (node-gyp), scripts postinstall, o APIs internas específicas de Node.js pueden necesitar el flag --node-modules-dir o directamente no funcionar. En la práctica, alrededor del 95% de los top 1000 paquetes npm funcionan sin problemas.

### ¿Deno es más rápido que Node.js?

Depende del workload. Para servir HTTP, el servidor basado en Hyper de Deno es 15-20% más rápido. Para trabajo CPU-bound, son esencialmente idénticos (ambos usan V8). Para tiempo de startup, Deno es más lento por el type-checking de TypeScript (a menos que uses --no-check). Bun es más rápido que ambos para startup.

### ¿Debería migrar mi proyecto Node.js a Deno 2?

Solo si valorás el TypeScript built-in de Deno, el modelo de permisos, o el runtime edge de Deno Deploy. Para proyectos Node.js existentes con pipelines de build complejos, el costo de migración raramente justifica los beneficios. Para proyectos nuevos, Deno 2 es un contendiente serio.

## Tips

1. **Resolución de npm specifiers:** Deno 2 resuelve specifiers ` npm: ` a través de un cache global en ` $DENO_DIR/npm/ `, no en ` node_modules/ `. Si un paquete usa scripts postinstall (addons nativos), se necesita el flag ` --node-modules-dir ` para crear node\_modules local. Verificar con ` deno info npm:nombre-paquete ` para ver el grafo de resolución antes de depurar problemas de imports.
2. **Hidratación parcial de Fresh 2.0:** Fresh usa señales de Preact para reactividad en islands. Envolver componentes interactivos con ` island() `, todo fuera de los islands envía cero JS al cliente. La clave: utilizar componentes ` <Partial> ` para actualizar secciones del DOM sin navegación completa de página. Esto te da transiciones estilo SPA con arquitectura MPA.
3. **Reutilización de V8 isolates en Deno Deploy:** Deno Deploy mantiene V8 isolates calientes entre requests. El primer request a un isolate frío tarda ~50ms, los siguientes reutilizan el isolate con ~1ms de overhead. Estructurar el código a nivel de módulo para que corra una vez (conexiones a DB, parsing de config) ya que persiste entre requests dentro del mismo ciclo de vida del isolate.
4. **Deno.serve() vs node:http en performance:** ` Deno.serve() ` usa Hyper (Rust) por debajo, bypaseando V8 para el manejo TCP. Para la capa HTTP específicamente, benchmarkea 15-20% más rápido que ` node:http ` de Node. Pero si tu handler es CPU-bound (rendering de templates, serialización JSON), la diferencia de runtime desaparece porque ambos pasan 95%+ del tiempo en V8.

- deno
- node-js
- bun
- fresh-framework
- javascript-runtimes
- typescript
- performance

[Volver al Blog](<https://lucioduran.com/es/blog>)
