react-brain
Browse decisions
React & language foundations 3App architecture 11UI 13Platform & native 9Build, test, observe, secure 6AI in React apps 3stack composerdecision recordscensusstaleness benchnewsletterarchitecturechangelogmethodologyroadmap

entriesapp-architecture · verified 2026-10-08 · react

Desktop apps & web-to-native shells (packaging a React web app)

reviewedconfidence: mediumthis tier holds 71% on the public scorecard (7/20 graded · 0 overturned) →

related decisions: p2p · security · crossplatform

re-verified 9× — 2026-10-08 · 2026-09-24 · 2026-09-01 · 2026-08-18 · 2026-07-17 · 2026-07-16 +3 · changelog

recommendation

Tauri 2 for a lightweight desktop app (OS native webview, tiny builds). The pick changes on need: deep Node/native integration, the biggest plugin ecosystem, or an existing Electron app goes to Electron; an existing web URL to wrap goes to Pake; no real OS integration needed goes to a PWA with no shell; React Native code rather than a web app goes to react-native-windows / react-native-macos.

  • wrap an existing web URL fast → Pake (Tauri under the hood)
  • new lightweight desktop app, fine writing some Rust → Tauri 2
  • deep Node/native integration, biggest plugin ecosystem, or existing Electron app → Electron
  • P2P / serverless / local-first desktop → Pear (Holepunch; the backend architecture is RB-E-P2P, depth in holepunch-p2p-systems)
  • web-first team also shipping mobile → Capacitor
  • no real OS integration needed → PWA (skip the shell)
  • existing REACT NATIVE codebase extending to desktop → react-native-windows / react-native-macos (native UI, shares RN code; check rn-macos version lag)
  • Linux-only app that should look like a Linux app → GTKX (React over GTK4, no webview; anything cross-platform goes back to the shells above)

Options & tradeoffs

the field considered — and why each one isn’t the default here

optiontradeoffevidence
Electronbundles its own Chromium + Node; largest ecosystem & most mature; deep Node/native access; heaviest install (~100–200MB)9.6M/wk · ships in 2/20
Tauri (2.x)Rust shell using the OS native webview (no bundled Chromium) → tiny builds (from ~600KB); desktop + mobile (iOS/Android); native logic in Rust. @tauri-apps/cli 2.11.5 (2026-09-20, verified vs npm)
Pakeone-command wrapper that turns a web URL into a desktop app on Tauri; <10MB (~20x smaller than Electron); fast path to ship an existing site, not a full app framework. pake-cli 3.17.3 (2026-10-05, verified vs npm + the V3.17.3 release): native capabilities are now scoped to the exact packaged origin — package the URL a site finally lands on
Pear (Holepunch)P2P/serverless desktop+mobile runtime (Bare); no central server, content-addressed distribution; depth → holepunch-p2p-systems42/wk
Capacitor (Ionic)wraps a web app into iOS/Android (and desktop via Electron) native shells; web-first teams shipping to mobile
PWA (no shell)installable from the browser; least native integration but zero packaging — try this before reaching for a shell
Native SDK (Vercel Labs) — EXPERIMENTALan OWNED engine drawing every pixel (neither bundled like Electron nor borrowed like Tauri): markup + TypeScript/Zig compiled to native at build time; TEA-style, NOT React. 3.6MB/~100ms calculator (vendor numbers); mobile experimental. Not picked while experimental; see note: NATIVE SDK
GTKX (1.x)the LINUX-NATIVE React answer: a React 19 renderer over GTK4 (JSX creates GObject instances, no webview), run as an ordinary Node process; stock React, Linux ONLY by design. Needs Node ≥24 + GTK4/GLib dev packages. @gtkx/react 1.6.0 (2026-08-29, verified vs npm), moving fast — pin it; see note: GTKX
react-native-windows / react-native-macos (Microsoft)the RN-NATIVE desktop path — real native UI from RN code, no webview; for RN teams extending to desktop, not for packaging a web app. rn-windows 0.84.0 (2026-06-18, preview 0.85.0-preview.2) tracks core closely; rn-macos jumped 0.81→0.83.0 (2026-09-30, its first 0.83 release, syncing core RN 0.83.10) and lags core RN by one minor instead of three (verified vs npm + GitHub).

