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

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

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

reviewedconfidence: mediumthis tier holds 50% on the public scorecard (3/19 graded · 0 overturned) →

related decisions: p2p

re-verified 3× — 2026-07-13 · 2026-07-10 · 2026-07-06 · changelog

recommendation

Lightweight desktop → Tauri 2 (native webview, tiny builds). Need maximum ecosystem / deep Node + native integration, or already on it → Electron. Just wrapping an existing web URL → Pake.

  • 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 → holepunch-p2p-systems
  • web-first team also shipping mobile → Capacitor; minimal need → ship a 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)

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)4.1M/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
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
Pear (Holepunch)P2P/serverless desktop+mobile runtime (Bare); no central server, content-addressed distribution; depth → holepunch-p2p-systems26/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
react-native-windows / react-native-macos (Microsoft)the RN-NATIVE desktop path — real native UI from RN code, no webview at all; rn-windows tracks core closely (0.84, 2026-06), rn-macos lags a few versions (0.81.x); for RN teams extending to desktop, not for packaging a web app

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 4.1M · pear-runtime-react-native 26 · pear-runtime 2k · react-native-windows 58k · react-native-macos 44k

Verified notes

The durable selection axis is the renderer: Electron BUNDLES Chromium (large, uniform, max ecosystem); Tauri (2.x stable) BORROWS the OS native webview (tiny — from ~600KB — but rendering varies by platform webview) and adds iOS/Android. Pake (V3.12.0, 2026-06-21) is a one-command web-URL→desktop wrapper built ON Tauri (<10MB, ~20x smaller than an Electron equivalent) — great for shipping an existing site, not for a from-scratch app. Pear is the heartit ecosystem's P2P desktop/mobile runtime (defer depth to holepunch-p2p-systems). Tauri/Pake facts verified vs tauri.app + the Pake repo; Electron/Capacitor/PWA rows are settled industry knowledge. RN-DESKTOP ADDED 2026-07-10 (React Weekly; verified vs npm + microsoft/react-native-windows releases): this entry was structurally web-shell-only — 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 choice.

Canonical reading

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

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 (V3.12.0, 2026-06-21) is a one-command web-URL→desktop wrapper built on Tauri, landing under 10 MB — roughly 20× smaller than the Electron equivalent. (Sources: tauri.app; the Pake repository.)

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.

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 · networking · crossplatform