---
title: "32 — Desktop Apps from Web Skills — Three Ways to Draw the Same Window"
uid: desktop-apps-javascript
tags: ["desktop", "tauri", "flutter", "cross-platform", "electron", "roadmap:frontend"]
excerpt: "Every cross-platform desktop framework answers the same two questions: how the UI gets rendered, and what the backend is written in. The answers are the whole tradeoff."
date: 2026-08-12T18:35:06+0000
source: https://www.aveshina.my.id/en/blog/desktop-apps-javascript
---

The thing I had to straighten out for desktop apps wasn't a new language — it was a choice I didn't realize was a choice. **Every cross-platform desktop framework answers the same two questions, and the answers are the whole tradeoff:** _how does the UI get rendered on screen,_ and _what runs as the backend (the part with file access, native APIs, the real machine)?_ Once I could see those two axes, the sea of names — Electron, Tauri, Flutter, NW.js — collapsed into a small grid of decisions, each with a price tag attached.

The payoff is that the same web skills (HTML, CSS, JavaScript) can ship a windowed, installable app that reads and writes files, shows a dock icon, and feels native — without me learning Swift, WinUI, or Qt. The cost lives entirely in those two answers.

```figure
<svg viewBox="0 0 740 290" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Three ways to build a desktop window from web skills. Left: Electron bundles a full Chromium browser plus a Node.js backend inside the app — maximum compatibility, heavy bundle. Middle: Tauri reuses the operating system's native webview for the UI and runs a small Rust backend — tiny, secure. Right: Flutter skips the browser entirely and draws its own pixels with a custom rendering engine driven by Dart — pixel-consistent across platforms.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- Electron column -->
    <rect x="20" y="30" width="220" height="230" rx="10" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="130" y="55" font-size="14" font-weight="700" fill="#1e1b4b" text-anchor="middle">Electron</text>
    <text x="130" y="72" font-size="10" fill="#475569" text-anchor="middle">bundled browser + Node</text>
    <rect x="40" y="90" width="180" height="44" rx="6" fill="#c7d2fe" stroke="#6366f1" stroke-width="1.2"/>
    <text x="130" y="108" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">UI: bundled Chromium</text>
    <text x="130" y="123" font-size="9.5" fill="#475569" text-anchor="middle">renders HTML/CSS/JS</text>
    <rect x="40" y="142" width="180" height="44" rx="6" fill="#c7d2fe" stroke="#6366f1" stroke-width="1.2"/>
    <text x="130" y="160" font-size="11" font-weight="700" fill="#1e1b4b" text-anchor="middle">Backend: Node.js</text>
    <text x="130" y="175" font-size="9.5" fill="#475569" text-anchor="middle">fs, native APIs</text>
    <text x="130" y="218" font-size="10" fill="#475569" text-anchor="middle">max compatibility</text>
    <text x="130" y="234" font-size="10" font-style="italic" fill="#64748b" text-anchor="middle">heavy bundle (~100MB+)</text>
    <text x="130" y="250" font-size="9.5" fill="#64748b" text-anchor="middle">VS Code, Discord, Slack</text>

    <!-- Tauri column -->
    <rect x="260" y="30" width="220" height="230" rx="10" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="370" y="55" font-size="14" font-weight="700" fill="#052e16" text-anchor="middle">Tauri</text>
    <text x="370" y="72" font-size="10" fill="#475569" text-anchor="middle">system webview + Rust</text>
    <rect x="280" y="90" width="180" height="44" rx="6" fill="#bbf7d0" stroke="#16a34a" stroke-width="1.2"/>
    <text x="370" y="108" font-size="11" font-weight="700" fill="#052e16" text-anchor="middle">UI: OS native webview</text>
    <text x="370" y="123" font-size="9.5" fill="#475569" text-anchor="middle">WebView2 / WKWebView</text>
    <rect x="280" y="142" width="180" height="44" rx="6" fill="#bbf7d0" stroke="#16a34a" stroke-width="1.2"/>
    <text x="370" y="160" font-size="11" font-weight="700" fill="#052e16" text-anchor="middle">Backend: Rust</text>
    <text x="370" y="175" font-size="9.5" fill="#475569" text-anchor="middle">compiled, sandboxed commands</text>
    <text x="370" y="218" font-size="10" fill="#475569" text-anchor="middle">tiny &amp; secure</text>
    <text x="370" y="234" font-size="10" font-style="italic" fill="#64748b" text-anchor="middle">bundle ~3–10MB</text>
    <text x="370" y="250" font-size="9.5" fill="#64748b" text-anchor="middle">1Password, new-wave tools</text>

    <!-- Flutter column -->
    <rect x="500" y="30" width="220" height="230" rx="10" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="610" y="55" font-size="14" font-weight="700" fill="#422006" text-anchor="middle">Flutter</text>
    <text x="610" y="72" font-size="10" fill="#475569" text-anchor="middle">custom renderer + Dart</text>
    <rect x="520" y="90" width="180" height="44" rx="6" fill="#fef08a" stroke="#ca8a04" stroke-width="1.2"/>
    <text x="610" y="108" font-size="11" font-weight="700" fill="#422006" text-anchor="middle">UI: own rendering engine</text>
    <text x="610" y="123" font-size="9.5" fill="#475569" text-anchor="middle">Skia / Impeller draws pixels</text>
    <rect x="520" y="142" width="180" height="44" rx="6" fill="#fef08a" stroke="#ca8a04" stroke-width="1.2"/>
    <text x="610" y="160" font-size="11" font-weight="700" fill="#422006" text-anchor="middle">Backend: Dart</text>
    <text x="610" y="175" font-size="9.5" fill="#475569" text-anchor="middle">single language, AOT compiled</text>
    <text x="610" y="218" font-size="10" fill="#475569" text-anchor="middle">pixel-consistent UI</text>
    <text x="610" y="234" font-size="10" font-style="italic" fill="#64748b" text-anchor="middle">desktop + mobile + web</text>
    <text x="610" y="250" font-size="9.5" fill="#64748b" text-anchor="middle">Google ads, Alibaba apps</text>

  </g>
</svg>
```

## Electron — ship the browser with the app

Electron's answer to both questions is the biggest possible: it **bundles a full Chromium browser and a full Node.js runtime inside every app** [1]. The UI is a web page rendered by that bundled Chromium; the backend is Node, with full access to the file system, native menus, tray icons, and OS dialogs. The two halves talk to each other over an inter-process bridge (a message channel between them) — the "main" process (Node) and the "renderers" (Chromium windows).

What clicked for me is why the bundle is so large: I'm shipping a browser _inside_ my installer. That's also the superpower. The app behaves identically on Windows, macOS, and Linux because it ignores each OS's native UI toolkit and renders everything in its own Chromium. There is no "depends on the user's browser version" — the version _is_ the one I shipped [2].

The tradeoffs that follow from that decision:

- **Bundle size and memory.** Every Electron app carries a copy of Chromium plus Node. Installers land in the tens to hundreds of megabytes, and a "hello world" window sits on hundreds of MB of RAM because there's a whole browser engine underneath. VS Code, Discord, and Slack are Electron — and the "Electron uses a lot of RAM" reputation comes straight from this design [1].
- **Max compatibility, lowest surprise.** The upside of shipping the browser is that any web tech that works in Chromium works here, full stop. No native-webview quirks per OS.
- **Rapid development.** The same React/Vue/vanilla JS I use for the web becomes a desktop app with a tiny bridge for native calls. For a web developer, the learning curve is mostly "where does the file-access code live," not "how do I draw a window."

Electron is the safe default when I value development speed and UI consistency over footprint, and it's the most battle-tested of the three — the apps I use every day are built on it.

## Tauri — borrow the browser the OS already has

Tauri flips both answers [3]. The UI is still a web page — same HTML/CSS/JS, same React if I want — but it is rendered by **the webview the operating system already ships** — a webview is the OS's built-in browser component for embedding a web page: WebView2 on Windows, WKWebView on macOS, WebKitGTK on Linux. No bundled Chromium. The backend, meanwhile, is a small **Rust** binary that compiles to a native executable, exposing only the commands I explicitly allow.

The mental shift: _the browser is not part of my app anymore._ It's a reusable OS component my app asks to render a window. That single decision reshapes every tradeoff:

- **Tiny bundles.** Without Chromium in the installer, a Tauri app lands in the low single-digit megabytes — often 10–20× smaller than the equivalent Electron app [4].
- **Performance and memory.** A compiled Rust backend and no second browser engine mean a much lighter footprint on disk and in RAM.
- **Security by allowlist.** Native access isn't blanket-granted the way Node grants it in Electron. The Rust core only exposes the JS frontend to commands I declare, and capabilities are scoped per-window [3]. The default posture is "the web layer can't touch the OS unless I said so."
- **The cost: webview inconsistency.** Because Tauri uses whatever webview the OS provides, I'm back in "depends on the user's browser engine" territory. Different platforms ship different webview versions with slightly different quirks. For most modern CSS/JS it's a non-issue, but it's the price of not bundling.

Tauri is the pick when footprint, security, and performance matter — small utilities, internal tools distributed widely, anything where "this is a 150MB chat app" would be embarrassing.

## Flutter — skip the browser entirely

Flutter takes the most radical route: **it doesn't use a browser or an OS webview at all** [5]. The UI is described in **Dart** (not JS), and Flutter's own rendering engine (Skia, and now Impeller) paints every pixel directly to a window. There's no HTML, no DOM, no CSS — the "widgets" are Dart objects that the engine turns into drawn shapes.

This is the option that doesn't really reuse my web skills — Dart isn't JavaScript, and the widget tree isn't HTML. What I trade in familiarity, I buy back in consistency:

