What you see
Launch B2 and the window appears immediately as an empty pane, then fills in with the app a moment later. It is worse under make app than in a bundled build, but it is not only a dev-mode artifact.
Why
Tauri shows a window as soon as it exists, which is well before the webview has a stylesheet, let alone a shell. ui/index.html is <div id="app"></div> and nothing else — every pixel comes from main.ts, and style.css is an import inside it. So the gap is: window shown → HTML+JS fetched → module evaluated (this is where the background colour arrives) → boot() → buildShell() → first render().
Under tauri dev the dev server serves the CSS through JS, which widens the first half of that gap; a bundled build gets a <link> in <head> and paints its ground sooner, but still shows an empty window until render().
Not related to page zoom (#224). Measured on a launch with a 160% saved size: the zoom lands 2ms in and the first frame is drawn at 7ms, so no frame is ever painted at the wrong scale. The blank window predates that work.
The obvious fix, and why it isn't one
Create the window hidden ("visible": false) and have the frontend reveal it through a show_window command once render() has painted.
This was tried and reverted. WebKit throttles (or suspends) requestAnimationFrame in a window that is not on screen, so the "wait two frames for real pixels, then reveal" signal never fires — and the window only ever appeared via the safety timer that was there for the case where the frontend breaks. The result was a fixed multi-second wait on every launch: strictly worse than the flash.
Anything built on "the hidden page tells us when it has painted" has to answer that first. Some directions, none of them explored yet:
- Signal from something that still runs while hidden — a
setTimeout chain, document.visibilityState, or the load/paint events WebKit does deliver to an offscreen page — rather than rAF.
- Reveal on a host-side signal instead: wry/WKWebView's own first-paint or navigation-finished notification, if Tauri surfaces one.
- Don't hide the window at all. Give it a background colour so it opens as B2's ground rather than white. Cheap, and it turns a blank white window into a blank dark one — but B2 follows the system theme (
system/light/dark, main.ts's THEME_KEY), and a window background is one fixed value, so it is right in one theme and wrong in the other. Possibly still a net win if the value tracks prefers-color-scheme at launch.
- Put a minimal static shell in
index.html — the ground colour and the header's outline — so the first paint is deliberate rather than empty. Costs a second place that has to know what the app looks like, which is exactly the kind of duplication this repo avoids.
Notes for whoever picks this up
- Any reveal-on-signal design needs the host-side failsafe that the reverted attempt had: an app whose frontend fails before it can ask to be shown must not end up running with no window, because a window is the only place it could report why.
- Measure before and after. The instrument that settled the zoom question was a probe in
boot recording performance.now() for the first rAF against the moment the zoom resolved — the same shape works here for "when did the first frame actually land".
What you see
Launch B2 and the window appears immediately as an empty pane, then fills in with the app a moment later. It is worse under
make appthan in a bundled build, but it is not only a dev-mode artifact.Why
Tauri shows a window as soon as it exists, which is well before the webview has a stylesheet, let alone a shell.
ui/index.htmlis<div id="app"></div>and nothing else — every pixel comes frommain.ts, andstyle.cssis animportinside it. So the gap is: window shown → HTML+JS fetched → module evaluated (this is where the background colour arrives) →boot()→buildShell()→ firstrender().Under
tauri devthe dev server serves the CSS through JS, which widens the first half of that gap; a bundled build gets a<link>in<head>and paints its ground sooner, but still shows an empty window untilrender().Not related to page zoom (#224). Measured on a launch with a 160% saved size: the zoom lands 2ms in and the first frame is drawn at 7ms, so no frame is ever painted at the wrong scale. The blank window predates that work.
The obvious fix, and why it isn't one
Create the window hidden (
"visible": false) and have the frontend reveal it through ashow_windowcommand oncerender()has painted.This was tried and reverted. WebKit throttles (or suspends)
requestAnimationFramein a window that is not on screen, so the "wait two frames for real pixels, then reveal" signal never fires — and the window only ever appeared via the safety timer that was there for the case where the frontend breaks. The result was a fixed multi-second wait on every launch: strictly worse than the flash.Anything built on "the hidden page tells us when it has painted" has to answer that first. Some directions, none of them explored yet:
setTimeoutchain,document.visibilityState, or the load/paint events WebKit does deliver to an offscreen page — rather thanrAF.system/light/dark,main.ts'sTHEME_KEY), and a window background is one fixed value, so it is right in one theme and wrong in the other. Possibly still a net win if the value tracksprefers-color-schemeat launch.index.html— the ground colour and the header's outline — so the first paint is deliberate rather than empty. Costs a second place that has to know what the app looks like, which is exactly the kind of duplication this repo avoids.Notes for whoever picks this up
bootrecordingperformance.now()for the firstrAFagainst the moment the zoom resolved — the same shape works here for "when did the first frame actually land".