Split View: Deno 2.9 — deno desktop 실험, 락파일 그대로 마이그레이션, 그리고 24시간 공급망 기본값
Deno 2.9 — deno desktop 실험, 락파일 그대로 마이그레이션, 그리고 24시간 공급망 기본값
- 들어가며 — 6월 25일의 Deno 2.9, 3주간의 패치
- deno desktop — Electron의 자리에 들어온 실험
- 락파일 시드 — 이번 릴리스에서 가장 실무적인 기능
- Node 26 호환 — "호환 타깃"이라는 말의 정확한 의미
- 벤더 벤치마크 정확히 읽기
- 공급망 기본값 — 24시간 대기가 디폴트가 됐다
- 업그레이드할 때 걸리는 것들
- 그래서 갈아탈 것인가
- 마치며
- 참고 자료
들어가며 — 6월 25일의 Deno 2.9, 3주간의 패치
지난 5월 Deno 2 시대 정리 글에서 Deno의 방향 전환 — Node를 거부하던 런타임이 Node 호환을 1급 시민으로 받아들인 것 — 을 다뤘습니다. 이번 글은 그 후속입니다. Deno 2.9가 2026년 6월 25일에 나왔고, GitHub 릴리스 태그 기준으로 v2.9.0(6월 25일) 이후 v2.9.1(7월 1일), v2.9.2(7월 8일), v2.9.3(7월 15일)까지 매주 패치가 따라붙었습니다. 직전 마이너인 2.8.0이 5월 22일이었으니, 5주 간격의 릴리스 리듬입니다.
릴리스 노트의 헤드라인은 deno desktop이지만, 제 판단으로 실무자에게 더 중요한 건 두 가지 다른 변화입니다. 기존 npm·pnpm·yarn·Bun 락파일을 그대로 읽어 들이는 마이그레이션 경로, 그리고 공급망 방어 기능이 기본값으로 켜진 것. 하나씩 뜯어보고, 벤더가 낸 성능 숫자는 측정 조건과 빠진 것까지 같이 읽겠습니다. 참고로 타입스크립트 컴파일러 쪽 격변은 TS 7 GA와 네이티브 컴파일러 글에서 따로 다뤘습니다 — 이 글은 런타임 계층 이야기입니다.
deno desktop — Electron의 자리에 들어온 실험
deno desktop은 웹 스택으로 네이티브 데스크톱 앱을 만드는 새 서브커맨드입니다(PR #33441, 6월 16일 머지). UI는 웹뷰에서 돌고, 로직은 Deno에서 돌고, 결과물은 단일 배포 바이너리입니다. 가장 단순한 형태는 엔트리포인트가 UI를 서빙하는 것인데, 데스크톱 엔트리포인트 안의 Deno.serve()는 웹뷰가 여는 포트에 자동으로 바인딩되므로 포트 배선이 필요 없습니다.
// main.ts — 이게 전부입니다
Deno.serve(() =>
new Response(
"<!DOCTYPE html><h1>Hello from Deno desktop</h1>",
{ headers: { "content-type": "text/html" } },
)
);
deno desktop main.ts # 네이티브 창이 뜹니다
deno desktop # 현재 디렉토리의 웹 프레임워크 자동 감지
deno desktop --hmr # 개발 중 HMR
프레임워크 자동 감지는 deno compile과 같은 메커니즘을 쓰고, 공식 릴리스 노트 기준으로 Next.js, Astro, Fresh, Remix, Nuxt, SvelteKit, SolidStart, TanStack Start, Vite SSR을 지원합니다. 런타임에는 Deno.BrowserWindow(창 제어와 웹뷰-Deno 브리지), Deno.Tray(시스템 트레이), macOS용 Deno.Dock, 네이티브 다이얼로그로 렌더링되는 prompt()/alert()/confirm(), 바이너리 패치를 백그라운드로 적용하는 Deno.autoUpdate()가 들어왔습니다.
렌더링 엔진은 --backend로 고릅니다. 기본값 webview는 OS 내장 엔진 — Windows의 WebView2, macOS·리눅스의 WebKit — 을 쓰므로 바이너리가 작고 실행이 빠르지만, 렌더링이 호스트 OS의 엔진 버전에 좌우됩니다. --backend cef는 Chromium Embedded Framework로 Chromium을 통째로 번들해서 모든 플랫폼에서 동일한 렌더링을 보장하는 대신, 릴리스 노트 표현으로 수십 MB가 추가되고 빌드 시 다운로드가 필요합니다. 정확히 Tauri(웹뷰) 대 Electron(번들 엔진)의 트레이드오프를 한 도구 안에서 고르는 구조입니다.
배포 쪽이 흥미롭습니다. --output 확장자를 따라 macOS는 .app·.dmg, Windows는 .exe·.msi, 리눅스는 .AppImage·.deb·.rpm으로 나오고, MSI와 deb·rpm 인스톨러가 순수 Rust로 만들어지기 때문에 플랫폼별 패키징 툴체인 없이 리눅스 CI 한 대에서 --all-targets로 전 플랫폼 바이너리를 뽑을 수 있습니다. 지원 타깃은 deno compile과 같은 다섯 개 — 리눅스 x64/arm64, Windows x64, macOS x64/arm64 — 이고, Windows arm64는 아직 없습니다.
언제 쓰지 말아야 하나. 벤더 스스로 2.9에서는 experimental이며 표면이 아직 안정화 중이라고 못박고 있습니다. 프로덕션 데스크톱 앱을 실험 플래그 위에 올리는 건 그 자체로 답이 나온 결정이고, 오늘 출시할 물건이라면 Electron·Tauri라는 검증된 선택지가 그대로 있습니다. webview 백엔드를 고르면 "사용자 OS의 WebKit·WebView2 버전마다 렌더링이 다를 수 있다"는 Tauri 사용자들이 이미 아는 고통이 따라오고, cef를 고르면 Electron식 용량이 돌아옵니다. 이 실험의 진짜 의미는 지금 당장의 프로덕션 채택이 아니라, "런타임·컴파일러·패키징이 이미 한 몸인 도구가 데스크톱까지 먹으려 한다"는 방향 신호에 가깝습니다.
락파일 시드 — 이번 릴리스에서 가장 실무적인 기능
전환 비용의 가장 큰 덩어리는 언제나 "정성껏 고정해 둔 의존성 그래프를 잃는 것"입니다. 2.9는 이 지점을 직접 겨냥합니다. deno.lock이 없는 프로젝트에서 deno install을 실행하면 package-lock.json, pnpm-lock.yaml, yarn.lock, bun.lock을 읽어 정확히 같은 해석 버전과 무결성 해시로 deno.lock을 시드합니다(bun.lock 지원 PR #35394). 재해석이 없으므로 몰래 올라가는 버전도 없다는 게 공식 설명입니다. 시드는 한 방향입니다 — 외부 락파일을 읽어 deno.lock을 만들고, 이후 관리는 deno.lock이 맡습니다. 되쓰기(write-back)는 하지 않습니다.
$ deno install
Seeded deno.lock from package-lock.json
pnpm 모노레포도 챙겼습니다. npm·yarn·Bun이 쓰는 package.json의 workspaces 필드는 원래 읽었지만 pnpm만 별도 파일을 쓰는데, 이제 pnpm-workspace.yaml을 발견하면 packages·catalog·catalogs 설정을 기존 필드를 건드리지 않고 이관해 줍니다(#34993). 그리고 진짜 node 바이너리가 설치돼 있지 않은 환경에서는 PATH에 스탠드인 node를 놓아 자신에게 포워딩하고 Node CLI 인자를 번역합니다(#34969) — Turbopack 워커 풀처럼 node를 직접 호출하는 도구를 위한 조치이고, 진짜 node가 있으면 절대 가리지 않으며 DENO_DISABLE_NODE_SHIM=1로 끌 수 있습니다.
이 묶음의 의미는 분명합니다. "Deno로 갈아타기"가 의존성 그래프 재구축이라는 도박이 아니라, 기존 락파일을 유지한 채 명령 두 개로 시험해 보고 아니면 돌아올 수 있는 가역적 실험이 됐다는 것. 다만 락파일 형식 호환의 엣지 케이스 — pnpm이 의존성 키에 peer를 인라인 인코딩하는 방식 같은 — 는 릴리스 직전까지 후속 PR로 다듬어지고 있었으므로, 복잡한 pnpm 모노레포라면 시드 결과의 버전들이 원본과 일치하는지 확인하고 넘어가는 게 안전합니다.
Node 26 호환 — "호환 타깃"이라는 말의 정확한 의미
2.9는 Node.js 호환 타깃을 Node 26으로 올렸습니다. process.version이 v26.3.0을 보고하고(#34747), Deno가 돌리는 node-compat 테스트 스위트도 26.3.0으로 올라갔습니다(#34746). 맥락을 붙이면 — endoflife.date 기준 Node 26은 2026년 5월 5일에 나온 Current 라인이고 LTS 승격은 2026년 10월 28일 예정이며, 실제 최신은 7월 8일의 26.5.0입니다. 즉 호환 타깃은 실제 Node보다 패치 두 개 뒤를 따라갑니다.
실질적인 개선도 몇 가지 있습니다. import "fs" 같은 맨(bare) 내장 모듈 지정자가 플래그 없이 무조건 node:fs로 해석되고, node_modules의 패키지가 내장 모듈을 가리던 버그도 Node와 같은 규칙(내장이 항상 승리)으로 고쳐졌습니다. Node-API는 버전 10을 보고하고, node:test에 mock.module()·mock.timers·파일 스냅샷이 붙었으며, process.resourceUsage() 같은 빠져 있던 API가 채워졌습니다.
정직하게 짚을 부분: "호환 타깃 26"은 Deno가 자신이 돌리는 호환 테스트 스위트의 기준 버전을 26.3.0으로 맞췄다는 뜻이지, Node 26 생태계가 100% 돈다는 인증이 아닙니다. 네이티브 애드온이나 V8 내부에 기대는 도구, Node 내부 구현 디테일에 의존하는 패키지는 여전히 개별 확인 대상입니다. 그 경계가 어디인지는 릴리스 노트가 말해 주지 않으므로, 후보 프로젝트에서 직접 테스트 스위트를 돌려 보는 것 외의 지름길은 없습니다.
벤더 벤치마크 정확히 읽기
릴리스 노트의 성능 숫자입니다. 전부 벤더 자체 측정이고, 조건은 이렇게 명시돼 있습니다 — 전용 x86_64 리눅스 박스 한 대, 서버와 부하 생성기를 서로 겹치지 않는 코어에 고정, 동시성 100, oha 3회 실행의 중앙값, 콜드 스타트는 hyperfine 150회 실행의 평균, 비교 대상은 Deno 2.8.0.
| 항목 | 2.8 | 2.9 | 변화 |
|---|---|---|---|
| 콜드 스타트 (hello world) | 34.2 ms | 17.3 ms | 1.98배 빠름 |
| Deno.serve "realworld" (JSON POST + Bearer 헤더 에코) | 56.8k req/s | 72.4k req/s | 1.27배 |
| Deno.serve plaintext | 77.0k req/s | 85.6k req/s | 1.11배 |
| Deno.serve 1 MiB 응답 | 1,617 req/s | 1,907 req/s | 1.18배 |
| 피크 RSS, realworld | 142 MB | 64 MB | 2.2배 감소 |
| 피크 RSS, 1 MiB 응답 | 197 MB | 63 MB | 3.1배 감소 |
배율은 원 수치와 산술적으로 맞아떨어집니다(직접 나눠 확인했습니다). 개선의 출처도 릴리스 노트가 밝히고 있습니다 — 스냅샷에서 node: 글로벌을 지연 로딩으로 빼고, Node 부트스트랩을 Node 워커로 한정하고, 지연 로딩되는 ESM에 V8 코드 캐시를 붙이고, 스냅샷을 축소한 것이 콜드 스타트의 절반을 만들었고, HTTP 처리량은 Deno 소유의 HTTP/1.1 서빙 경로(#34446)가, RSS 평탄화와 함께 crypto.subtle과 console의 JS에서 Rust로의 포팅이 나머지를 만들었습니다.
이 숫자들이 말하지 않는 것도 그만큼 분명합니다.
- 비교 대상이 자기 자신입니다. 2.8 대비 2.9이지, Node나 Bun과의 비교가 아닙니다. "1.27배"는 갈아탈 이유가 아니라 업그레이드할 이유입니다.
- 루프백 트래픽입니다. 같은 박스에서 코어만 나눠 쏜 부하입니다. 1 MiB 워크로드의 1,907 req/s는 대역폭으로 약 16 Gbps에 해당하는데, 이건 NIC를 거치지 않는 루프백이라 가능한 수치입니다. 실제 네트워크·TLS 종단이 끼면 그림이 달라집니다.
- HTTP/1.1 평문 경로입니다. 새 서빙 경로 자체가 HTTP/1.1 대상이고, TLS나 HTTP/2 수치는 제시되지 않았습니다.
- 처리량과 RSS뿐입니다. 지연 분포 — p99 같은 꼬리 지연 — 는 없습니다. 3회 중앙값이라 분산 정보도 없습니다.
- 에코 워크로드입니다. "realworld"라는 이름이 붙었지만 DB도 디스크도 없는 JSON 에코입니다. 실제 앱의 병목은 대개 런타임 바깥에 있습니다.
그 위에서 하나만 고르라면 저는 처리량이 아니라 메모리를 보겠습니다. 2.8에서는 RSS가 워크로드에 따라 약 94 MB에서 197 MB까지 자랐는데, 2.9는 측정된 워크로드들에서 62 MB 부근으로 평탄하게 유지됩니다. 같은 머신에 더 많은 인스턴스를 올릴 수 있다는 뜻이라, 컨테이너 밀도로 비용을 계산하는 팀에게는 처리량 1.1~1.3배보다 이쪽이 실질적인 변화입니다 — 단, 해당 워크로드들에서의 결과라는 단서는 그대로 둡니다.
공급망 기본값 — 24시간 대기가 디폴트가 됐다
2.9에서 조용히 가장 큰 정책 변화는 이것입니다. 2.6에서 도입됐던 min-release-age — 게시된 지 일정 시간이 지나지 않은 npm 패키지 버전의 설치를 거부하는 기능 — 가 24시간 창으로 기본 활성화됐습니다(#35458, 릴리스 당일 머지). 근거는 단순합니다. npm 공급망 공격의 상당수는 악성 버전이 게시되고 하루 이틀 안에 탐지·게시 철회되므로, 기다리는 것만으로 큰 부류의 공격이 걸러집니다. 지난주 axios 사건 정리에서 본 것처럼 provenance가 있어도 뚫리는 구멍이 있는 만큼, 시간 지연은 단순하지만 실효적인 방어층입니다.
# .npmrc — 명시 값이 기본값보다 우선합니다
min-release-age=72h # 3일로 늘리기
min-release-age=0 # 완전히 끄기
운영에서 걸릴 수 있는 지점을 짚어 두면 — 자사 패키지를 npm에 게시하고 곧바로 설치하는 파이프라인은 이제 기본 설정에서 24시간 동안 실패합니다. 핫픽스를 게시 즉시 배포에 태우는 워크플로라면 .npmrc에서 값을 조정해야 하고, 이 트레이드오프(신선한 패치 지연 vs 공격 창 차단)는 각자 계산할 몫입니다. npm 패키지에 적용되는 기능이라는 점도 명확히 해 둡니다.
여기에 옵트인 방어가 하나 더 얹혔습니다. trust-policy=no-downgrade는 pnpm의 설계를 따라 각 버전의 게시 방식 — 2FA 라이브 승인이 붙는 staged publishing, provenance 증명이 붙는 trusted publishing, provenance 단독 — 의 신뢰 순위를 매기고, 이전 버전들보다 약한 증거로 게시된 새 버전의 해석을 하드 에러로 만듭니다. 유지보수자 토큰 탈취 공격(2025년 8월의 s1ngularity 사건이 릴리스 노트에 예시로 등장합니다)의 전형적 흔적인 "provenance로 잘 게시되던 패키지가 갑자기 평범한 토큰 게시로 나오는" 상황을 잡는 장치입니다. 레지스트리 전반의 provenance 채택이 아직 고르지 않아 기본값은 꺼져 있다는 설명까지, 릴리스 노트가 스스로 한계를 밝히는 부분은 그대로 옮겨 둡니다.
업그레이드할 때 걸리는 것들
동작 변경이 하나 있습니다. Deno.serve의 자동 응답 압축이 기본 비활성으로 바뀌었습니다. 기존 버전의 자동 압축에 기대어 대역폭을 아끼고 있었다면, 2.9로 올리는 순간 응답이 비압축으로 나갑니다 — 서버별로 automaticCompression: true를 주거나 DENO_SERVE_AUTOMATIC_COMPRESSION=1 환경 변수로 프로세스 전체에 되살릴 수 있습니다. CDN 뒤라면 체감이 없겠지만 직접 서빙 중이라면 업그레이드 체크리스트에 올려 둘 항목입니다.
나머지는 실험 딱지가 붙은 채로 들어왔습니다. CSS 모듈 임포트(with { type: "css" }로 CSSStyleSheet를 받는 웹 표준 방식)는 --unstable-raw-imports 플래그 뒤에 있고, deno compile --bundle — 트리셰이킹 후 임베드해서 바이너리를 줄이는 — 은 실험 플래그이며 벤더 자체 측정으로 lodash hello world가 11.6 MB에서 1.5 MB로 줄었다는 수치가 제시돼 있습니다(당연히 npm 의존성이 무거운 프로젝트일수록 효과가 크고, 일반화는 각자의 빌드에서 확인할 일입니다).
테스트 러너는 눈에 띄게 두꺼워졌습니다. t.assertSnapshot() 내장 스냅샷, Deno.test.each 파라미터라이즈, --shard CI 분할, 변경된 파일에 영향받는 테스트만 고르는 --changed, 플래키 대응 --retry와 커버리지 임계값까지 — Vitest나 Jest를 부르던 이유를 하나씩 지우는 방향입니다. 그 밖에 deno task의 입력 기반 캐싱, npm ls에 해당하는 deno list, 그리고 ML-KEM·ML-DSA·SLH-DSA 등 NIST 포스트퀀텀 알고리즘의 Web Crypto 구현( OpenSSH 10.4의 ML-DSA 이야기와 같은 흐름의 런타임 버전)도 이번 릴리스에 들어 있습니다.
그래서 갈아탈 것인가
정리해 보겠습니다.
시험해 볼 이유가 생긴 경우
- 기존 Node 프로젝트인데 락파일을 잃지 않고 가역적으로 실험할 수 있게 됐습니다 — 전환 비용의 구조가 바뀐 게 이번 릴리스의 핵심입니다.
- 포매터·린터·테스트 러너·태스크 러너·컴파일러를 하나로 줄이고 싶은 팀 — 2.9의 테스트 러너 보강으로 외부 도구 의존이 실제로 줄었습니다.
- npm 공급망 방어를 도구 기본값으로 받고 싶은 조직 — 24시간 대기 기본화는 현재 주요 런타임 중 가장 공격적인 기본값입니다.
아직 아닌 경우
- 네이티브 애드온·Node 내부 API에 깊이 기댄 코드베이스 — "호환 타깃 26"이 보장하지 않는 영역입니다.
- 프로덕션 데스크톱 앱 — deno desktop은 실험이고, Electron·Tauri를 대체할 단계가 아닙니다.
- Bun 특화 API(
bun:sqlite,Bun.serve등) 위에 선 코드 — 락파일은 읽어 주지만 API는 번역해 주지 않습니다. - 이미 Node 26에서 문제없이 돌고 전환으로 얻을 구체적 항목을 못 대는 팀 — 벤치마크 표가 보여 주듯 2.9의 숫자는 Node 대비 우위 주장이 아닙니다.
참고로 경쟁 런타임 쪽 상황을 한 줄 남기면 — Bun의 GitHub 릴리스 기준 마지막 안정판은 5월 13일의 v1.3.14로 두 달째 새 안정 릴리스가 없지만, 저장소 커밋은 오늘도 이어지고 있어 정체로 읽는 건 무리입니다. 다만 매주 패치를 내보내는 Deno와의 릴리스 리듬 차이는 기록해 둘 만한 사실입니다. Bun 쪽 상태 평가는 5월의 Bun 회고를 참고하십시오.
마치며
Deno 2.9를 관통하는 논리는 "전환 비용을 구조적으로 낮추기"입니다. 락파일을 그대로 읽고, node 바이너리 자리를 메꾸고, pnpm 워크스페이스 설정을 이관하고, Node 26 호환 타깃을 따라가는 것 — 전부 같은 방향입니다. 2년 전 Node 호환을 받아들인 자기 부정이, 이제 경쟁자의 락파일 포맷까지 읽는 단계에 왔습니다. 그 위에 얹힌 deno desktop은 아직 실험이지만 도구 통합이라는 같은 논리의 연장이고, min-release-age 기본화는 런타임이 공급망 방어의 기본값을 정하는 시대가 왔다는 신호입니다.
벤더 숫자는 조건과 함께 읽으면 유용했습니다 — 루프백 마이크로벤치마크라는 한계 안에서, 콜드 스타트 절반과 RSS 평탄화는 메커니즘 설명까지 붙은 믿을 만한 개선입니다. 다만 그 표에 Node와 Bun이 없다는 사실이 이 릴리스의 성격을 정확히 말해 줍니다. 2.9는 "우리가 더 빠르다"가 아니라 "옮겨 오는 비용을 줄였다"는 릴리스이고, 그 주장만큼은 숫자가 아니라 기능 목록이 증명하고 있습니다.
참고 자료
- Deno 2.9 공식 릴리스 노트
- denoland/deno GitHub 릴리스 (v2.9.0~v2.9.3 날짜 확인)
- PR #33441 — feat: deno desktop subcommand
- PR #35458 — feat: enable default minimum dependency age
- PR #34746 — node-compat 테스트 스위트 26.3.0 상향 · PR #34747 — process.version v26.3.0
- PR #34446 — Deno 소유 HTTP/1.1 서빙 경로
- Node.js 릴리스 일정 — endoflife.date
- Deno 2 시대 정리 — Node 호환의 시대(관련 글)
- axios npm 침해 사건 — provenance가 놓친 것(관련 글)
Deno 2.9 — The deno desktop Experiment, Lockfile-Preserving Migration, and a 24-Hour Supply-Chain Default
- Introduction — Deno 2.9 on June 25, Three Weeks of Patches
- deno desktop — An Experiment Stepping Into Electron's Spot
- Lockfile Seeding — The Most Practical Feature in This Release
- Node 26 Compatibility — What "Compatibility Target" Precisely Means
- Reading the Vendor Benchmarks Precisely
- The Supply-Chain Default — A 24-Hour Wait Is Now the Default
- What Bites You on Upgrade
- So, Should You Switch?
- Closing
- References
Introduction — Deno 2.9 on June 25, Three Weeks of Patches
Back in May, the Deno 2 era retrospective covered Deno's pivot — a runtime that once rejected Node embracing Node compatibility as a first-class citizen. This post is the follow-up. Deno 2.9 shipped on June 25, 2026, and going by GitHub release tags, weekly patches followed: v2.9.0 (June 25), then v2.9.1 (July 1), v2.9.2 (July 8), and v2.9.3 (July 15). The prior minor, 2.8.0, landed May 22 — a five-week release cadence.
The release-notes headline is deno desktop, but in my judgment two other changes matter more to practitioners: a migration path that reads existing npm, pnpm, yarn, and Bun lockfiles as-is, and a supply-chain defense that's now on by default. Let's take them one at a time, and read the vendor's performance numbers alongside their measurement conditions and what's left out. As a side note, the upheaval on the TypeScript compiler side is covered separately in TS 7 GA and the Native Compiler — this post stays at the runtime layer.
deno desktop — An Experiment Stepping Into Electron's Spot
deno desktop is a new subcommand for building native desktop apps with the web stack (PR #33441, merged June 16). The UI runs in a webview, the logic runs in Deno, and the output is a single deployable binary. In its simplest form, the entrypoint just serves the UI — Deno.serve() inside a desktop entrypoint auto-binds to the port the webview opens, so there's no port wiring to do.
// main.ts — that's the whole thing
Deno.serve(() =>
new Response(
"<!DOCTYPE html><h1>Hello from Deno desktop</h1>",
{ headers: { "content-type": "text/html" } },
)
);
deno desktop main.ts # a native window opens
deno desktop # auto-detects the web framework in the current directory
deno desktop --hmr # HMR during development
Framework auto-detection uses the same mechanism as deno compile, and per the official release notes it supports Next.js, Astro, Fresh, Remix, Nuxt, SvelteKit, SolidStart, TanStack Start, and Vite SSR. New runtime APIs include Deno.BrowserWindow (window control and the webview-Deno bridge), Deno.Tray (system tray), Deno.Dock for macOS, prompt()/alert()/confirm() rendered as native dialogs, and Deno.autoUpdate() for applying binary patches in the background.
The rendering engine is chosen with --backend. The default, webview, uses the OS's built-in engine — WebView2 on Windows, WebKit on macOS and Linux — which keeps the binary small and startup fast, but ties rendering to whatever engine version the host OS ships. --backend cef bundles the whole Chromium via the Chromium Embedded Framework to guarantee identical rendering across platforms, at the cost of — in the release notes' own words — tens of megabytes added and a download required at build time. It's exactly the Tauri-(webview) versus Electron-(bundled-engine) tradeoff, offered as a single choice inside one tool.
The deployment side is interesting. Following the --output extension, macOS produces .app/.dmg, Windows produces .exe/.msi, and Linux produces .AppImage/.deb/.rpm, and because the MSI and the deb/rpm installers are built in pure Rust, you can pull binaries for every platform from a single Linux CI box with --all-targets, no platform-specific packaging toolchain required. Supported targets are the same five as deno compile — Linux x64/arm64, Windows x64, macOS x64/arm64 — and Windows arm64 isn't there yet.
When not to use it. The vendor itself is explicit that this is experimental in 2.9 and the surface is still stabilizing. Putting a production desktop app on top of an experimental flag is a decision that already answers itself, and if you're shipping something today, the proven options — Electron, Tauri — are right there. Pick the webview backend and you inherit a pain Tauri users already know: "rendering can differ by the user's OS WebKit/WebView2 version." Pick cef and Electron-scale footprint comes back. The real significance of this experiment isn't production adoption today — it's a directional signal that a tool where runtime, compiler, and packaging are already one body is now reaching for the desktop too.
Lockfile Seeding — The Most Practical Feature in This Release
The biggest chunk of switching cost is always "losing a dependency graph you carefully pinned." 2.9 aims directly at that. Run deno install in a project with no deno.lock, and it reads package-lock.json, pnpm-lock.yaml, yarn.lock, or bun.lock to seed deno.lock with exactly the same resolved versions and integrity hashes (bun.lock support in PR #35394). No re-resolution means no version silently drifting up, per the official explanation. Seeding is one-directional — it reads the external lockfile to produce deno.lock, and deno.lock handles everything after that. There's no write-back.
$ deno install
Seeded deno.lock from package-lock.json
pnpm monorepos are covered too. Deno already read the workspaces field in package.json that npm, yarn, and Bun use, but pnpm alone uses a separate file — now, when it finds pnpm-workspace.yaml, it migrates the packages/catalog/catalogs settings over without touching the existing fields (#34993). And in environments without a real node binary installed, it places a stand-in node on the PATH that forwards to itself and translates Node CLI arguments (#34969) — a fix for tools like Turbopack's worker pool that call node directly; it never shadows a real node if one exists, and it can be turned off with DENO_DISABLE_NODE_SHIM=1.
The point of this bundle of changes is clear: "switching to Deno" is no longer a gamble on rebuilding your dependency graph — it's become a reversible experiment you can try with two commands while keeping your existing lockfile, and back out of if it doesn't work. That said, edge cases in lockfile-format compatibility — like how pnpm inline-encodes peers into dependency keys — were still being polished in follow-up PRs right up to release, so if you're running a complex pnpm monorepo, it's worth checking that the seeded versions actually match the originals before moving on.
Node 26 Compatibility — What "Compatibility Target" Precisely Means
2.9 raises the Node.js compatibility target to Node 26. process.version now reports v26.3.0 (#34747), and the node-compat test suite Deno runs was also bumped to 26.3.0 (#34746). For context — per endoflife.date, Node 26 is the Current line released May 5, 2026, with LTS promotion slated for October 28, 2026, and the actual latest is 26.5.0 from July 8. In other words, the compatibility target trails the real Node by two patch releases.
There are some substantive improvements too. Bare built-in module specifiers like import "fs" now unconditionally resolve to node:fs without a flag, and the bug where a package in node_modules could shadow a built-in module was fixed to match Node's own rule (built-ins always win). Node-API now reports version 10, node:test gained mock.module(), mock.timers, and file snapshots, and previously-missing APIs like process.resourceUsage() were filled in.
Here's the honest caveat: "compatibility target 26" means Deno aligned the baseline version of the compat test suite it runs against to 26.3.0 — it is not a certification that the Node 26 ecosystem works 100%. Native addons, tools that reach into V8 internals, or packages that depend on Node's internal implementation details are still something you have to check individually. The release notes don't say where that boundary sits, so there's no shortcut beyond running your candidate project's own test suite.
Reading the Vendor Benchmarks Precisely
Now the performance numbers from the release notes. All of it is vendor-measured, and the conditions are stated as follows: a single dedicated x86_64 Linux box, server and load generator pinned to non-overlapping cores, concurrency of 100, the median of 3 oha runs, cold start as the mean of 150 hyperfine runs, compared against Deno 2.8.0.
| Metric | 2.8 | 2.9 | Change |
|---|---|---|---|
| Cold start (hello world) | 34.2 ms | 17.3 ms | 1.98x faster |
| Deno.serve "realworld" (JSON POST + Bearer header echo) | 56.8k req/s | 72.4k req/s | 1.27x |
| Deno.serve plaintext | 77.0k req/s | 85.6k req/s | 1.11x |
| Deno.serve 1 MiB response | 1,617 req/s | 1,907 req/s | 1.18x |
| Peak RSS, realworld | 142 MB | 64 MB | 2.2x reduction |
| Peak RSS, 1 MiB response | 197 MB | 63 MB | 3.1x reduction |
The multipliers check out arithmetically against the raw numbers (I divided them out myself to confirm). The release notes also state where the improvement comes from — lazy-loading node: globals out of the snapshot, confining Node bootstrap to Node workers, attaching a V8 code cache to lazily-loaded ESM, and shrinking the snapshot account for half of the cold-start gain; HTTP throughput comes from Deno's own HTTP/1.1 serving path (#34446), with the rest coming from porting crypto.subtle and console from JS to Rust alongside the RSS flattening.
What these numbers don't say is just as clear.
- The comparison is against itself. It's 2.9 versus 2.8, not versus Node or Bun. "1.27x" is a reason to upgrade, not a reason to switch.
- This is loopback traffic. Load was fired from the same box, just split across cores. The 1,907 req/s on the 1 MiB workload works out to roughly 16 Gbps of bandwidth — a figure only possible because it never touches a NIC, being loopback. Once a real network or TLS termination enters the picture, the picture changes.
- This is the HTTP/1.1 plaintext path. The new serving path itself targets HTTP/1.1, and no TLS or HTTP/2 figures are given.
- It's throughput and RSS only. There's no latency distribution — no p99 tail latency. And with only 3 runs and a median, there's no variance information either.
- It's an echo workload. It's labeled "realworld," but it's a JSON echo with no DB and no disk. Real apps usually bottleneck somewhere outside the runtime.
If I had to pick just one thing off that table, I'd watch memory, not throughput. On 2.8, RSS ranged roughly from 94 MB to 197 MB depending on workload; on 2.9, it stays flat around 62 MB across the measured workloads. That means you can pack more instances onto the same machine, and for teams that price things by container density, that matters more in practice than the 1.1–1.3x throughput gain — with the same caveat attached: this is what happened on those specific workloads.
The Supply-Chain Default — A 24-Hour Wait Is Now the Default
The quietly biggest policy change in 2.9 is this: min-release-age — introduced back in 2.6, a feature that refuses to install npm package versions that haven't been published for long enough — is now enabled by default with a 24-hour window (#35458, merged on release day). The rationale is simple. A large share of npm supply-chain attacks get detected and have the malicious version unpublished within a day or two of publication, so waiting alone filters out a big class of attacks. As we saw in last week's axios incident writeup, even provenance leaves gaps that get exploited — so a time delay is a simple but genuinely effective extra layer of defense.
# .npmrc — an explicit value overrides the default
min-release-age=72h # extend it to three days
min-release-age=0 # turn it off entirely
Worth flagging where this can bite in practice — a pipeline that publishes your own package to npm and installs it right after now fails by default for 24 hours. If your workflow ships a hotfix to production the moment it's published, you'll need to adjust the value in .npmrc, and that tradeoff (delaying fresh patches versus blocking the attack window) is one each team has to weigh for itself. Worth being explicit too: this applies to npm packages.
There's one more opt-in defense layered on top. trust-policy=no-downgrade, following pnpm's design, ranks the trust level of how each version was published — staged publishing with live 2FA approval, trusted publishing with a provenance attestation, or provenance alone — and turns resolution of a new version published with weaker evidence than prior versions into a hard error. It's built to catch a classic sign of a maintainer-token-theft attack (the release notes cite the August 2025 s1ngularity incident as an example): a package that was reliably published with provenance suddenly showing up published with a plain token instead. And true to form, the release notes disclose their own limitation here too — the default is off, because provenance adoption across the registry is still uneven — and that caveat is carried over as-is.
What Bites You on Upgrade
There's one behavior change. Deno.serve's automatic response compression is now disabled by default. If you were relying on the old version's automatic compression to save bandwidth, responses go out uncompressed the moment you move to 2.9 — you can bring it back per-server with automaticCompression: true, or process-wide with the DENO_SERVE_AUTOMATIC_COMPRESSION=1 environment variable. Behind a CDN you probably won't notice, but if you're serving directly, put this on your upgrade checklist.
Everything else arrived carrying the experimental label. CSS module imports (the web-standard way of getting a CSSStyleSheet via with { type: "css" }) sit behind the --unstable-raw-imports flag, and deno compile --bundle — which tree-shakes then embeds to shrink the binary — is an experimental flag with a vendor-measured figure showing a lodash hello-world binary shrinking from 11.6 MB to 1.5 MB (naturally the effect scales with how heavy your npm dependencies are, and generalizing it is something to confirm on your own build).
The test runner got noticeably heavier. Built-in snapshots via t.assertSnapshot(), parameterized tests with Deno.test.each, CI splitting with --shard, --changed to select only tests affected by changed files, --retry for flaky tests, and coverage thresholds — one by one, these erase the reasons you used to reach for Vitest or Jest. Beyond that, this release also brings input-based caching for deno task, deno list (an npm-ls equivalent), and Web Crypto implementations of NIST post-quantum algorithms — ML-KEM, ML-DSA, SLH-DSA (a runtime-side version of the same thread covered in the ML-DSA story in OpenSSH 10.4).
So, Should You Switch?
Let's sum up.
When it's worth trying
- You're on an existing Node project and can now experiment reversibly without losing your lockfile — a structural change in switching cost is the core of this release.
- You're a team wanting to consolidate formatter, linter, test runner, task runner, and compiler into one — 2.9's beefed-up test runner has genuinely cut external tool dependence.
- You're an organization that wants npm supply-chain defense as a tool default — defaulting to a 24-hour wait is the most aggressive default among the major runtimes right now.
Not yet
- A codebase that leans heavily on native addons or Node internal APIs — "compatibility target 26" doesn't cover that ground.
- A production desktop app — deno desktop is experimental, not yet at the stage of replacing Electron or Tauri.
- Code built on Bun-specific APIs (
bun:sqlite,Bun.serve, etc.) — the lockfile gets read, but the API doesn't get translated. - A team already running fine on Node 26 with no concrete item to point to as the gain from switching — as the benchmark table shows, 2.9's numbers aren't a claim of superiority over Node.
For context, a one-line note on the competing runtime: per Bun's GitHub releases, the last stable was v1.3.14 on May 13 — two months without a new stable release — but commits to the repo continue today, so calling it stalled would be a stretch. Still, the difference in release cadence against Deno's weekly patches is worth noting for the record. For an assessment of where Bun stands, see May's Bun retrospective.
Closing
The logic running through Deno 2.9 is "structurally lower the cost of switching." Reading lockfiles as-is, filling the node binary's spot, migrating pnpm workspace settings, keeping pace with the Node 26 compatibility target — it all points the same direction. The self-negation of embracing Node compatibility two years ago has now reached the stage of reading a competitor's own lockfile formats. Layered on top, deno desktop is still experimental, but it's an extension of the same logic of tool consolidation, and defaulting min-release-age on is a signal that we've reached an era where the runtime itself sets the default for supply-chain defense.
The vendor numbers were useful once read alongside their conditions — within the limits of a loopback microbenchmark, the halved cold start and the flattened RSS are credible improvements, backed by an explanation of the mechanism. But the fact that Node and Bun don't appear anywhere on that table tells you exactly what kind of release this is. 2.9 isn't a "we're faster" release — it's a "we lowered the cost of moving over" release, and that claim is proven not by the numbers but by the feature list.
References
- Deno 2.9 official release notes
- denoland/deno GitHub releases (dates for v2.9.0–v2.9.3)
- PR #33441 — feat: deno desktop subcommand
- PR #35458 — feat: enable default minimum dependency age
- PR #34746 — bump node-compat test suite to 26.3.0 · PR #34747 — process.version v26.3.0
- PR #34446 — Deno-owned HTTP/1.1 serving path
- Node.js release schedule — endoflife.date
- The Deno 2 era retrospective — the Node-compatibility era (related post)
- The axios npm compromise — what provenance missed (related post)