Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(Almost) unavoidable FOUC and my "fix" - Blogpost style issue #3592

Open
Gabgobie opened this issue Jan 19, 2025 · 5 comments
Open

(Almost) unavoidable FOUC and my "fix" - Blogpost style issue #3592

Gabgobie opened this issue Jan 19, 2025 · 5 comments

Comments

@Gabgobie
Copy link

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.

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.

use dioxus::prelude::*;

#[component]
pub fn PageNotFound(segments: Vec<String>) -> 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 <div id="main"></div> and a <script> 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

<!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();
              }
          });
          }
      );
    </script>
  </body>
</html>

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 <div id="main"></div> 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

<!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">
    <link rel="icon" href="/assets/logo-b33bea773a5932c2.svg" type="image/svg+xml">
  </head>
  <body cz-shortcut-listen="true">
    <div id="main" data-dioxus-id="0">
      <!--placeholder-->
      <!--placeholder-->
      <!--placeholder-->
      <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
        </h1>
      </div>
    </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();
              }
          });
          }
      );
    </script>
  </body>
</html>

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 <div id="main"></div>.

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

<!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" class="w-screen h-screen bg-white dark:bg-black"></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();
              }
          });
          }
      );
    </script>
  </body>
</html>

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.

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 <div id="main"></div> 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 (next) section for a more complete code snippet.

TL;DR

Edit the index.html by adding your TailwindCSS classes to the <div id="main"></div> and add a document::eval with the following contents to your main dioxus component, usually called App

This may look like so:

index.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" class="w-screen h-screen bg-white dark:bg-black"></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();
              }
          });
          }
      );
    </script>
  </body>
</html>

main.rs

#![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::<sites::Routes> {}
    }
}

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. 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 <div id="main"></div> in the dioxus.toml.

Generated - and especially frequently re-generated - files should never need to be edited by hand.

Best,
Gab

@Gabgobie
Copy link
Author

Self triage:

  • Enhancement
  • Bug (Undesirable behavior)

@Gabgobie
Copy link
Author

To further expand on this:

I think that the blocking=render feature would fix this when applied to the wasm and js preload but this feature seems to still be missing in firefox. Chromium based browsers (only tested on edge) don't experience this issue as they seem to already be blocking for the loading of dioxus by default. I haven't tested WebKit but they seem to have been working on this.

I'm guessing this issue is Firefox specific.

@ealmloff
Copy link
Member

ealmloff commented Jan 20, 2025

The remaining issue:

You have to edit the index.html every time you recompile by hand.

You can use a regular css file to apply the background style to the main div: #main { background-color: black }. Then the html doesn't need to be changed at all

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.

Static site generation (SSG) is a much more robust solution to this problem. It generates all static content on pages at build time to speed up initial load times. With SSG you can link to stylesheets with the normal document::Stylesheet component instead of the dioxus.toml workaround and it will get hoisted to the head at build time. You can set it up like this and serve it with dx serve --ssg. Better examples/documentation for SSG is coming soon. (Edit: opened a PR to add docs here)

@Gabgobie
Copy link
Author

Gabgobie commented Jan 24, 2025

Thanks for your thoughts on this and apologies for the late reply.

You can use a regular css file to apply the background style to the main div: #main { background-color: black }. Then the html doesn't need to be changed at all

That sounds like a better alternative. I just upgraded to dioxus-cli 0.6.2 (from 0.6.1) and realised that this won't work for much longer. The [web.resource] section in Dioxus.toml is deprecated and thus the generated index.html will never link to any stylesheets anymore. The document::Stylesheet { href: assets::TAILWIND_CSS } will only insert the stylesheet after the WASM SPA has been initialized.

Static site generation (SSG) is a much more robust solution to this problem.

I 100% agree. After taking a look at your example, I started setting it up for myself. As it seems SSG requires the fullstack feature. I think this shouldn't be the case. I am developing front and backend independently from each other and thus use Dioxus web, desktop and hopefully some day mobile. Now whenever I run dx serve --ssg, my build is failing after successfully compiling the app. [dev] Build failed: Other(Failed to find server executable) so I guess I'll have to setup a server for now.

  • Have you considered making SSG the default behavior?
  • Why is a static_routes server function necessary? An enum that derives Routable already has static_routes available as your example shows. This could be used instead.

Best,
Gab

Edit: For anyone else migrating from just a client to fullstack for this purpose: run cargo clean. I had a bunch of issues and clearing out the cache magically fixed all of them

Edit 2: There seems to be some kind of issue related to this that I am discussing over on Discord

@ealmloff
Copy link
Member

I 100% agree. After taking a look at your example, I started setting it up for myself. As it seems SSG requires the fullstack feature. I think this shouldn't be the case. I am developing front and backend independently from each other and thus use Dioxus web, desktop and hopefully some day mobile. Now whenever I run dx serve --ssg, my build is failing after successfully compiling the app. [dev] Build failed: Other(Failed to find server executable) so I guess I'll have to setup a server for now.

  • Have you considered making SSG the default behavior?

SSG enables fullstack which builds the html initially on the server and then hydrates it on the client. To do that, you need to set up a temporary pre-render server for the build. That is why the fullstack feature is required and a server is required at build time. You don't need to deploy the server the CLI builds. DioxusLabs/docsite#394 adds more docs about the server setup required for ssg

Because ssg prerenders the html on the server, all components need to be runnable on the server. They can't use web specific libraries like web-sys outside of effects. The server and client also need to produce exactly the same html or it will cause hydration issues. DioxusLabs/docsite#397 adds more docs about hydration

  • Why is a static_routes server function necessary? An enum that derives Routable already has static_routes available as your example shows. This could be used instead.

That is what the original implementation of ssg did. The new version of ssg that lives in the CLI could do something similar as the default

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants