Deep dive
Decision table
| Job | Pick | Why |
|---|---|---|
| Web frontend | TypeScript + React (or Vue, Svelte, Solid) | Ecosystem and hiring |
| Full-stack web app | Next.js, Remix/React Router, SvelteKit, Nuxt | SSR, routing, data loading |
| API server | Node + Fastify or Hono; NestJS for large teams | Mature, fast enough |
| Edge / serverless functions | Hono or plain handlers on Cloudflare Workers / Vercel / Deno Deploy | V8 isolates, ms cold starts |
| Fast tooling and scripts | Bun or Deno | Built-in TS, test runner, bundler |
| Runtime validation | zod / valibot (types inferred from schemas) | One source of truth for type and check |
| Desktop app | Electron (mature) or Tauri (Rust shell, smaller) | Web skills on desktop |
| Mobile | React Native / Expo | Shared logic with web |
| CPU hot path | Rust → WASM, or a native addon (napi-rs) | Escape the JIT and GC |
Memory and performance model
- V8: interpreter (Ignition) → baseline and optimizing JITs (Sparkplug, Maglev, TurboFan) guided by type feedback. Hidden classes and inline caches make objects with stable shapes fast; changing shapes or mixing types in one array deoptimizes.
- Generational GC (Orinoco): young-generation scavenges are cheap; old-generation marking is mostly concurrent. Heap size limit set by
--max-old-space-size(MiB). - Typed arrays (
Float64Array,Uint8Array) give contiguous, unboxed memory. - Startup: Node order of 30-50 ms; V8 isolates on edge platforms start in order of ms.
Concurrency model
- Event loop: timers → I/O callbacks (libuv over epoll/kqueue/IOCP) → microtasks (promise continuations) after each task.
awaityields to the loop. - File system and DNS calls use libuv's thread pool (default 4 threads,
UV_THREADPOOL_SIZE). worker_threadsfor CPU work, communicating bypostMessage(structured clone) orSharedArrayBuffer+Atomics.- Scale across cores with multiple processes (cluster, or one container per core behind a load balancer).
Code
TypeScript (Node 22+, Bun or Deno; npm i zod): validate at the boundary, time out every request, bound concurrency.
import { z } from "zod";
const User = z.object({
id: z.number().int().positive(),
email: z.string().email(),
tags: z.array(z.string()).max(20).default([]),
});
type User = z.infer<typeof User>; // static type derived from the runtime schema
async function fetchJson(url: string, ms = 5_000): Promise<unknown> {
const res = await fetch(url, { signal: AbortSignal.timeout(ms) }); // never hang forever
if (!res.ok) throw new Error(`${url}: HTTP ${res.status}`);
return res.json(); // `unknown`, not `any`: forces validation
}
// At most `limit` promises in flight; results keep input order.
export async function mapLimit<T, R>(items: T[], limit: number, fn: (t: T) => Promise<R>): Promise<R[]> {
const out = new Array<R>(items.length);
let next = 0; // safe without locks: single-threaded, no await between read and increment
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
while (next < items.length) {
const i = next++;
out[i] = await fn(items[i]!);
}
});
await Promise.all(workers);
return out;
}
export async function loadUsers(ids: number[]): Promise<User[]> {
const settled = await mapLimit(ids, 8, (id) =>
fetchJson(`https://api.example.com/users/${id}`)
.then((j) => ({ ok: true as const, user: User.parse(j) }))
.catch((err: unknown) => ({ ok: false as const, err })),
);
const users: User[] = [];
for (const r of settled) {
if (r.ok) users.push(r.user);
else console.error("skipped:", r.err);
}
return users;
}
process.on("unhandledRejection", (e) => { console.error(e); process.exit(1); }); // fail loudly
tsconfig.json for a Node service:
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"verbatimModuleSyntax": true,
"skipLibCheck": true,
"outDir": "dist"
}
}
Efficiency
- Cost: hot JIT-compiled loops run order of 2-5x slower than C; every number is a float64. A Cloudflare Workers isolate is created inside an already running runtime, which removes the virtual-machine cold start (Cloudflare docs). libuv's thread pool has 4 threads by default (libuv docs). V8's young-generation GC copies only survivors, so its cost scales with surviving objects, not allocations (V8 blog). Bound: one thread's CPU for compute, the event loop for latency.
- Levers:
- objects with one stable shape (same hidden class) — keeps inline caches monomorphic (V8 docs) — hot property accesses
- typed arrays instead of object arrays — contiguous unboxed memory (order of) — numeric buffers
worker_threads— parallel CPU work; they do not help I/O-bound work (Node.js docs) — CPU-bound handlersUV_THREADPOOL_SIZE— more concurrent fs/DNS/crypto jobs than the default 4 (libuv docs) — heavy file or hash workloads- Rust compiled to WASM or a native addon — escapes JIT and GC (order of) — CPU hot paths
- Naive → standard → state of the art:
- CPU work inline in the request handler · blocks the event loop for every client
worker_threadsor one process per core · parallel, structured-clone message cost- hot kernel in WASM or a native addon, shared via
SharedArrayBuffer· near-native, no copy
Build and deploy
- Type-check with
tsc --noEmitin CI; transpile with esbuild/swc/tsc, or run.tsdirectly (Bun, Deno, recent Node with type stripping for erasable syntax). - Lockfile +
npm ci(or pnpm); disable install scripts for dependencies where possible (see supply chain). - Containers:
node:22-slimor distroless Node,NODE_ENV=production, non-root, heap limit below the container limit. - Frontend: Vite for bundling and dev server; ship modern ES modules, code-split routes.
Pitfalls
- Blocking the event loop:
JSON.parseof huge payloads, sync crypto/fs, regex backtracking, big loops in a request handler. - Floating promises (no
await, no.catch): errors vanish or crash later; enable@typescript-eslint/no-floating-promises. anyandasassertions spreading through the codebase; types lie, runtime explodes.- Assuming
===on objects compares contents, orsort()sorts numbers (it sorts strings by default). - Money in floats; IDs above 2^53 parsed from JSON losing precision (use strings).
- Dependency sprawl: thousands of transitive packages; audit and prune.
- Memory leaks from closures captured by long-lived listeners or caches without bounds.
localStoragefor auth tokens (XSS-readable); prefer HttpOnly cookies.
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
|---|---|---|---|---|---|---|---|---|
| Node.js | C++ / JS | nodejs/node | brew install node | — | — | — | — | V8 + libuv; most used server runtime |
| TypeScript | TypeScript | microsoft/TypeScript | npm install typescript --save-dev | — | — | — | — | tsc, type checker |
| Deno | Rust | denoland/deno | curl -fsSL https://deno.land/install.sh | sh | — | — | — | — | runs TypeScript directly, permission model |
| Bun | Zig | oven-sh/bun | curl -fsSL https://bun.com/install | bash | — | — | — | — | runtime, package manager, bundler, test runner |
| Vite | TypeScript | vitejs/vite | npm create vite@latest | — | — | — | — | frontend dev server and bundler |
Sources
- Ecma International, ECMA-262: ECMAScript Language Specification. spec
- Microsoft, TypeScript Handbook. docs
- OpenJS Foundation, Node.js documentation: The Node.js Event Loop, Timers, and process.nextTick() docs · worker_threads docs
- V8 project, v8.dev blog (Ignition, TurboFan, Sparkplug, Maglev, Orinoco). blog · Trash talk: the Orinoco garbage collector blog · Maps (Hidden Classes) docs
- libuv, Thread pool work scheduling. docs
- Cloudflare, How Workers works. docs
- Meta, React Native. site · OpenJS Foundation, Electron. site