On this page · 17 sections
Editor’s note: This is a v0.1.0 retrospective from when MDHero was viewer-only. Since v0.2.0, MDHero also includes a built-in lightweight editor (
Cmd+Eto toggle,Cmd+Sto save). The bundle-size and design philosophy below still apply — the editor is a focused addition, not an Electron-style runtime.
Download MarkText and you’re looking at 170MB on disk. Obsidian: 180MB. VS Code: north of 300MB. These apps ship an entire copy of Chromium plus a Node.js runtime, bundled into a neat .dmg that quietly consumes a quarter gigabyte of your drive.
For an app whose entire job is to render markdown — a format designed to be readable as plain text — that felt wrong. We wanted a genuinely lightweight markdown viewer: a native markdown viewer that opens instantly, weighs almost nothing on disk, and behaves like a real desktop app instead of a browser pretending to be one.
MDHero ships at 7.5MB on macOS (.dmg) and 3.9MB on Windows (.exe installer). The uncompressed macOS .app bundle is about 12MB. This post explains how we got there, what we gave up, and whether a Tauri markdown viewer was worth the trade-offs versus the Electron default.
The Electron tax
Every Electron app is, at minimum, three things packaged together:
- Chromium (~120MB) — a full browser engine
- Node.js (~40MB) — a JavaScript runtime
- Your app — usually a few MB of HTML, CSS, and JS
The math is brutal. If your application code is 5MB, Electron wraps it in 160MB of infrastructure. That’s a 97% overhead ratio. You could fit the entire text of Wikipedia in less space than Electron’s hello-world template.
This isn’t an exaggeration — you can verify it yourself. Run npx create-electron-app my-app, build it, and check the .app bundle size. On macOS, a blank window with zero functionality produces a ~160MB application.
For complex apps like VS Code, that trade-off makes sense. VS Code is essentially a browser — it runs extensions, renders webviews, manages terminal instances. The Chromium overhead pays for itself. But a markdown viewer doesn’t need any of that. It needs to parse text and render HTML. Shipping a browser to display a browser engine’s output is recursive in a way that should bother us.
Why Electron apps are so big
Chromium can’t be shared between Electron apps because each app pins a specific Chromium version for compatibility. So if you run Slack, Discord, and VS Code, you have three independent copies of Chromium on disk. Apple and Microsoft solved this decades ago with shared system frameworks — Electron chose a different path.
Tauri vs Electron: why we chose Tauri
Tauri makes a simple bet: your operating system already has a perfectly good web rendering engine. macOS ships WebKit (the engine behind Safari). Windows 10+ ships WebView2 (Chromium-based Edge). Linux has WebKitGTK. Instead of bundling a browser, Tauri calls the one that’s already there.
The backend story is similar. Where Electron embeds Node.js for backend logic — file system access, native APIs, IPC — Tauri uses Rust. The Rust binary compiles to native machine code with no runtime, no garbage collector, and no V8 engine.
Here’s what that looks like in practice:
| Component | Electron | Tauri |
|---|---|---|
| Renderer | Bundled Chromium (~120MB) | OS WebView (0MB added) |
| Backend runtime | Node.js (~40MB) | Rust (compiled, ~2-4MB) |
| IPC bridge | Electron IPC | Tauri commands (Rust FFI) |
| App code | JS/HTML/CSS (~2-5MB) | JS/HTML/CSS (~2-5MB) |
| Typical total | 150-300MB | 5-15MB |
The OS WebView line is the key insight. On macOS, WKWebView is part of the system frameworks — it’s already loaded in memory for other apps, it’s already on disk, and it’s already receiving security updates from Apple. Tauri simply asks the OS to open a web view and points it at your frontend assets. Zero additional bytes.
The WebView caveat
Using the OS WebView means you inherit its quirks. Safari’s WebKit on macOS behaves differently from Edge’s WebView2 on Windows. CSS features that work perfectly on one platform may need workarounds on the other. We’ll get into this in the trade-offs section.
The frontend: SvelteKit
For the UI layer, we chose SvelteKit. This is the part that actually runs inside the WebView — the HTML, CSS, and JavaScript that renders your markdown, draws the sidebar, handles tabs, and responds to keyboard shortcuts.
Why Svelte specifically? Compiled output size.
React ships a ~40KB runtime (gzipped) that every app pays for. Vue is similar. Svelte takes a different approach: the compiler transforms your components into imperative DOM manipulation code at build time. There’s no virtual DOM diffing at runtime, no framework overhead in the bundle. A Svelte component compiles to roughly the same code you’d write by hand if you were calling document.createElement directly.
For MDHero, the entire frontend build — every component, every style, every utility — compresses to about 1.2MB of assets. That includes:
- The application shell (tabs, sidebar, toolbar)
- CSS for light and dark themes
- All the JavaScript for markdown rendering, keyboard shortcuts, drag-and-drop
- Embedded font subsets
A comparable React app with the same features would likely produce 3-5MB of frontend assets after tree-shaking. Not a dramatic difference in absolute terms, but when your entire download is under 8MB, it matters.
dist/
├── index.html 0.8 KB
├── assets/
│ ├── index-[hash].js 380 KB ← app logic + Svelte compiled output
│ ├── index-[hash].css 48 KB ← all styles
│ ├── vendor-[hash].js 620 KB ← markdown-it, shiki, katex
│ └── fonts/ ~150 KB ← subsetted variable fonts
└── total ~1.2 MB
The vendor chunk is where the real weight lives — and that’s our rendering pipeline.
The rendering pipeline
MDHero’s job is to turn .md text into styled, interactive HTML. Here’s the chain:
1. Parsing: markdown-it
markdown-it is the parser. It takes raw markdown text and produces an HTML string. We chose it over alternatives like remark and marked for a few reasons:
- Plugin architecture — GFM tables, task lists, footnotes, and custom containers are all plugins, not core features. You pay only for what you use.
- Speed — markdown-it parses a 10,000-line file in about 15ms on a modern machine. That’s fast enough to re-render on every keystroke if we ever add editing.
- CommonMark compliance — it follows the spec, which means fewer “why does this render differently?” surprises.
We load five plugins: GFM tables, task lists, footnotes, front matter detection, and a custom plugin for LLM paste cleanup (detecting and fixing escaped \n characters from ChatGPT/Claude output).
2. Syntax highlighting: Shiki
Code blocks get highlighted by Shiki, which uses the same TextMate grammars as VS Code. This means syntax highlighting in MDHero looks identical to what developers see in their editor. We bundle grammars for 25+ languages, loaded on demand — only the grammars for languages actually present in the document get parsed.
Shiki is heavier than alternatives like Prism or highlight.js, but the output quality is noticeably better for edge cases (nested template strings in TypeScript, embedded SQL in Python, JSX mixed with TypeScript generics). For a viewer, rendering quality is the product.
3. Math: KaTeX
LaTeX math expressions ($...$ inline, $$...$$ block) are rendered by KaTeX. KaTeX was chosen over MathJax because it’s roughly 5x faster for initial rendering and produces output as plain HTML+CSS rather than requiring SVG. The trade-off is that KaTeX supports fewer LaTeX commands, but for the math you’d write in a markdown file — equations, matrices, Greek letters, summations — it covers everything.
4. Diagrams: Mermaid
Mermaid renders flowcharts, sequence diagrams, Gantt charts, and other diagram types from text definitions inside fenced code blocks. This is the heaviest dependency in the rendering chain — Mermaid itself is about 200KB gzipped. We lazy-load it: the Mermaid library only downloads and initializes when the parser detects a ```mermaid code block in the document. If your file doesn’t use diagrams, you never pay for Mermaid.
Lazy loading strategy
Shiki grammars and Mermaid are both lazy-loaded. On a typical README with Python and JavaScript code blocks but no diagrams, the rendering pipeline loads only the markdown-it core (~40KB), two Shiki grammars (~15KB each), and KaTeX (~90KB). Total rendering overhead: about 160KB. This is why MDHero opens files almost instantly — you only pay for the features the document actually uses.
What we cut
Small apps are as much about what you don’t build as what you do. Here’s what we deliberately left out, and why:
No auto-updater daemon. Electron apps typically use electron-updater or Squirrel, which runs a background process to check for updates. We use GitHub Releases. The app checks for a new version on launch (a single HTTPS request to the GitHub API), shows a badge if one exists, and links you to the download page. No background daemon, no delta patching, no signature verification infrastructure. The cost: users download the full ~8MB app again instead of a small delta. We’re fine with that.
No built-in spell checker. Electron gets spell checking for free because Chromium includes one. Tauri’s WebView on macOS exposes the system spell checker, but we’re building a viewer, not an editor. Spell checking a read-only document is useless, so we don’t initialize it.
No plugin system. Obsidian’s plugin system is why Obsidian is 180MB and growing. Plugin infrastructure requires a sandbox, an API surface, a package manager, a settings UI. Every plugin system eventually becomes a platform. We’re an app.
No embedded browser. Some markdown tools include a “web clipping” feature — paste a URL, it fetches the page, converts it to markdown. That requires an HTTP client, an HTML-to-markdown converter, and a content sanitizer. It’s a cool feature. It’s also three dependencies, a new attack surface, and a feature that has nothing to do with viewing markdown files on your local disk.
No file sync. No iCloud integration, no Dropbox watch, no Git pull. MDHero reads files from your file system. If you want sync, use whatever sync tool you already use — the file system is the API.
Each of these features is individually reasonable. Collectively, they’d add 5-10MB of dependencies, introduce maintenance surface area, and blur the focus of the app. A viewer views.
Binary size breakdown: a native markdown viewer in ~8MB
Where do the bytes actually go? Here are the actual sizes from the v0.1.0 GitHub release:
GitHub Release (v0.1.0)
├── MDHero_0.1.0_aarch64.dmg 7.5 MB ← macOS download
├── MDHero_aarch64.app.tar.gz 6.2 MB ← macOS compressed bundle
├── MDHero_0.1.0_x64_en-US.msi 5.0 MB ← Windows MSI installer
└── MDHero_0.1.0_x64-setup.exe 3.9 MB ← Windows NSIS installer
The uncompressed macOS .app bundle is about 12MB on disk, which breaks down roughly as:
MDHero.app (~12MB uncompressed)
├── Rust backend binary ~3.5 MB
│ ├── Tauri runtime + IPC ~1.5 MB
│ ├── File system operations ~0.3 MB
│ ├── Window management ~0.4 MB
│ ├── System tray + menus ~0.2 MB
│ ├── Rust std library ~0.8 MB
│ └── Other (serde, etc.) ~0.3 MB
├── Frontend assets ~1.2 MB
│ ├── JavaScript (app + vendor) ~1.0 MB
│ ├── CSS ~0.05 MB
│ └── Fonts ~0.15 MB
├── App metadata + icons ~0.3 MB
│ ├── Icon set (multiple sizes) ~0.2 MB
│ └── Info.plist + signatures ~0.1 MB
└── Framework stubs + overhead ~7.0 MB
└── macOS bundle structure ~7.0 MB
The Rust binary is the biggest single component. Rust doesn’t have a separate runtime, but it does statically link portions of its standard library (string handling, file I/O, collections, error types). With opt-level = "z" (optimize for size) and lto = true (link-time optimization), we strip this down significantly. Without LTO, the binary would be about 6MB.
On Windows, the .exe NSIS installer is only 3.9MB — smaller than macOS because Windows 10+ already ships WebView2 (via Edge), so there’s no need to bundle a bootstrapper. The .msi installer is 5MB because it includes a WebView2 bootstrapper for the rare case where Edge isn’t installed.
Stripping and UPX
We run strip on the release binary to remove debug symbols. Some Tauri apps also use UPX compression to further reduce binary size, but we’ve avoided it — UPX-packed binaries trigger false positives in some antivirus software, and the 1-2MB savings isn’t worth the support tickets.
Developer experience trade-offs
Tauri is not Electron with a Rust coat of paint. The developer experience is meaningfully different, and not always better. Here’s what we’ve learned building with it for six months.
WebView inconsistencies
This is the big one. On macOS, you’re running WebKit. On Windows, you’re running Chromium (via Edge). They’re different engines with different behaviors.
Examples we’ve hit:
- CSS
backdrop-filter— works out of the box on macOS WebKit, needed a-webkit-prefix on some older macOS versions, and had different blur rendering on Windows. scrollbar-width: thin— supported on Windows WebView2 (Chromium), not on macOS WebKit. We use::-webkit-scrollbarpseudo-elements instead and style them per platform.Intl.Segmenter— available on Windows WebView2 since it tracks Chrome releases, but only landed in Safari 17.4. If your macOS user is on Ventura without updates, it’s missing.- File drag-and-drop — the
dataTransferAPI returns file paths differently on each platform. On macOS you get afile://URL; on Windows you get a backslash-separated path. The Tauri file drop event abstracts this, but only if you use Tauri’s API instead of raw browser events.
None of these are showstoppers, but they add up. Every feature needs testing on both platforms, and the debugging story is different on each.
Debugging
Electron apps are Chrome. You open Chrome DevTools and everything works — breakpoints, network inspection, performance profiling, memory snapshots. The tooling is world-class because it is Chrome’s tooling.
Tauri on macOS uses Safari’s Web Inspector. It’s adequate but less polished. Console output sometimes loses stack traces. The network tab doesn’t capture Tauri IPC calls (they go through the Rust bridge, not HTTP). Performance profiling works but the flame charts are harder to read.
On Windows, you can attach Edge DevTools to the WebView2 instance, which is excellent — it’s the same Chromium DevTools experience. But macOS developers (which is most of our development time) are stuck with Safari’s tooling.
Fewer npm packages “just work”
The Electron ecosystem has thousands of packages that assume they’re running in a Node.js + Chromium environment. Packages that call require('electron') for native file dialogs, clipboard access, or system notifications. In Tauri, those APIs exist but with different signatures — you call them through @tauri-apps/api or invoke Rust commands.
This means you can’t just npm install electron-store for persistent settings. You write a Rust command that reads/writes a JSON file, then call it from the frontend. It’s more work, but the result is a Rust function that’s type-safe, doesn’t load a 40MB runtime, and runs in microseconds instead of milliseconds.
Build times
Rust compilation is not fast. A clean build of the Tauri backend takes about 45 seconds on an M2 MacBook Pro. Incremental builds are faster (5-10 seconds for a Rust change), and frontend-only changes hot-reload instantly through Vite. But if you’re used to Electron’s sub-second reloads for everything, the first cargo build will test your patience.
We mitigate this with cargo-watch for automatic rebuilds and by keeping the Rust layer thin — most UI logic lives in the Svelte frontend where iteration is fast.
Performance benefits of a small markdown app
The size advantage is nice for download and disk usage, but the runtime benefits matter more day-to-day. A small markdown app doesn’t just save disk space — it changes how the app feels in use.
Cold start time
When you double-click an Electron app, here’s what happens: the OS loads the app bundle, Chromium initializes its multi-process architecture (browser process, GPU process, renderer process), V8 (the JavaScript engine) boots, Node.js initializes its module system, and then your app code starts running. On a fast machine, this takes 1-3 seconds. On an older laptop, 3-5 seconds.
When you double-click MDHero: the OS loads a native binary, Tauri requests a WebView from the OS (which is already warm because the OS uses it internally), and the frontend assets load from the local bundle. Time to first paint: about 400ms on an M1 Mac. Time to fully rendered markdown: about 600ms, including file parsing.
That difference compounds. If you open markdown files ten times a day — checking READMEs, reading Claude Code plans, reviewing documentation — the cumulative time savings adds up. This is the practical case for a fast markdown reader: not benchmark bragging rights, but fewer seconds of wasted attention each time you double-click a .md file.
Memory usage
We tested five apps opening the same 5,000-line markdown file with code blocks, tables, and a Mermaid diagram:
| App | Idle memory | With file loaded |
|---|---|---|
| MDHero | 45 MB | 72 MB |
| Typora | 120 MB | 180 MB |
| MarkText | 190 MB | 280 MB |
| Obsidian | 210 MB | 310 MB |
| VS Code | 380 MB | 450 MB |
MDHero uses less memory idle than the other apps use for just their runtime overhead. This matters on machines with 8GB of RAM (still the base config for MacBook Air), where every megabyte of memory pressure contributes to swap and slowdowns.
How we measured
Memory figures are RSS (Resident Set Size) from Activity Monitor on macOS, measured 30 seconds after the file finished rendering to let GC settle. Each app was launched fresh with no other files open. Your numbers will vary — these are directional, not definitive.
Native OS integration
Because Tauri talks directly to the OS, native integrations are first-class:
- macOS: native title bar with traffic light buttons, system accent color support, proper full-screen behavior, Handoff support, file association registration that actually works in Finder
- Windows: native window chrome, taskbar jump lists, proper DPI scaling, system dark mode detection
- Both: native file open dialogs, drag-and-drop from Finder/Explorer, system font rendering (not Chromium’s Skia-based rendering)
The font rendering difference is surprisingly noticeable. Chromium uses its own text rendering pipeline (Skia), which looks slightly different from native macOS text. Tauri’s WebKit on macOS uses CoreText — the same renderer as every other native Mac app. Text in MDHero looks like it belongs on your Mac because it is rendered by your Mac.
Would we choose Tauri again?
Yes. Without reservation for this type of app.
The qualification matters: “this type of app.” MDHero is a viewer. It reads files from disk, renders them in a web view, and provides navigation UI. The Rust backend handles file I/O, window management, and system integration. The frontend handles rendering and interaction. That’s a clean split that maps perfectly to Tauri’s architecture.
If we were building a collaborative editor with real-time sync, a plugin marketplace, or an app that needed to embed a terminal emulator — we’d think harder. Electron’s maturity, ecosystem, and tooling have real value for complex apps. The debug-ability alone might be worth the 160MB tax.
But for tools that do one thing well — viewers, converters, launchers, clipboard managers, system utilities — Tauri is the right choice in 2026. The 1.x rough edges have smoothed out in 2.x. The community is growing. The documentation is solid. And your users get an app that downloads in seconds, launches instantly, and doesn’t eat their RAM.
The 8MB binary isn’t a gimmick or an optimization exercise. It’s a side effect of making the right architectural choice: don’t ship what the OS already provides. If you’re shopping for a non-Electron, native markdown viewer that respects your disk and your battery, that architecture is the whole point.
See also: The best free markdown viewers in 2026 — a comparison of MDHero against Typora, MarkText, Obsidian, and other markdown reader options on macOS and Windows.
Appendix: reproducing our measurements
If you want to verify our numbers or run your own comparisons:
# Check app bundle size on macOS
du -sh /Applications/MDHero.app
# Check RSS memory usage for a running app
ps -o rss,comm -p $(pgrep -f MDHero) | awk '{print $1/1024 " MB", $2}'
# Check Electron app bundle contents
find /Applications/SomeElectronApp.app -name "*.pak" -o -name "*.dat" | \
xargs du -sh | sort -rh | head -10
# Build a blank Electron app to see baseline size
npx create-electron-app blank-test && cd blank-test
npm run make
du -sh out/make/**/*.app
These are the tools we used. No proprietary benchmarking, no cherry-picked runs. Just du and ps.