Saltar al contenido

El arranque sin parpadeo

Por qué una ventana nueva destella en blanco antes de pintar tu interfaz, las tres capas que hay que alinear para que no lo haga, y el temporizador de rescate sin el cual la aplicación puede arrancar invisible.

De dónde sale el destello

Del nivel 4.2: el core y la webview arrancan en paralelo. La ventana existe antes de que el contenido esté pintado, y entre esos dos momentos el sistema tiene que mostrar algo.

TXT
la ventana existe  ──────────────────────────────▶  visible, en blanco                        la webview carga  ─────────▶  visible, con tu interfaz                        └── el destello ──┘

La duración depende de lo que tarde tu frontend en pintar el primer píxel, así que en desarrollo con recarga en caliente puede ser imperceptible y en la versión compilada, con la aplicación arrancando en frío en un disco lento, ser bastante visible.

Y hay un agravante en las interfaces oscuras: el blanco por defecto contra una interfaz oscura produce un destello que llama mucho más la atención que el mismo retraso con una interfaz clara.

Las tres capas

El primer arreglo, y el más barato, es que lo que se ve durante el destello sea del color de tu aplicación. Hay tres capas superpuestas y las tres tienen que coincidir:

Poner los tres del mismo color hace desaparecer el destello como destello: sigue habiendo un retraso hasta que la interfaz aparece, pero deja de haber un cambio brusco de blanco a oscuro.

Y una precisión sobre las aplicaciones con tema claro y oscuro: el color de las capas 1 y 2 se decide al crear la ventana, cuando todavía no sabes qué tema va a aplicar el usuario. La respuesta razonable es leer la preferencia del sistema en Rust antes de crear la ventana y elegir en consecuencia; no es perfecto —si el usuario forzó un tema distinto al del sistema dentro de tu aplicación, el primer instante será del otro color— y es mucho mejor que un blanco fijo.

Abrir oculta y mostrar cuando está listo

El segundo arreglo elimina el retraso visible del todo, a cambio de un riesgo.

JSON
// tauri.conf.json"windows": [{ "label": "main", "visible": false }]
JS
// en el frontend, cuando la interfaz está lista de verdadimport { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';await getCurrentWebviewWindow().show();

Ahora la ventana aparece ya pintada. El usuario ve un retraso —el icono del dock rebotando, nada en pantalla— y después la aplicación completa, que es la sensación de una aplicación nativa.

«Lista de verdad» hay que definirlo, y aquí se equivoca mucha gente. No es cuando el documento ha cargado: es cuando la primera pantalla tiene su contenido. Si muestras la ventana en cuanto carga el HTML, has cambiado un destello blanco por un destello de esqueleto vacío, que no es mejor.

El temporizador de rescate

Aquí está el riesgo, y es serio: si algo falla entre el arranque y la llamada que muestra la ventana, la aplicación queda corriendo e invisible. El proceso está ahí, el icono aparece, y no hay ventana. El usuario no tiene forma de saber qué pasa ni cómo salir salvo matar el proceso.

Y las causas no son raras: un error de JavaScript antes de la llamada, un comando que no responde, una promesa que nunca se resuelve porque el core todavía estaba en su gancho de inicialización —la carrera del nivel 4.2—.

La cobertura es un temporizador en el core:

Rust
.setup(|app| {    let win = app.get_webview_window("main").unwrap();    let rescate = win.clone();    // Si en tres segundos nadie la ha mostrado, la mostramos igual.    tauri::async_runtime::spawn(async move {        tokio::time::sleep(std::time::Duration::from_secs(3)).await;        if !rescate.is_visible().unwrap_or(false) {            let _ = rescate.show();        }    });    Ok(())})

Tres segundos es una ventana razonable: más que cualquier arranque sano y menos de lo que alguien tarda en pensar que la aplicación está rota. Y el resultado en el peor caso es una ventana con la interfaz a medias, que es infinitamente mejor que ninguna ventana.