← Writing · 1489 words · Stack

Both my app and my website run on single Rust binaries. The framework was the problem.

I made two unrelated decisions in the last six months. The first was to nuke a macOS app after it hit a debugging wall and rebuild it as a single Rust binary. The second was to stop using component frameworks for my websites and wire up shared nav and footer blocks with a tiny CLI and a comment-based marker convention instead. Neither was a Rust evangelism move. Both arrived at the same primitive: one binary, one job, no runtime to babysit.

This article is about what forced each decision, what it looks like in practice, and why I think the same stack pressure shows up even more clearly when agents are doing your iteration work.

Case one: a 3,800-line SwiftUI app that earned its own funeral

I run a fleet of AI agents across a personal operating system built on top of filesystem-as-database sprint tracking. To see what all those agents are doing at any moment. What is queued, what is in flight, what shipped. I built a macOS kanban app to read the sprint folders and display them as cards.

The first version was SwiftUI. Twenty sprints shipped under that implementation. It worked. Then it stopped working: a launch failure from a corrupted preferences plist, then a 100%-CPU spin in the timeline view that I could not trace without deep instrumentation. The app had accreted around 3,800 lines of view/store/filter/markdown plumbing. The debugging cost exceeded the rebuild cost. I filed a spec, nuked the Swift source, and started over.

The rebuild is Rust with iced for the GUI layer. The constraint from the spec is explicit: "Single Rust binary. No webview, no embedded JS runtime, no Tauri." Acceptance criteria include cargo build --release producing a single binary and tools/package.sh producing an .app bundle under 5 MB.

The reason this worked is that the prior implementation's complexity was self-inflicted. The kanban app reads sprint README.md files, parses frontmatter, and displays cards in three views. It does not need a component model. It does not need reactive data binding between a dozen observable classes. It does not need a WebView that pulls in two JavaScript runtimes. The SwiftUI abstraction was solving problems I had given myself by choosing SwiftUI.

A Rust binary with iced gives me exactly the surface the app needs: render views from data, poll the filesystem every two seconds, open a drawer when a card is clicked. No preferences plist to corrupt. No timeline CPU bug that outlives my debugging patience.

The 20 shipped Swift-era sprints are preserved in the project's done/ folder as historical record. That is what I kept. The source got deleted.

Case two: sites composed from HTML markers and a 1.8 MB binary

Three sites I run actively. Felixhellstrom.com, ettsmart.se, and phoeberyanofficial.com. Share the same composition approach. Nav, footer, and other shared blocks are managed by a standalone CLI called pagekit (formerly fragments). The binary is 1.8 MB.

The convention is three pieces. A fragments.toml at the site root configures where the shared blocks live and where the target pages are. A _fragments/ directory holds the fragment files: nav.html, footer.html, nav-blog.html, and a few others depending on the site. Each target page uses comment markers to bracket the region that the binary owns:

<!-- fragment:nav -->
<header class="site-nav">
  . .
</header>
<!-- /fragment:nav -->

When I run pagekit sync, the binary reads each file in _fragments/, finds all pages under pages/ that contain the matching marker pair, and overwrites the content between the markers with the current fragment. That is the entire mechanism. Literal text substitution. No template engine, no component registry, no module graph, no hydration step. I wrote about the design in Marker pairs over template engines.

Every page on all three sites stays valid standalone HTML at all times. I can open any page file in a browser directly without a server. I can hand any page to an agent with no context about how the site is structured and the agent reads exactly what the browser renders. There is no compilation output to reason about, no intermediate representation, no state to reconcile.

The design surface of the tool is intentional. The composition problem for the sites I run is not complex: a binary holds a few shared blocks, a few dozen pages each contain those blocks, keep them in sync when a block changes. The tool I need for that problem is not a framework. It is a binary that reads a TOML config and does string substitution.

The alternative would be an Astro or Next.js project where nav is a component, pages are .astro or .tsx files, and the deploy artifact is a build step away from the source. For the sites I run, that adds a layer of abstraction I do not use. The marker pair convention gives me component reuse without a component model. The same thesis as Vanilla HTML beats your framework.

Why this matters more when agents are doing the work

I am not the only one iterating on these projects. Agents edit these files constantly, across multiple dispatched sessions per day.

When an agent edits a SwiftUI file, it is reasoning about a framework whose runtime behavior lives partly in the binary, not the source. The compiler catches type errors, but the runtime behavior of a reactive view graph with multiple observable object layers is not fully legible from the source alone. When my SwiftUI kanban app developed a 100%-CPU spin, no agent could tell me where it came from without instrumenting the running binary and observing system calls. That is a category of debugging that costs more than rebuilding.

When an agent edits a Rust iced view, the failure mode changes. The code compiles or it does not. The rendering logic is in the source. If something is wrong, the compiler says so, or the visual output shows it plainly. The agent's iteration loop is shorter because the gap between source and observed behavior is smaller. The type system catches the class of errors that would have surfaced as a runtime crash in the SwiftUI build. The same dynamic I described in Why I built my agent orchestrator in Rust on tmux.

The same applies to the fragment sites. When an agent edits _fragments/nav.html, it edits exactly what every page in the site uses. No build step interposes between the file and the browser. If the agent tells me the nav looks right in the file, the nav looks right in the browser. No transpile step, no module bundler, no hydration delay. The artifact the agent produces is the artifact the user sees.

Framework consensus assumes you need the framework's runtime: the bundler, the router, the dev server, the component model, the virtual DOM. For the work a solo developer actually does. A site that is five to fifty pages, a desktop app that reads local files and renders them as cards, an internal tool that does one job. The framework runtime is overhead you imported by default. The primitive that fits the problem is a binary that reads input and produces output.

There is also a compounding effect on agentic iteration. One agent editing fifty HTML files across a site with pagekit writes fifty files and then runs one sync command. No rebuild between any of them. One agent editing fifty .astro pages pays a rebuild on each save. At fifty edits, that wall-clock difference compounds, and agent reasoning quality drops when the loop slows because context evicts faster than it accumulates. The shorter the loop, the more the agent acts like a coworker. The longer the loop, the more it acts like a task queue.

Where the boundary actually is

I want to be honest about when this stops working.

The kanban approach does not scale if the GUI is genuinely complex. iced is Elm-style retained mode: message-passing, no mutable shared state between components. For an app that needs rich text editing, live collaboration, or deeply nested interactive state, that model is the wrong primitive and a framework with better tooling around those affordances wins.

The marker-pair approach has a ceiling. Around fifty pages, or when you need multi-level templates, per-section variants, or conditionally included blocks based on content metadata, you want a proper static site generator. Eleventy or Astro are the right tools for that, not a binary that does string substitution.

Both cases also assume you are the sole operator, or that you have tight control over how source files are modified. The moment a second editor starts touching pages without knowing the marker convention, you have a coordination problem that tooling alone cannot solve.

But: a solo developer running a portfolio site, a marketing site, a desktop tool for personal observability, or an internal dashboard is inside the boundary where these primitives hold. The framework assumption is that you need the abstraction layer. For the scope of work I actually ship, I have not needed it. The binary is the abstraction layer.

I stopped reaching for the framework by default after the second time I found myself rebuilding something that had outgrown its own complexity. Both times, the replacement was smaller, faster to rebuild, and easier for agents to iterate on. That pattern is the point.

Related

Browse by tag →