Hydration pitfalls

When the page is rendered on the server, the browser first shows the HTML received from the server. Then the app starts and, instead of creating the DOM from scratch, it adopts the nodes which are already there. This is called hydration. Vertigo walks its own tree and the server-rendered one side by side, matching elements in order by tag name. Attributes and texts that differ are corrected in place.

If an element doesn't match, the server-rendered node is removed and a new one is created. The page still works, but the work is wasted, and the user may see a flicker or lose a focus or scroll position.

How to check

Open the browser console. After the page loads, vertigo prints a summary:

Hydration complete: 4/6 matched (66.67%), 1 markers skipped, 9 vnodes in batch.

If less than 100% got matched, the summary is a warning, preceded by a warning for every removed node, like Hydration 1: Removing node div. The same report is also available as window.__vertigo_hydration.

Invalid HTML nesting

The server sends HTML as text and the browser parses it, fixing what is invalid. So the browser ends up with a different tree than the one vertigo builds, and the fixed parts don't match:

    use vertigo::dom;

    // Wrong: <p> can't contain a <div>, so the browser closes the paragraph before it
    let wrong_paragraph = dom! {
        <p>"Status: " <div>"OK"</div></p>
    };

    // Wrong: the browser puts a <tbody> between <table> and <tr>
    let wrong_table = dom! {
        <table><tr><td>"Cell"</td></tr></table>
    };

    // Right
    let paragraph = dom! {
        <div>"Status: " <div>"OK"</div></div>
    };

    let table = dom! {
        <table><tbody><tr><td>"Cell"</td></tr></tbody></table>
    };

The same goes for other elements that can't be nested, like <a> inside <a> or <button> inside <button>.

Rendering differently in the browser

Anything that is known only in the browser (is_browser(), cookies, current time, random values) can make the first render differ from the server one. Changed text is cheap, as it's just updated in place. But a different element can't be matched:

    use vertigo::{dom, get_driver};

    let is_browser = get_driver().is_browser();

    // Wrong: different elements on the server and in the browser,
    // so the server-rendered one gets removed and a new one is created
    let wrong = if is_browser {
        dom! { <strong>"Welcome back"</strong> }
    } else {
        dom! { <span>"Loading..."</span> }
    };

    // Better: the same element, only the text differs, so it's just updated in place
    let text = if is_browser {
        "Welcome back"
    } else {
        "Loading..."
    };
    let better = dom! { <span>{text}</span> };

Fetched data

Responses fetched by LazyCache during server-side rendering are sent to the browser along with the HTML, so during hydration the browser renders the same data instead of a loading state. Data loaded in some other way won't be available yet when the app starts in the browser.