DioxusLabs / DioxusLabs/dioxus
(Almost) unavoidable FOUC and my "fix" - Blogpost style issue
- Dominant language
- Rust
- Stars
- 39.1k
- Forks
- 1.9k
- Avg merge
- 4d 10h
- Merged PRs (30d)
- 4
Description
Hi,
I have been fighting with a FOUC (Flash Of Unstyled Content) for the past few hours and finally found a solution that somewhat works for me but has to be reapplied each time I recompile.
In this issue I am both looking for a better and more permanent fix as well as to help out anyone else experiencing this issue. This may be a long read since I intend on explaining the issue, the fix and how I got there. You can find the [TL;DR at the end](#TL;DR).
# L;R
It all began with my 404 page. It's a simple page. A black background with white text. I am using TailwindCSS here, in this case with a different behavior for dark and light mode. Ofc. this issue will only be noticable when loading in dark mode.
This component is the catchall in my router and will always be displayed on its own without any styling around it.
```Rust
use dioxus::prelude::*;
#[component]
pub fn PageNotFound(segments: Vec) -> Element {
rsx! {
div {
class: "flex items-center justify-center w-screen h-screen bg-white dark:bg-black",
h1 {
class: "font-mono text-4xl text-black dark:text-white",
"404 Not Found"
}
}
}
}
```
Now when loading an unknown route and getting to this page, (on Firefox) the following happens:
1. Your eyes get burned out of their sockets by bright white nothingness.
2. The component loads and the page goes black.
3. Seizure warning in case of frequent reloads!
My first thought was that this issue was caused by the stylesheet being loaded too late. After wasting a lot of time trying to get it to load any earlier than it already is, I noticed that this was not the cause. In fact, the cause was something else entirely:
The page was ready to be rendered and the styles were injected only after that. How so? Well, `dx` will generate an `index.html` for you that loads the resources you specified in the `dioxus.toml`, specifies a `
` and a `` that loads and initializes the WASM SPA.Now this is where the issue arrises. In Firefox that script doesn't seem to be blocking. As an effect, you have a page that is ready to render with no content and firefox will display just that. An empty, white page. Rather bright after you had just been looking at the pitch black dark mode page.
At this time the HTML will look more or less like this:
> index.html
```HTML
<!DOCTYPE html>
<html>
<head>
<title>Your App</title>
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta charset="UTF-8" />
<link rel="stylesheet" href="assets/tailwind.css">
<link rel="preload" href="/./wasm/frontend_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
<link rel="preload" href="/./wasm/frontend.js" as="script">
</head>
<body>
<div id="main"></div>
<script>
// We can't use a module script here because we need to start the script immediately when streaming
import("/./wasm/frontend.js").then(
({ default: init }) => {
init("/./wasm/frontend_bg.wasm").then((wasm) => {
if (wasm.__wbindgen_start == undefined) {
wasm.main();
}
});
}
);
```
If you would like to take an extended look at the page as it would be rendered, you can just remove the script so the WASM is never launched.
Next, the script finishes and Dioxus will fill the `
` with the contents of your SPA. This is when the styles you applied inside your Rust code become part of the HTML. This may look like this:> index.html
```HTML
Your App
404 Not Found
// We can't use a module script here because we need to start the script immediately when streaming
import("/./wasm/frontend.js").then(
({ default: init }) => {
init("/./wasm/frontend_bg.wasm").then((wasm) => {
if (wasm.__wbindgen_start == undefined) {
wasm.main();
}
});
}
);
```
As you can see, your styles are only applied AFTER the Dioxus app has finished loading and initializing. Now what can we do about it? For starters, we can apply a style to our unstyled `
`.In my case, using TailwindCSS, that would be the following classes: `class="w-screen h-screen bg-white dark:bg-black"`
The only thing they are doing is to ensure the entire screen is either black or white depending on your settings for light or dark mode.
Now your `index.html` should look like this:
> index.html
```HTML
Your App
// We can't use a module script here because we need to start the script immediately when streaming
import("/./wasm/frontend.js").then(
({ default: init }) => {
init("/./wasm/frontend_bg.wasm").then((wasm) => {
if (wasm.__wbindgen_start == undefined) {
wasm.main();
}
});
}
);
```
But this is where the next issue arrises: Now your entire page is going to have a black or white background unless you override this behavior in your components. We don't want that. These styles are only here to prevent flashing the user until the WASM has loaded. We need to get rid of them at the right moment in time.
Our savior: `document::eval()`
Using JavaScript, we can remove the classes we applied earlier from the `div` at runtime.
```Rust
let _ = use_resource(move || async {
document::eval(r#"document.getElementById('main').removeAttribute('class');"#).await.unwrap();
});
```
Place the above snippet in your main dioxus component. It is usually called `App`. The snippet will remove the classes we applied from the `
` as soon as the SPA is loaded. No more inheriting classes we don't want or need anymore. The same could probably also be done with `style` directly so we don't even depend on loading the css file first.Take a look at the [TL;DR](#TL;DR) (next) section for a more complete code snippet.
# TL;DR
Edit the `index.html` by adding your TailwindCSS classes to the `
` and add a `document::eval` with the following contents to your main dioxus component, usually called `App`This may look like so:
> index.html
```html
Your App
// We can't use a module script here because we need to start the script immediately when streaming
import("/./wasm/frontend.js").then(
({ default: init }) => {
init("/./wasm/frontend_bg.wasm").then((wasm) => {
if (wasm.__wbindgen_start == undefined) {
wasm.main();
}
});
}
);
```
> main.rs
```Rust
#![allow(non_snake_case)]
// Import the Dioxus prelude to gain access to the `rsx!` macro and the `Scope` and `Element` types.
use dioxus::logger::tracing::Level;
use dioxus::prelude::*;
mod assets;
mod components;
mod sites;
mod svg;
fn main() {
dioxus::logger::init(Level::INFO).expect("logger failed to init");
#[cfg(feature = "desktop")]
fn launch_app() {
use dioxus::desktop::tao;
let window = tao::window::WindowBuilder::new().with_resizable(true);
dioxus::LaunchBuilder::new()
.with_cfg(
dioxus::desktop::Config::new()
.with_window(window)
.with_menu(None),
)
.launch(App);
}
#[cfg(not(feature = "desktop"))]
fn launch_app() {
dioxus::launch(App);
}
launch_app();
}
#[component]
fn App() -> Element {
let _ = use_resource(move || async {
document::eval(r#"document.getElementById('main').removeAttribute('class');"#).await.unwrap();
});
rsx! {
{
#[cfg(not(feature = "web"))]
document::Stylesheet {
href: assets::TAILWIND_CSS,
}
},
document::Title {
"Your App"
}
document::Link { href: assets::LOGO, rel: "icon", r#type: "image/svg+xml" }
Router:: {}
}
}
```
The remaining issue:
1. You have to edit the `index.html` every time you recompile by hand.
2. You have to decide on a style that should be applied to your entire page first and only after the Dioxus WASM has loaded and initialized, the actual page style will be applied, no matter which style that may be. That may be an issue with more colourful pages or if you have a variety of different backgrounds throughout your page
As for the second issue, there are only two ways I can think of to defeat it. One is to use the SSR feature, the other to ensure that loading the SPA is blocking.
# Conclusion
We defeated the FOUC. But at what cost?
Now we have to edit our `index.html` each time we recompile. To my knowledge, there is no way to specify the classes/styles that are to be applied by default in our [`dioxus.toml`](https://dioxuslabs.com/learn/0.6/CLI/configure/). This is where I think an enhancement can be made by either finding a way to make the loading a blocking operation so the browser, specifically Firefox, doesn't render the unstyled, empty page immediately but instead waits for the SPA to show up or by providing a way to apply the default classes/styles for `
` in the `dioxus.toml`.Generated - and especially frequently re-generated - files should never need to be edited by hand.
Best,
Gab
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.