Notes

Why BlockClock's Web App Is React, Not Flutter

Published 2026-09-14 · 6 min read

BlockClock is a Bitcoin dashboard built to live on a screen you are not using: a split-flap board showing block height, price, mempool fees, hash rate, and a countdown to the next halving, refreshed from public data with nothing running behind it. There is no account system and no server of my own — every number on the board is read straight from public Bitcoin and price APIs by the browser itself. The web app you can open today is built in React. It did not start that way.

The first build looked fine

The original web version was built in Flutter, reusing the same codebase I was already writing for a future mobile app. In a browser it worked exactly as intended: the clock ran, the numbers updated every few seconds, the split-flap animation flipped convincingly on every digit. Click around and nothing seemed wrong. The problem was invisible until I looked at what the page actually sends to anything that isn't a browser running JavaScript.

Canvas rendering leaves the page empty

Flutter web doesn't build a page out of ordinary HTML elements the way a normal website does. It draws the entire interface onto a single canvas and paints every pixel itself — the text, the flap animation, the layout, all of it. That's exactly why a Flutter app looks pixel-identical everywhere it runs, but it comes at a cost: the HTML document the server actually ships is close to empty. A canvas tag, a loading script, and nothing else. No headings. No numbers. No words at all for anything to read.

A person with a browser never notices, because their browser executes the JavaScript and paints the canvas for them. But a meaningful share of the tools that touch a web page do not run JavaScript first — they read the raw response and stop there. To those tools, a canvas-rendered page and a blank page look the same.

Moving the clock face to React

The fix was a rewrite of the web app in React with Vite, built to match a design rather than improvised on the fly, with the interface built out of ordinary DOM elements instead of canvas paint. The split-flap board, the four views, the live data connections — all of it moved over. The board still refreshes itself from the same public sources: block explorer APIs for height, fees and mempool state, and a price API for the dollar figure. Nothing about where the data comes from changed, only how the page presents it to anything reading the markup.

The Flutter codebase didn't get deleted. It's kept in the project for a later phase where BlockClock becomes an installable mobile app — a context where canvas rendering and Flutter's write-once, run-everywhere approach are the right trade-off again, because a native app doesn't need a crawler to read its interior. Web and mobile turned out to want different architectures, so now there are two codebases, on purpose, each doing a job the other one is bad at.

React didn't solve the whole problem

Switching frameworks fixed the canvas issue, but it uncovered a second problem that looks similar and isn't. React, like Flutter web, renders its interface after the page loads: by default, a React app ships a nearly empty <div id="root"> in its raw HTML and fills it in with JavaScript once the browser runs it. Search engines that execute JavaScript can eventually see the content, but plenty of the tools that read a page in the meantime — crawlers that skip script execution, some automated summarizers, link previews — read the first response and move on. For a while, that first response was a title tag, a meta description, and an empty div. No visible text anywhere in it.

The actual fix ended up being simpler than the framework choice had been: split the site into a content page and an app, instead of trying to make one page do both jobs well. The root of the site is now an ordinary static page that explains what the clock shows, in full sentences, before any JavaScript runs at all. The live board itself moved to a separate address, clearly marked so it isn't indexed on its own. A crawler landing on the homepage now reads real paragraphs immediately; a person who wants the actual clock clicks through to it. Separating "the page that explains" from "the page that runs" turned out to matter more than which JavaScript framework was doing the running.

What the board actually reads

Every figure on the board is a live reading, not an estimate or a delayed quote. The Clock view puts one metric on the split-flap board at a time — block height by default, or price, hash rate, or the countdown to the next halving, whichever you set it to. Overview lines up price, block height, and mempool activity together, including a feed of unconfirmed transactions as they arrive. Network covers the state of the chain itself: hash rate, mining difficulty and when it next adjusts, mempool size, and circulating supply measured against Bitcoin's 21 million cap. Halving tracks blocks remaining until the next reward cut, an estimated date, and the history of every halving so far laid out next to the ones still ahead.

None of it is fetched from a server I run. The browser talks directly to public block-explorer and price APIs, which is also why there's no login screen and nothing to configure before the board starts working — open it, and within a second or two it's reading the live chain.