evidence: npm weekly downloads (signals snapshot) · “ships in n/D” = adoption across the production-app census, honest denominators

npm weekly downloads (from the corpus's last signals run): electron 9.6M · pear-runtime-react-native 42 · pear-runtime 2k · react-native-windows 120k · react-native-macos 114k

Verified notes

The durable selection axis is the renderer. Electron BUNDLES Chromium (large, uniform, maximum ecosystem); Tauri (2.x stable) BORROWS the OS native webview (tiny — from ~600KB — but rendering varies by platform webview) and adds iOS/Android; Pake and Pear also borrow. Three lanes skip the webview: react-native-windows/-macos render RN code natively, GTKX renders React to GTK4 on Linux, and Native SDK owns its renderer. The default is the lightest shell that grants the OS access you need. Tauri/Pake facts are verified vs tauri.app and the Pake repo; Electron/Capacitor/PWA rows are settled industry knowledge; the PWA-vs-Electron limits are sourced in the Zakharchenko reading (2026-10-08). PAKE (2026-09-24, verified vs npm and the Pake README): a one-command web-URL→desktop wrapper built ON Tauri, under 10MB and ~20x smaller than an Electron equivalent; pake-cli 3.17.1 (2026-09-20). → Good for shipping an existing site, not for a from-scratch app. 3.17.3 (2026-10-05, verified vs the V3.17.3 release + npm) grants notifications, downloads, zoom, window controls and open-in-browser only to local pages and the exact origin of the packaged URL, so a page that navigates to another site loses them — package the final landing URL (the release's example: https://mail.google.com, not https://gmail.com). RN DESKTOP (2026-07-10, verified vs npm + microsoft/react-native-windows releases; pins re-verified 2026-09-24): for teams whose CODE is React Native (not a web app), react-native-windows/-macos is the share-your-RN-code answer — a different axis than the bundled-vs-borrowed renderer. rn-windows is at 0.84.0, rn-macos at 0.81.9. GTKX (2026-09-01, verified vs the 1.0, 1.1 and 1.5 posts and npm): the only Linux-NATIVE React row; every other row reaches Linux through a webview or not at all. JSX creates GObject instances; the app is an ordinary Node process (node:fs, fetch, npm all work; GTK and JS share one thread); hooks, context, Suspense, portals and the React Compiler behave as usual. Linux only, deliberately — that narrowness lets it expose the whole toolkit instead of a cross-platform subset. Needs Linux, Node ≥24 and the GTK4/GLib dev packages; `npm create gtkx`. 1.0 shipped 2026-08-04. 1.1 (2026-08-11) adds `gtkx deploy`, which generates the desktop entry + AppStream metainfo and builds .flatpak/.deb/.rpm/.AppImage from a config block instead of the eleven hand-written packaging files 1.0 required. 1.5 (blog 2026-08-27, no breaking changes) adds @gtkx/forms (React Hook Form adapter binding native Adwaita rows — Entry/PasswordEntry/Switch/Spin/ComboRow — with typed values and native invalid styling) and @gtkx/i18n (gettext POT/MO pipeline behind the real react-i18next API, with generated TS declarations enforcing message keys). @gtkx/react on npm is 1.6.0 (2026-08-29) while the blog is at 1.5 → pin it. Same shape as react-native-windows — share React knowledge, not a web app — with the platform scope inverted: one OS, whole toolkit. NATIVE SDK (2026-07-17, verified vs native-sdk.dev/bridge + /platform-support; mobile status re-verified 2026-09-24): declarative markup + TypeScript/Zig compiled to native at BUILD time (no browser, no WebView, no JS engine in the binary); a TEA-style single update function, NOT React; 45+ built-in components and an embedded automation server for coding agents; desktop hosts run full apps, mobile is experimental. It ships a 16KiB-capped async-JSON WebView Bridge, and RN Rewind's critique stands quoted: async serialized JSON bridge + declarative views + native renderer ≈ "inventing React Native 0.59". → The owned-renderer lane is real but experimental; watch, don't bet. EXPO-DESKTOP LEAD (2026-07-16, TWiR #284): shirakaba/expo-desktop targets Expo → macOS/Windows directly — early and unproven, but a third RN-desktop lane (vs Microsoft's forks) to watch. CROSS-REFERENCES: RB-E-P2P owns the Pear backend architecture and the holepunch-p2p-systems skill owns its depth; RB-E-SECURITY owns shell security; RB-E-CROSSPLATFORM owns sharing code across web and native. HISTORY: 2026-10-08 REOPENED: Pake 3.17.3 scopes native capabilities to the packaged origin; PWA-vs-Electron reading added (first source for the PWA row's limits). 2026-10-01 REOPENED: react-native-macos jumped 0.81.9 → 0.83.0 (first 0.83 release, syncs core RN 0.83.10), narrowing its lag behind react-native-windows (0.84.0) from three minors to one. 2026-09-24 CORRECTED: Pake pin V3.12.0 (2026-06-21) → pake-cli 3.17.1 (2026-09-20). 2026-08-18 GAP CLOSED: GTKX promoted skip→option when 1.0 shipped (the reopen signal wired at TWiR #291), filling the Linux-native React hole. 2026-07-17 GAP CLOSED: Native SDK promoted lead→option on a second independent signal in a week (RN Rewind #49), resolving twir-290's too-early reopen condition. 2026-07-10 GAP CLOSED: RN-desktop rows added (React Weekly); the entry had been web-shell-only.

Canonical reading

Editorial annotations on why each piece matters — the articles themselves are the originals; read them there.

Electron to PWA: There and Back AgainArtem Zakharchenko (kettanaito)

Six months moving an EPUB editor (Byte Binding) from Electron to a PWA and back. The PWA won on size, startup and updates, but lost on desktop basics: file associations and launch parameters blocked by open WICG bugs, APIs that differ or are missing in Safari, an installed PWA always opening in the browser it came from, no custom tray/dock menus or global hotkeys, Cmd+S/Cmd+Q reserved by the browser, no 'Open with' entry, and a hard-to-find install icon. His verdict: a PWA is a good addition to a website, not a desktop-app platform.

The Conductor Rewrite: what they changed to make it fastDennis Brotzky (performance.dev)

A React + Tauri desktop performance postmortem — SQLite local-first, TanStack Router structural sharing (stable refs killing re-render cascades), virtualization — fixing root causes instead of papering over with useMemo. Directly relevant to React-on-desktop.

A Technical Deep Dive Into the New RaycastPetr Nikolaev & Thomas Paul Mann (Raycast)

How Raycast ships one React + TypeScript codebase to both macOS and Windows via native shells (Swift/C#) plus WebKit — IPC design, memory breakdown, and concrete WebKit workarounds. A durable model for cross-platform desktop on React without Electron.

Sources

Depth (in-domain rules) is owned by the engineering-principles skill — this entry is selection breadth.

The full explanation

The reviewed long-form essay behind this entry — the why, not a how-to. Also on GitHub.

About desktop apps & web-to-native shells for React

Diataxis: Explanation. This page builds understanding of the desktop-shell decision — the reasoning behind the pick. It is not a tutorial and not step-by-step how-to: the candidate list and one-line tradeoffs live in the index entry RB-E-DESKTOP; shell security depth lives in RB-E-SECURITY, and Pear (Holepunch) depth in the holepunch-p2p-systems skill. Read this to understand why; read those to do.

React already runs on the desktop — the question is the shell

A "React desktop app" is almost never a special build of React. It is your ordinary React-DOM app rendered inside a window that the operating system treats as a native application — its own icon, dock/taskbar presence, menus, file associations, and OS APIs. The React layer is unchanged. What you are actually choosing is the shell: the thin native host that opens a window, points a web renderer at your UI, and brokers access to the machine.

That reframing matters because it collapses most of the agonising. You are not picking a UI framework here (RB-E-STYLING, RB-E-COMPONENT-LIBS already did that); you are picking how much machinery to staple to a web app you mostly already have. The right default is therefore the lightest shell that grants the OS access you actually need — and often that is no shell at all (a PWA).

The one distinction that organises everything

Every desktop shell answers one question, and the answer drives size, consistency, and risk: does it bring its own browser, or borrow the operating system's?

  • Bundled renderer (Electron). Ships its own copy of Chromium + Node.js inside every app. Consequence: identical rendering on every OS and full Node/native power — at the cost of a large install (often 100 MB+) and the memory of a whole browser per app.
  • Borrowed renderer (Tauri, Pake, Pear, Wails…). Uses the OS native webview already on the machine — WebView2 (Chromium) on Windows, WKWebView (WebKit) on macOS, WebKitGTK (WebKit) on Linux. Consequence: tiny installs (Tauri builds start around ~600 KB) — at the cost that rendering now varies by platform webview, so the thing you must test is exactly what Electron buys you for free.

A second, orthogonal axis is distribution: most shells assume a central server or store to download from; Pear (Holepunch) instead distributes apps peer-to-peer and content-addressed, with no central host. For the heartit ecosystem that axis, not size, is the deciding one.

Hold those two axes — bundled vs borrowed renderer and centralised vs P2P distribution — and the option list stops being a grab-bag.

The default, and why

Lightweight desktop → Tauri 2 (native webview, tiny builds). Just wrapping an existing web URL → Pake. Need deep Node/native integration, the biggest plugin ecosystem, or already on it → Electron. P2P / serverless / local-first → Pear. And before any of these: if you don't need real OS integration, ship a PWA and skip the shell entirely.

The ordering is deliberate. Start by asking whether you need a shell at all; then prefer the borrowed-renderer path (small, modern) unless a concrete need — uniform rendering, a mature plugin, or heavy Node use — pulls you to Electron. The verified facts behind the picks: Tauri 2.x is stable, uses the OS webview, and spans desktop and mobile; Pake is a one-command web-URL→desktop wrapper built on Tauri, landing under 10 MB — roughly 20× smaller than the Electron equivalent (pake-cli 3.17.1, 2026-09-20). (Sources: tauri.app; the Pake repository; npm.)

The landscape, and when each one wins

Treat these as reasons to deviate from the default, not a menu to agonise over.

Electron — the incumbent. It wins on maturity and uniformity: one Chromium means your app renders the same on Windows, macOS, and Linux, and the plugin/native-module ecosystem is the largest by far. Choose it when you lean hard on Node in the main process, need a specific mature integration, or already have an Electron app that works. The price is size and per-app memory, and a security posture you must configure (below).

Tauri (2.x) — the modern lightweight default. Rust core + OS webview yields tiny, fast-starting apps and a locked-down-by-default security model (explicit capabilities rather than an open Node surface), plus a single codebase that also targets iOS/Android. The costs are rendering variance across OS webviews (you must test each) and writing native logic in Rust rather than Node. (Wails is the Go-based analog of the same borrowed-renderer idea, if your native code is Go.)

Pake — not really a framework but a preset over Tauri: point it at a URL and it emits a desktop app with an icon, dock presence, and shortcuts. It wins as the fastest path to ship an existing website as a desktop client, and as proof of how small the borrowed-renderer approach gets. It is the wrong tool the moment you need real app logic, deep OS integration, or offline behaviour beyond what the site already does — at that point you've outgrown the wrapper and want Tauri (or Electron) proper.

Pear (Holepunch) — a P2P application runtime (on the Bare JS runtime) that runs your JS/React-DOM app with no central server: apps are distributed peer-to-peer and content-addressed. It wins when serverless, local-first, or censorship-resistant distribution is the point — the heartit desktop apps live here. You're trading the familiar store/server deployment model for the P2P one; the depth of that model (identity, discovery, replication, update channels) is owned by the holepunch-p2p-systems skill.

Capacitor (Ionic) — the same borrowed-renderer idea aimed primarily at mobile (iOS/Android), with desktop reached via an Electron target. It wins for a web-first team that wants one wrapped web app across mobile and desktop and is comfortable in the Ionic toolchain. For desktop specifically, it's an indirection over Electron rather than a lighter option.

PWA (no shell) — the browser is the shell. Installable from the address bar, auto- updating, zero packaging or signing. It wins whenever you don't truly need deep OS access: it is the lightest possible "desktop app." Its ceiling is the browser sandbox — limited filesystem, no arbitrary native APIs, weaker offline/background story than a real shell. Try this first; reach for a shell only when you hit that ceiling.

Three lanes with no webview at all. If your code is React Native rather than a web app, the entry points to react-native-windows / react-native-macos — real native UI from RN code (rn-windows 0.84.0 tracks core closely; rn-macos 0.81.9 lags). For a Linux-only app that should look like a Linux app, GTKX renders stock React to GTK4 as an ordinary Node process — no webview, Linux only by design, moving fast enough to pin. Native SDK (Vercel Labs) owns its renderer and compiles markup + TypeScript/Zig to native, but it is not React and its mobile support is experimental, so the entry lists it without picking it.

Tradeoffs and failure modes to name out loud

  • Reaching for Electron by reflex. Shipping 150 MB and a full Chromium to host what is effectively a bookmark is the classic over-build. If you don't use Node or deep OS APIs, a PWA or Pake does the job at a fraction of the weight.
  • Assuming a borrowed renderer renders identically everywhere. Tauri/Pake/Pear use the OS webview, so a CSS/JS quirk can appear on Linux's WebKitGTK but not on Windows' Chromium-based WebView2. Uniform rendering is precisely the thing you gave up for the small size — budget cross-OS testing. (This cuts the other way too: it's the single best reason to stay on Electron.)
  • Forking your UI in the shell. The whole value is that desktop is just another target of the same React-DOM app (RB-E-CROSSPLATFORM). Keep the shell a thin host; if real product logic migrates into Electron's main process or Tauri's Rust side, you've grown a second app to maintain.
  • Trusting web content with native power. A desktop shell deliberately punches a hole from web content to the machine. Electron's historic footgun is leaving Node reachable from the renderer (mitigate with context isolation, sandboxing, and an explicit IPC surface); Tauri inverts the default with an opt-in capability allowlist. Either way the bridge is the attack surface — depth on this is owned by RB-E-SECURITY.

How it interacts with the rest of the stack

  • Cross-platform (RB-E-CROSSPLATFORM). Desktop is the web target wearing a native coat: the same shared logic/state/data package and the same React-DOM UI run inside the shell. This entry is "how do I package the web build as an app"; that entry is "how do I share code across web and native."
  • Meta-frameworks & build (RB-E-META-FRAMEWORKS, RB-E-BUILD). Whatever produces your web bundle feeds the shell. Note that RSC/SSR meta-frameworks assume a server, which a shipped desktop client may not have — a desktop shell usually wants a client-rendered build (SPA/static) it can load locally, which is also why Pake (pure URL wrap) is so cheap.
  • Security (RB-E-SECURITY). The renderer↔native bridge, IPC allowlists, and content-security posture of the shell are security concerns, not packaging trivia.
  • P2P runtime depth. Pear's identity, discovery, replication, and update model are owned by the holepunch-p2p-systems skill — this page only places Pear on the map.

In one paragraph

A React desktop app is your web app in a native window; the real choice is the shell, and it turns on one question — bundle a browser or borrow the OS webview. Electron bundles Chromium for uniform rendering and full Node power at ~100 MB+; Tauri 2 borrows the OS webview for tiny, modern, more-locked-down apps (test per-OS, write Rust for native bits); Pake is the one-command way to wrap an existing URL on Tauri; Pear swaps centralised distribution for P2P and is heartit's path; Capacitor suits web-first teams already shipping mobile; and a PWA is the lightest "desktop app" of all — start there and only add a shell when you hit the sandbox's ceiling. Keep the shell thin, test the borrowed renderer across platforms, and treat the web↔native bridge as an attack surface.


See also: RB-E-CROSSPLATFORM (desktop as another target of share-by-layer), RB-E-META-FRAMEWORKS / RB-E-BUILD (the web build that feeds the shell), RB-E-SECURITY (the renderer↔native bridge). Pear / P2P depth: the holepunch-p2p-systems skill.

Related in app-architecture: state · data · p2p · nav · meta-frameworks · forms · auth · networking · uploads · crossplatform