{"schemaVersion":1,"id":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework","title":"Deno 2: Full Node Compatibility, Fresh 2.0, and Comparative Benchmarks Against Node 22 and Bun","description":"An honest technical assessment of Deno 2's Node.js compatibility layer, npm specifiers, Fresh 2.0's island architecture, and performance benchmarks that tell a more nuanced story than the marketing suggests.","language":"en","datePublished":"2025-11-09","dateModified":null,"contentFormat":"text/markdown","content":"JavaScript Runtimes\n\n# Deno 2: Full Node Compatibility, Fresh 2.0, and Comparative Benchmarks Against Node 22 and Bun\n\n2025-11-098 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/deno-2-node-compat-fresh-framework>)\n\n## The Node.js Compatibility Layer\n\n[Deno](<https://deno.com/>) 2's compatibility with Node.js is built on three pillars:\n\n### 1. npm Specifiers\n\nYou can import any npm package directly:\n\n```typescript\nimport express from \"npm:express@4\";\nimport { PrismaClient } from \"npm:@prisma/client\";\nimport chalk from \"npm:chalk@5\";\n```\n\nUnder the hood, Deno downloads the package to a global cache (` ~/.cache/deno/npm/ `) and rewrites the import map at resolution time. The key difference from Node: **there's no ` node_modules ` folder by default.** Deno resolves dependencies from the global cache, deduplicating across projects.\n\nThis actually solves one of Node's longest-standing pain points. If you have 10 projects that all use ` lodash@4.17.21 `, Node stores 10 copies. Deno stores one.\n\n```bash\n# See where a package resolves to\n$ deno info npm:express@4\nlocal: /home/user/.cache/deno/npm/registry.npmjs.org/express/4.21.1/\n```\n\n### 2. Node Built-in Module Polyfills\n\nDeno implements the ` node: ` protocol for built-in modules:\n\n```typescript\nimport { readFile } from \"node:fs/promises\";\nimport { createServer } from \"node:http\";\nimport { join } from \"node:path\";\nimport { Buffer } from \"node:buffer\";\n```\n\nThe coverage is impressive but not complete. As of Deno 2.1, here's the reality:\n\n```\nFully implemented: node:fs, node:path, node:url, node:buffer,\n node:crypto, node:stream, node:events,\n node:util, node:os, node:process, node:http,\n node:https, node:net, node:tls, node:zlib,\n node:assert, node:child_process, node:worker_threads\n\nPartially implemented: node:cluster (no sticky sessions),\n node:dgram (basic UDP only),\n node:dns (no resolveNs/resolveSrv),\n node:vm (no runInContext)\n\nNot implemented: node:v8 (V8 heap snapshots),\n node:trace_events,\n node:perf_hooks (partial)\n```\n\nFor most web applications, the fully implemented list covers everything you need. Where trouble arises is niche packages that depend on ` node:vm ` for sandboxing or ` node:v8 ` for heap analysis.\n\n### 3. package.json Support\n\nDeno 2 reads ` package.json ` and respects its fields:\n\n```json\n{\n \"name\": \"my-app\",\n \"scripts\": {\n \"dev\": \"deno task dev\",\n \"build\": \"deno task build\"\n },\n \"dependencies\": {\n \"hono\": \"^4.0.0\",\n \"drizzle-orm\": \"^0.30.0\"\n }\n}\n```\n\n```bash\n# Deno reads package.json and installs dependencies\n$ deno install\n# Or just run, Deno auto-installs on first use\n$ deno run -A server.ts\n```\n\nThis is the feature that makes migration realistic. You can take an existing Node.js project, point Deno at it, and most things just work. The migration path is incremental: start running with Deno, fix compatibility issues one at a time, then gradually adopt Deno-native APIs where they make sense.\n\n## Fresh 2.0: The Framework That Gets Islands Right\n\n[Fresh](<https://fresh.deno.dev/>) is Deno's full-stack web framework, and version 2.0 is a significant rethinking. The core idea hasn't changed, server-side rendering by default with \"islands\" of interactivity, but the implementation is much more refined.\n\n### How Islands Work\n\n```\nTraditional SPA:\n Server renders HTML → Client downloads entire React app →\n Hydrates everything → Page becomes interactive\n\nFresh Islands:\n Server renders HTML → Client downloads ONLY island JS →\n Hydrates ONLY islands → Rest of page is static HTML (zero JS)\n```\n\nThe following shows what this looks like in practice:\n\n```tsx\n// routes/products/[id].tsx, Server-rendered route (zero client JS)\nimport { Handlers, PageProps } from \"$fresh/server.ts\";\nimport { ProductDetails } from \"../components/ProductDetails.tsx\";\nimport AddToCartIsland from \"../islands/AddToCart.tsx\";\nimport ReviewsIsland from \"../islands/Reviews.tsx\";\n\ninterface Product {\n id: string;\n name: string;\n price: number;\n description: string;\n}\n\nexport const handler: Handlers<Product> = {\n async GET(_req, ctx) {\n const product = await db.products.findUnique({\n where: { id: ctx.params.id },\n });\n if (!product) return ctx.renderNotFound();\n return ctx.render(product);\n },\n};\n\nexport default function ProductPage({ data }: PageProps<Product>) {\n return (\n <div class=\"max-w-4xl mx-auto\">\n {/* This renders on the server. Zero JS sent to client. */}\n <ProductDetails product={data} />\n\n {/* This is an island. Only this component's JS is sent. */}\n <AddToCartIsland productId={data.id} price={data.price} />\n\n {/* Another island. Loaded independently. */}\n <ReviewsIsland productId={data.id} />\n </div>\n );\n}\n```\n\n```tsx\n// islands/AddToCart.tsx, This ships JS to the client\nimport { useSignal } from \"@preact/signals\";\n\ninterface Props {\n productId: string;\n price: number;\n}\n\nexport default function AddToCart({ productId, price }: Props) {\n const quantity = useSignal(1);\n const loading = useSignal(false);\n\n async function handleAddToCart() {\n loading.value = true;\n await fetch(\"/api/cart\", {\n method: \"POST\",\n body: JSON.stringify({ productId, quantity: quantity.value }),\n });\n loading.value = false;\n }\n\n return (\n <div class=\"flex items-center gap-4\">\n <select\n value={quantity.value}\n onChange={(e) => quantity.value = Number(e.currentTarget.value)}\n >\n {[1, 2, 3, 4, 5].map((n) => <option value={n}>{n}</option>)}\n </select>\n <button\n onClick={handleAddToCart}\n disabled={loading.value}\n class=\"bg-blue-600 text-white px-4 py-2 rounded\"\n >\n Add to Cart, ${(price * quantity.value).toFixed(2)}\n </button>\n </div>\n );\n}\n```\n\nThe ` ProductDetails ` component sends zero JavaScript. The ` AddToCart ` island sends maybe 3KB of JS (Preact signals runtime + component). For a product page that might be 50KB of HTML, you're sending 3KB of JS instead of the 150KB+ that a React SPA would require.\n\n### Partials: SPA Navigation Without SPAs\n\nFresh 2.0 introduced Partials, a way to update sections of the page without full navigation:\n\n```tsx\n// components/ProductList.tsx\nimport { Partial } from \"$fresh/runtime.ts\";\n\nexport function ProductList({ products, page }) {\n return (\n <Partial name=\"product-list\">\n <div class=\"grid grid-cols-3 gap-4\">\n {products.map((p) => (\n <a href={`/products?page=${page}`} f-partial=\"/partials/products\">\n <ProductCard product={p} />\n </a>\n ))}\n </div>\n <Pagination page={page} />\n </Partial>\n );\n}\n```\n\nClicking a link with ` f-partial ` attribute fetches the new HTML fragment from the server and swaps just that section of the DOM. No client-side routing library. No React state management. Just HTML-over-the-wire with a tiny (~2KB) runtime.\n\n## Benchmarks: Contextual Analysis\n\nRuntime benchmarks runtimes with \"hello world\" HTTP servers. Those benchmarks are useless for real-world applications. Here is what actually matters.\n\n### Test Setup\n\nAll tests run on the same machine (AMD Ryzen 9 7950X, 32GB RAM, Ubuntu 24.04):\n\n- **Node 22.11.0** with Fastify 5.0\n- **Deno 2.1.4** with Deno.serve()\n- **Bun 1.1.38** with Bun.serve()\n\n### Hello World HTTP (requests/sec)\n\n```\nNode 22 (Fastify): 128,000 req/s\nDeno 2.1 (Deno.serve): 152,000 req/s\nBun 1.1 (Bun.serve): 167,000 req/s\n```\n\nDeno wins against Node by ~19%. Bun wins against Deno by ~10%. These numbers match the marketing. However, this benchmark is measuring the HTTP server implementation, not the runtime. Deno uses Hyper (Rust), Bun uses its own C/Zig implementation, Node uses libuv+llhttp.\n\n### JSON API with Database Access\n\nMore realistic, fetch from SQLite, serialize JSON, return:\n\n```\nNode 22 (Fastify + better-sqlite3): 24,500 req/s\nDeno 2.1 (Deno.serve + deno-sqlite): 22,800 req/s\nBun 1.1 (Bun.serve + bun:sqlite): 26,100 req/s\n```\n\nThe gap shrinks dramatically. Now most time is spent in V8 (all three use it) and SQLite. The runtime's HTTP layer barely matters.\n\n### Server-Side Rendering (Preact/JSX)\n\nRender a product page with 50 items:\n\n```\nNode 22 (Fastify + preact-render-to-string): 3,800 req/s\nDeno 2.1 (Fresh, SSR): 3,650 req/s\nBun 1.1 (Elysia + preact-render-to-string): 3,900 req/s\n```\n\nEssentially identical. SSR is entirely CPU-bound in V8. The runtime doesn't matter.\n\n### Startup Time\n\nThis is where the differences are stark:\n\n```\nNode 22 (empty script): 35ms\nNode 22 (Fastify server): 120ms\nDeno 2.1 (empty script): 45ms\nDeno 2.1 (with type-check): 280ms\nDeno 2.1 (--no-check): 50ms\nBun 1.1 (empty script): 8ms\nBun 1.1 (Elysia server): 22ms\n```\n\nBun absolutely destroys both Node and Deno on startup. For serverless functions where cold start matters, this is Bun's killer feature. Deno's TypeScript type-checking adds ~230ms, use ` --no-check ` in production if startup time matters.\n\n### Memory Usage\n\nIdle HTTP server:\n\n```\nNode 22 (Fastify): 45MB RSS\nDeno 2.1 (Deno.serve): 38MB RSS\nBun 1.1 (Bun.serve): 32MB RSS\n```\n\nUnder load (10,000 concurrent connections):\n\n```\nNode 22 (Fastify): 210MB RSS\nDeno 2.1 (Deno.serve): 180MB RSS\nBun 1.1 (Bun.serve): 165MB RSS\n```\n\nDeno is slightly leaner than Node, likely because it doesn't load the full Node.js standard library. Bun's custom allocator gives it a consistent memory advantage.\n\n## When to Choose What\n\nBased on production use of all three runtimes across different projects, here is an honest assessment:\n\n**Choose Deno 2 when:**\n\n- You want built-in TypeScript without a build step\n- Security matters (permissions model is unique and valuable)\n- You're deploying to Deno Deploy (their edge runtime is excellent)\n- You're starting a new project and want Fresh's island architecture\n- You want a single tool for formatting, linting, testing, and running\n\n**Choose Node 22 when:**\n\n- You have an existing Node.js codebase\n- You depend on native addons (node-gyp)\n- Your team knows Node and the ecosystem deeply\n- You need the broadest possible package compatibility\n- You're using a Node-specific framework (NestJS, etc.)\n\n**Choose Bun when:**\n\n- Cold start time is critical (serverless, CLI tools)\n- You want the fastest possible development experience\n- You need built-in SQLite\n- You're willing to accept a less mature runtime for speed gains\n\nThe uncomfortable truth that neither the Deno nor the Bun marketing team will tell anyone: **for most web applications, the runtime doesn't matter.** Your bottleneck is database queries, network I/O, and business logic. The difference between 128K and 167K requests per second on a hello world benchmark will never be relevant in production.\n\nWhat does matter is developer experience, ecosystem compatibility, and deployment story. On those dimensions, Deno 2 made a massive leap forward. It's no longer asking you to abandon the npm ecosystem, it's letting you use it while offering something better for new code.\n\nThat's a much stronger pitch than \"we're X% faster at serving empty HTTP responses.\"\n\n## Frequently asked questions\n\n### Can I use any npm package with Deno 2?\n\nMost packages work out of the box with npm: specifiers. Packages that rely on native addons (node-gyp), postinstall scripts, or Node.js-specific internal APIs may need the --node-modules-dir flag or may not work at all. In practice, about 95% of the top 1000 npm packages work without issues.\n\n### Is Deno faster than Node.js?\n\nIt depends on the workload. For HTTP serving, Deno's Hyper-based server is 15-20% faster. For CPU-bound work, they're essentially identical (both use V8). For startup time, Deno is slower due to TypeScript type-checking (unless using --no-check). Bun is faster than both for startup.\n\n### Should I migrate my Node.js project to Deno 2?\n\nOnly if you value Deno's built-in TypeScript, permissions model, or Deno Deploy's edge runtime. For existing Node.js projects with complex build pipelines, the migration cost rarely justifies the benefits. For new projects, Deno 2 is a serious contender.\n\n## Tips\n\n1. **npm specifier resolution:** Deno 2 resolves ` npm: ` specifiers through a global cache at ` $DENO_DIR/npm/ `, not ` node_modules/ `. If a package uses postinstall scripts (native addons), you need ` --node-modules-dir ` flag to create a local node\\_modules. Check with ` deno info npm:package-name ` to see the resolution graph before debugging import issues.\n2. **Fresh 2.0 partial hydration:** Fresh uses Preact signals for island reactivity. Wrap interactive components with ` island() `, everything outside islands ships zero JS to the client. The key insight: use ` <Partial> ` components for updating DOM sections without full page navigation. This gives you SPA-like transitions with MPA architecture.\n3. **V8 isolate reuse in Deno Deploy:** Deno Deploy keeps V8 isolates warm between requests. First request to a cold isolate takes ~50ms, subsequent requests reuse the isolate at ~1ms overhead. Structure your module-level code to run once (database connections, config parsing) since it persists across requests within the same isolate lifecycle.\n4. **Deno.serve() vs node:http performance:** ` Deno.serve() ` uses Hyper (Rust) under the hood, bypassing V8 for TCP handling. For the HTTP layer specifically, it benchmarks 15-20% faster than Node's ` node:http `. But if your handler is CPU-bound (template rendering, JSON serialization), the runtime difference disappears because both spend 95%+ time in V8.\n\n- deno\n- node-js\n- bun\n- fresh-framework\n- javascript-runtimes\n- typescript\n- performance\n\n[Back to Blog](<https://lucioduran.com/blog>)","headings":[{"level":1,"text":"Deno 2: Full Node Compatibility, Fresh 2.0, and Comparative Benchmarks Against Node 22 and Bun","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#title"},{"level":2,"text":"The Node.js Compatibility Layer","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#the-nodejs-compatibility-layer"},{"level":3,"text":"1. npm Specifiers","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#1-npm-specifiers"},{"level":3,"text":"2. Node Built-in Module Polyfills","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#2-node-built-in-module-polyfills"},{"level":3,"text":"3. package.json Support","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#3-packagejson-support"},{"level":2,"text":"Fresh 2.0: The Framework That Gets Islands Right","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#fresh-20-the-framework-that-gets-islands-right"},{"level":3,"text":"How Islands Work","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#how-islands-work"},{"level":3,"text":"Partials: SPA Navigation Without SPAs","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#partials-spa-navigation-without-spas"},{"level":2,"text":"Benchmarks: Contextual Analysis","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#benchmarks-contextual-analysis"},{"level":3,"text":"Test Setup","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#test-setup"},{"level":3,"text":"Hello World HTTP (requests/sec)","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#hello-world-http-requestssec"},{"level":3,"text":"JSON API with Database Access","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#json-api-with-database-access"},{"level":3,"text":"Server-Side Rendering (Preact/JSX)","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#server-side-rendering-preactjsx"},{"level":3,"text":"Startup Time","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#startup-time"},{"level":3,"text":"Memory Usage","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#memory-usage"},{"level":2,"text":"When to Choose What","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#when-to-choose-what"},{"level":2,"text":"Frequently asked questions","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#questions"},{"level":3,"text":"Can I use any npm package with Deno 2?","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#question-1"},{"level":3,"text":"Is Deno faster than Node.js?","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#question-2"},{"level":3,"text":"Should I migrate my Node.js project to Deno 2?","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#article-tips"}],"sections":[{"heading":"Deno 2: Full Node Compatibility, Fresh 2.0, and Comparative Benchmarks Against Node 22 and Bun","level":1,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework","content":"JavaScript Runtimes"},{"heading":"Deno 2: Full Node Compatibility, Fresh 2.0, and Comparative Benchmarks Against Node 22 and Bun","level":1,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#title","content":"2025-11-098 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/deno-2-node-compat-fresh-framework>)"},{"heading":"The Node.js Compatibility Layer","level":2,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#the-nodejs-compatibility-layer","content":"[Deno](<https://deno.com/>) 2's compatibility with Node.js is built on three pillars:"},{"heading":"1. npm Specifiers","level":3,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#1-npm-specifiers","content":"You can import any npm package directly:\n\n```typescript\nimport express from \"npm:express@4\";\nimport { PrismaClient } from \"npm:@prisma/client\";\nimport chalk from \"npm:chalk@5\";\n```\n\nUnder the hood, Deno downloads the package to a global cache (` ~/.cache/deno/npm/ `) and rewrites the import map at resolution time. The key difference from Node: **there's no ` node_modules ` folder by default.** Deno resolves dependencies from the global cache, deduplicating across projects.\n\nThis actually solves one of Node's longest-standing pain points. If you have 10 projects that all use ` lodash@4.17.21 `, Node stores 10 copies. Deno stores one.\n\n```bash\n# See where a package resolves to\n$ deno info npm:express@4\nlocal: /home/user/.cache/deno/npm/registry.npmjs.org/express/4.21.1/\n```"},{"heading":"2. Node Built-in Module Polyfills","level":3,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#2-node-built-in-module-polyfills","content":"Deno implements the ` node: ` protocol for built-in modules:\n\n```typescript\nimport { readFile } from \"node:fs/promises\";\nimport { createServer } from \"node:http\";\nimport { join } from \"node:path\";\nimport { Buffer } from \"node:buffer\";\n```\n\nThe coverage is impressive but not complete. As of Deno 2.1, here's the reality:\n\n```\nFully implemented: node:fs, node:path, node:url, node:buffer,\n node:crypto, node:stream, node:events,\n node:util, node:os, node:process, node:http,\n node:https, node:net, node:tls, node:zlib,\n node:assert, node:child_process, node:worker_threads\n\nPartially implemented: node:cluster (no sticky sessions),\n node:dgram (basic UDP only),\n node:dns (no resolveNs/resolveSrv),\n node:vm (no runInContext)\n\nNot implemented: node:v8 (V8 heap snapshots),\n node:trace_events,\n node:perf_hooks (partial)\n```\n\nFor most web applications, the fully implemented list covers everything you need. Where trouble arises is niche packages that depend on ` node:vm ` for sandboxing or ` node:v8 ` for heap analysis."},{"heading":"3. package.json Support","level":3,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#3-packagejson-support","content":"Deno 2 reads ` package.json ` and respects its fields:\n\n```json\n{\n \"name\": \"my-app\",\n \"scripts\": {\n \"dev\": \"deno task dev\",\n \"build\": \"deno task build\"\n },\n \"dependencies\": {\n \"hono\": \"^4.0.0\",\n \"drizzle-orm\": \"^0.30.0\"\n }\n}\n```\n\n```bash\n# Deno reads package.json and installs dependencies\n$ deno install\n# Or just run, Deno auto-installs on first use\n$ deno run -A server.ts\n```\n\nThis is the feature that makes migration realistic. You can take an existing Node.js project, point Deno at it, and most things just work. The migration path is incremental: start running with Deno, fix compatibility issues one at a time, then gradually adopt Deno-native APIs where they make sense."},{"heading":"Fresh 2.0: The Framework That Gets Islands Right","level":2,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#fresh-20-the-framework-that-gets-islands-right","content":"[Fresh](<https://fresh.deno.dev/>) is Deno's full-stack web framework, and version 2.0 is a significant rethinking. The core idea hasn't changed, server-side rendering by default with \"islands\" of interactivity, but the implementation is much more refined."},{"heading":"How Islands Work","level":3,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#how-islands-work","content":"```\nTraditional SPA:\n Server renders HTML → Client downloads entire React app →\n Hydrates everything → Page becomes interactive\n\nFresh Islands:\n Server renders HTML → Client downloads ONLY island JS →\n Hydrates ONLY islands → Rest of page is static HTML (zero JS)\n```\n\nThe following shows what this looks like in practice:\n\n```tsx\n// routes/products/[id].tsx, Server-rendered route (zero client JS)\nimport { Handlers, PageProps } from \"$fresh/server.ts\";\nimport { ProductDetails } from \"../components/ProductDetails.tsx\";\nimport AddToCartIsland from \"../islands/AddToCart.tsx\";\nimport ReviewsIsland from \"../islands/Reviews.tsx\";\n\ninterface Product {\n id: string;\n name: string;\n price: number;\n description: string;\n}\n\nexport const handler: Handlers<Product> = {\n async GET(_req, ctx) {\n const product = await db.products.findUnique({\n where: { id: ctx.params.id },\n });\n if (!product) return ctx.renderNotFound();\n return ctx.render(product);\n },\n};\n\nexport default function ProductPage({ data }: PageProps<Product>) {\n return (\n <div class=\"max-w-4xl mx-auto\">\n {/* This renders on the server. Zero JS sent to client. */}\n <ProductDetails product={data} />\n\n {/* This is an island. Only this component's JS is sent. */}\n <AddToCartIsland productId={data.id} price={data.price} />\n\n {/* Another island. Loaded independently. */}\n <ReviewsIsland productId={data.id} />\n </div>\n );\n}\n```\n\n```tsx\n// islands/AddToCart.tsx, This ships JS to the client\nimport { useSignal } from \"@preact/signals\";\n\ninterface Props {\n productId: string;\n price: number;\n}\n\nexport default function AddToCart({ productId, price }: Props) {\n const quantity = useSignal(1);\n const loading = useSignal(false);\n\n async function handleAddToCart() {\n loading.value = true;\n await fetch(\"/api/cart\", {\n method: \"POST\",\n body: JSON.stringify({ productId, quantity: quantity.value }),\n });\n loading.value = false;\n }\n\n return (\n <div class=\"flex items-center gap-4\">\n <select\n value={quantity.value}\n onChange={(e) => quantity.value = Number(e.currentTarget.value)}\n >\n {[1, 2, 3, 4, 5].map((n) => <option value={n}>{n}</option>)}\n </select>\n <button\n onClick={handleAddToCart}\n disabled={loading.value}\n class=\"bg-blue-600 text-white px-4 py-2 rounded\"\n >\n Add to Cart, ${(price * quantity.value).toFixed(2)}\n </button>\n </div>\n );\n}\n```\n\nThe ` ProductDetails ` component sends zero JavaScript. The ` AddToCart ` island sends maybe 3KB of JS (Preact signals runtime + component). For a product page that might be 50KB of HTML, you're sending 3KB of JS instead of the 150KB+ that a React SPA would require."},{"heading":"Partials: SPA Navigation Without SPAs","level":3,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#partials-spa-navigation-without-spas","content":"Fresh 2.0 introduced Partials, a way to update sections of the page without full navigation:\n\n```tsx\n// components/ProductList.tsx\nimport { Partial } from \"$fresh/runtime.ts\";\n\nexport function ProductList({ products, page }) {\n return (\n <Partial name=\"product-list\">\n <div class=\"grid grid-cols-3 gap-4\">\n {products.map((p) => (\n <a href={`/products?page=${page}`} f-partial=\"/partials/products\">\n <ProductCard product={p} />\n </a>\n ))}\n </div>\n <Pagination page={page} />\n </Partial>\n );\n}\n```\n\nClicking a link with ` f-partial ` attribute fetches the new HTML fragment from the server and swaps just that section of the DOM. No client-side routing library. No React state management. Just HTML-over-the-wire with a tiny (~2KB) runtime."},{"heading":"Benchmarks: Contextual Analysis","level":2,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#benchmarks-contextual-analysis","content":"Runtime benchmarks runtimes with \"hello world\" HTTP servers. Those benchmarks are useless for real-world applications. Here is what actually matters."},{"heading":"Test Setup","level":3,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#test-setup","content":"All tests run on the same machine (AMD Ryzen 9 7950X, 32GB RAM, Ubuntu 24.04):\n\n- **Node 22.11.0** with Fastify 5.0\n- **Deno 2.1.4** with Deno.serve()\n- **Bun 1.1.38** with Bun.serve()"},{"heading":"Hello World HTTP (requests/sec)","level":3,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#hello-world-http-requestssec","content":"```\nNode 22 (Fastify): 128,000 req/s\nDeno 2.1 (Deno.serve): 152,000 req/s\nBun 1.1 (Bun.serve): 167,000 req/s\n```\n\nDeno wins against Node by ~19%. Bun wins against Deno by ~10%. These numbers match the marketing. However, this benchmark is measuring the HTTP server implementation, not the runtime. Deno uses Hyper (Rust), Bun uses its own C/Zig implementation, Node uses libuv+llhttp."},{"heading":"JSON API with Database Access","level":3,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#json-api-with-database-access","content":"More realistic, fetch from SQLite, serialize JSON, return:\n\n```\nNode 22 (Fastify + better-sqlite3): 24,500 req/s\nDeno 2.1 (Deno.serve + deno-sqlite): 22,800 req/s\nBun 1.1 (Bun.serve + bun:sqlite): 26,100 req/s\n```\n\nThe gap shrinks dramatically. Now most time is spent in V8 (all three use it) and SQLite. The runtime's HTTP layer barely matters."},{"heading":"Server-Side Rendering (Preact/JSX)","level":3,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#server-side-rendering-preactjsx","content":"Render a product page with 50 items:\n\n```\nNode 22 (Fastify + preact-render-to-string): 3,800 req/s\nDeno 2.1 (Fresh, SSR): 3,650 req/s\nBun 1.1 (Elysia + preact-render-to-string): 3,900 req/s\n```\n\nEssentially identical. SSR is entirely CPU-bound in V8. The runtime doesn't matter."},{"heading":"Startup Time","level":3,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#startup-time","content":"This is where the differences are stark:\n\n```\nNode 22 (empty script): 35ms\nNode 22 (Fastify server): 120ms\nDeno 2.1 (empty script): 45ms\nDeno 2.1 (with type-check): 280ms\nDeno 2.1 (--no-check): 50ms\nBun 1.1 (empty script): 8ms\nBun 1.1 (Elysia server): 22ms\n```\n\nBun absolutely destroys both Node and Deno on startup. For serverless functions where cold start matters, this is Bun's killer feature. Deno's TypeScript type-checking adds ~230ms, use ` --no-check ` in production if startup time matters."},{"heading":"Memory Usage","level":3,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#memory-usage","content":"Idle HTTP server:\n\n```\nNode 22 (Fastify): 45MB RSS\nDeno 2.1 (Deno.serve): 38MB RSS\nBun 1.1 (Bun.serve): 32MB RSS\n```\n\nUnder load (10,000 concurrent connections):\n\n```\nNode 22 (Fastify): 210MB RSS\nDeno 2.1 (Deno.serve): 180MB RSS\nBun 1.1 (Bun.serve): 165MB RSS\n```\n\nDeno is slightly leaner than Node, likely because it doesn't load the full Node.js standard library. Bun's custom allocator gives it a consistent memory advantage."},{"heading":"When to Choose What","level":2,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#when-to-choose-what","content":"Based on production use of all three runtimes across different projects, here is an honest assessment:\n\n**Choose Deno 2 when:**\n\n- You want built-in TypeScript without a build step\n- Security matters (permissions model is unique and valuable)\n- You're deploying to Deno Deploy (their edge runtime is excellent)\n- You're starting a new project and want Fresh's island architecture\n- You want a single tool for formatting, linting, testing, and running\n\n**Choose Node 22 when:**\n\n- You have an existing Node.js codebase\n- You depend on native addons (node-gyp)\n- Your team knows Node and the ecosystem deeply\n- You need the broadest possible package compatibility\n- You're using a Node-specific framework (NestJS, etc.)\n\n**Choose Bun when:**\n\n- Cold start time is critical (serverless, CLI tools)\n- You want the fastest possible development experience\n- You need built-in SQLite\n- You're willing to accept a less mature runtime for speed gains\n\nThe uncomfortable truth that neither the Deno nor the Bun marketing team will tell anyone: **for most web applications, the runtime doesn't matter.** Your bottleneck is database queries, network I/O, and business logic. The difference between 128K and 167K requests per second on a hello world benchmark will never be relevant in production.\n\nWhat does matter is developer experience, ecosystem compatibility, and deployment story. On those dimensions, Deno 2 made a massive leap forward. It's no longer asking you to abandon the npm ecosystem, it's letting you use it while offering something better for new code.\n\nThat's a much stronger pitch than \"we're X% faster at serving empty HTTP responses.\""},{"heading":"Frequently asked questions","level":2,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#questions","content":""},{"heading":"Can I use any npm package with Deno 2?","level":3,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#question-1","content":"Most packages work out of the box with npm: specifiers. Packages that rely on native addons (node-gyp), postinstall scripts, or Node.js-specific internal APIs may need the --node-modules-dir flag or may not work at all. In practice, about 95% of the top 1000 npm packages work without issues."},{"heading":"Is Deno faster than Node.js?","level":3,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#question-2","content":"It depends on the workload. For HTTP serving, Deno's Hyper-based server is 15-20% faster. For CPU-bound work, they're essentially identical (both use V8). For startup time, Deno is slower due to TypeScript type-checking (unless using --no-check). Bun is faster than both for startup."},{"heading":"Should I migrate my Node.js project to Deno 2?","level":3,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#question-3","content":"Only if you value Deno's built-in TypeScript, permissions model, or Deno Deploy's edge runtime. For existing Node.js projects with complex build pipelines, the migration cost rarely justifies the benefits. For new projects, Deno 2 is a serious contender."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#article-tips","content":"1. **npm specifier resolution:** Deno 2 resolves ` npm: ` specifiers through a global cache at ` $DENO_DIR/npm/ `, not ` node_modules/ `. If a package uses postinstall scripts (native addons), you need ` --node-modules-dir ` flag to create a local node\\_modules. Check with ` deno info npm:package-name ` to see the resolution graph before debugging import issues.\n2. **Fresh 2.0 partial hydration:** Fresh uses Preact signals for island reactivity. Wrap interactive components with ` island() `, everything outside islands ships zero JS to the client. The key insight: use ` <Partial> ` components for updating DOM sections without full page navigation. This gives you SPA-like transitions with MPA architecture.\n3. **V8 isolate reuse in Deno Deploy:** Deno Deploy keeps V8 isolates warm between requests. First request to a cold isolate takes ~50ms, subsequent requests reuse the isolate at ~1ms overhead. Structure your module-level code to run once (database connections, config parsing) since it persists across requests within the same isolate lifecycle.\n4. **Deno.serve() vs node:http performance:** ` Deno.serve() ` uses Hyper (Rust) under the hood, bypassing V8 for TCP handling. For the HTTP layer specifically, it benchmarks 15-20% faster than Node's ` node:http `. But if your handler is CPU-bound (template rendering, JSON serialization), the runtime difference disappears because both spend 95%+ time in V8.\n\n- deno\n- node-js\n- bun\n- fresh-framework\n- javascript-runtimes\n- typescript\n- performance\n\n[Back to Blog](<https://lucioduran.com/blog>)"}],"links":[{"text":"Español","url":"https://lucioduran.com/es/blog/deno-2-node-compat-fresh-framework"},{"text":"Deno","url":"https://deno.com/"},{"text":"Fresh","url":"https://fresh.deno.dev/"},{"text":"Back to Blog","url":"https://lucioduran.com/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework"},"headline":"Deno 2: Full Node Compatibility, Fresh 2.0, and Comparative Benchmarks Against Node 22 and Bun","description":"An honest technical assessment of Deno 2's Node.js compatibility layer, npm specifiers, Fresh 2.0's island architecture, and performance benchmarks that tell a more nuanced story than the marketing suggests.","datePublished":"2025-11-09","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"JavaScript Runtimes","keywords":"deno, node-js, bun, fresh-framework, javascript-runtimes, typescript, performance","wordCount":1422,"timeRequired":"PT8M","workTranslation":[{"@type":"BlogPosting","inLanguage":"es","url":"https://lucioduran.com/es/blog/deno-2-node-compat-fresh-framework"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/blog"},{"@type":"ListItem","position":3,"name":"Deno 2: Full Node Compatibility, Fresh 2.0, and Comparative Benchmarks Against Node 22 and Bun","item":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Can I use any npm package with Deno 2?","acceptedAnswer":{"@type":"Answer","text":"Most packages work out of the box with npm: specifiers. Packages that rely on native addons (node-gyp), postinstall scripts, or Node.js-specific internal APIs may need the --node-modules-dir flag or may not work at all. In practice, about 95% of the top 1000 npm packages work without issues."}},{"@type":"Question","name":"Is Deno faster than Node.js?","acceptedAnswer":{"@type":"Answer","text":"It depends on the workload. For HTTP serving, Deno's Hyper-based server is 15-20% faster. For CPU-bound work, they're essentially identical (both use V8). For startup time, Deno is slower due to TypeScript type-checking (unless using --no-check). Bun is faster than both for startup."}},{"@type":"Question","name":"Should I migrate my Node.js project to Deno 2?","acceptedAnswer":{"@type":"Answer","text":"Only if you value Deno's built-in TypeScript, permissions model, or Deno Deploy's edge runtime. For existing Node.js projects with complex build pipelines, the migration cost rarely justifies the benefits. For new projects, Deno 2 is a serious contender."}}]}]}],"contentHash":"3bc17f9b64a009a599c775b689eb76f3b078be753e1d166d8bc70b8d56425f3b","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/blog/deno-2-node-compat-fresh-framework","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":67454,"contentBytes":13422}}