- **One codebase, truly.** Flutter compiles the same Dart to Windows, macOS, Linux, iOS, Android, and the web [5][6]. The rendering engine is the same everywhere, so a button looks identical across platforms — because nothing is delegated to a native toolkit.
- **Pixel consistency and smooth animation.** Because Flutter owns the paint pipeline, it sidesteps the per-platform rendering quirks that haunt both webview-based options. Animations hit a consistent frame budget (frames per second) on every target.
- **The cost: a new language and way of thinking.** Flutter is desktop-capable, but its center of gravity is mobile. The desktop story is real and improving, but the ecosystem, patterns, and most tutorials are mobile-first. Coming from web, I'm learning Dart and a widget tree, not reusing HTML/CSS muscle memory [6].

Flutter earns its place when the goal is a product that spans mobile and desktop from one codebase with identical visuals, and I'm willing to invest in Dart up front.

## The decision, compressed

The three options aren't points on a line; they're three corners of a triangle, each optimized for a different thing:

- **Electron** — bundle the browser, get perfect web compatibility and fast shipping, accept the weight. Powers the heavyweights.
- **Tauri** — borrow the OS webview, write the core in Rust, get tiny and secure apps, accept per-platform webview variance.
- **Flutter** — render everything yourself in Dart, get pixel-identical cross-platform UI including mobile, accept a new language and a non-web way of thinking.

## How I use this

The practical habit these notes left me with is a single triage question before reaching for a framework: _what's the backend, and what's drawing the pixels?_ If the answer is "Node everywhere and I need it out the door," it's Electron. If the answer is "as small and locked-down as possible," it's Tauri. If the answer is "mobile and desktop, same UI, I'll learn Dart," it's Flutter. Most of the "which is best" noise dissolves once I notice the three aren't competing on the same axis — they each picked a different thing to optimize.

## References

[1] OpenJS Foundation, "Electron — Build cross-platform desktop apps with JavaScript, HTML, and CSS," Electron Documentation, 2024. [Online]. Available: [https://www.electronjs.org/docs/latest/](https://www.electronjs.org/docs/latest/)

[2] OpenJS Foundation, "Process Model," Electron Documentation, 2024. [Online]. Available: [https://www.electronjs.org/docs/latest/tutorial/process-model](https://www.electronjs.org/docs/latest/tutorial/process-model)

[3] Tauri Programme within the Codex Project, "Tauri — Build smaller, faster, and more secure desktop applications with a web frontend," Tauri Documentation, 2024. [Online]. Available: [https://tauri.app/](https://tauri.app/)

[4] Tauri Programme, "Tauri vs. Electron," Tauri Documentation, 2024. [Online]. Available: [https://v2.tauri.app/concepts/](https://v2.tauri.app/concepts/)

[5] Google, "Flutter — Build apps for any screen," Flutter Documentation, 2024. [Online]. Available: [https://flutter.dev/](https://flutter.dev/)

[6] Google, "Flutter for desktop," Flutter Documentation, 2024. [Online]. Available: [https://flutter.dev/multi-platform/desktop](https://flutter.dev/multi-platform/desktop)

```quiz
Q: What does Electron bundle inside each app to render its UI?
- The operating system's native webview
- A full copy of Chromium plus a Node.js runtime
- A custom rendering engine written in Dart
correct: 1
explain: Electron ships a complete Chromium browser and a Node.js runtime inside every app. That guarantees identical rendering across platforms but is the reason Electron installers and memory footprints are large.

Q: Why are Tauri app bundles dramatically smaller than Electron's?
- Tauri compresses Chromium into a smaller archive
- Tauri reuses the OS's existing webview and writes the backend in compiled Rust
- Tauri apps don't have a backend at all
correct: 1
explain: Tauri renders the UI with the webview the OS already provides (WebView2 / WKWebView / WebKitGTK) and compiles the backend to a small Rust binary — so there's no bundled browser in the installer.

Q: Flutter does NOT use a browser or OS webview. How does it render the UI?
- It compiles HTML/CSS to native widgets
- Its own rendering engine (Skia / Impeller) draws pixels directly from a Dart widget tree
- It delegates rendering to Chromium like Electron
correct: 1
explain: Flutter's engine paints every pixel itself from Dart widgets. There's no DOM, no HTML, no CSS — which is how it keeps visuals identical across desktop, mobile, and web.

Q: Which framework keeps native access locked down by an explicit allowlist of commands?
- Electron
- Tauri
- Flutter
correct: 1
explain: Tauri's Rust core only exposes the commands the developer explicitly declares, and capabilities are scoped per window. Electron's Node backend grants broad access by default.

Q: You're a web developer who needs a desktop app out fast and doesn't mind a heavy footprint. The natural pick is…
- Electron
- Tauri
- Flutter
correct: 0
explain: Electron reuses the exact web stack (HTML/CSS/JS + any frontend framework) and is battle-tested (VS Code, Discord). The cost is bundle size and memory — accepted up front in exchange for speed and compatibility.
```
