In the first two parts of this series, I wrote about how to use reactjs together with spring boot on a GraalVM and how to use the same routing / navigation on the server side and in the browser.

Today, I want to look into whether rendering on the server side is even worth it. TL;DR: Out-of-the-box, the improvement in time-to-first-render was not that that much. But with a few tricks, I was able to reduce the time by ~60%.

After the last blog post, a friend gave me some great feedback. And one of the things he wrote was “But you do know that googlebot also executes JavaScript?”

My response was yes, but server-side rendering is not only about search engine optimization, it should also improve the time to first render when you had never opened the site before.

Add Some CSS

And after that conversation, I decided to measure the time to first render (over a simulated “Slow 3G” network) and tweak the code a bit. I added some CSS in client/src/MainNavigation.css to make the example a little bit more realistic. This is in the commit 4513dee3f0135575cca7bb272b74d8bd44a44832.

.navigation {
    display: flex;
    list-style: none;
    padding: 0;
}

.navigation li {
    margin: 0;
    padding: 0;
}

.navigation li::after {
    display: inline;
    content: "|";
    padding: 0 0.5em;
}

.navigation li:last-child::after {
    content: "";
}

I am adding this CSS in client/src/MainNavigation.tsx as a webpack import:

import React from 'react'
import { NavLink } from 'react-router-dom'

import './MainNavigation.css'

export const MainNavigation = () => {
    return (<ul className="navigation">
        <li><NavLink to="/">Home</NavLink></li>
        <li><NavLink to="/r/about">About</NavLink></li>
        <li><NavLink to="/r/list">List</NavLink></li>
    </ul>)
}

Client-Side Rendering Only

Here is the result from the production webpack build, without any server-side rendering:

Rendering on the client only: 5.28 seconds

5.28 seconds for rendering on the client only - Surely we can do better than that when using server-side rendering, right?

Server-Side Rendering

Server-side rendering with no modification: 4.08 seconds

4.08 seconds for server-side rendering with no modification - That’s not so much better than before. But let’s look into why this is slower. Rendering is waiting for the CSS and the JavaScript to load.

Defer JavaScript Loading

What if I defer loading all JavaScript? That should improve performance a little bit… But there is some work to do. When I defer the scripts, the function that I want to call in client/public/index.html might not exsit yet - I must wait for it to be defined:

<script type="module">
    function renderWhenAvailable() {
        if(window.renderApp) {
            window.renderApp()
        } else {
            window.setTimeout(renderWhenAvailable, 100)
        }
    }
    if(window.isServer) {
        window.renderAppOnServer()
    } else {
        renderWhenAvailable()
    }
</script>

After that, I can modify my kotlin code (Controller.kt) to add defer="defer" to all scripts:

@Controller
class HtmlController {
    val indexHtml by lazy {
        HtmlController::class.java.getResource("/reactapp/index.html").readText()
                .replace("<script", "<script defer=\"defer\"")
    }
}

I did that in the commit e81d57bfdf4f18379a5719133055c35459089b63. Is it better?

Defer executing all JavaScript: 4.11 seconds

4.11 seconds for server-side rendering and deferring all scripts. That’s not better. The renderer is still waiting for the CSS to load.

Inline CSS With styled-components

Can we improve that? We would have to copy all the CSS code into the HTML file itself - Something that styled-components does by default. So, I changed client/src/MainNavigation.tsx to use styled-components:

import React from 'react'
import { NavLink } from 'react-router-dom'
import styled from 'styled-components'

const Nav = styled.ul`
    display: flex;
    list-style: none;
    padding: 0;
`

const Item = styled.li`
    margin: 0;
    padding: 0;
    ::after {
        display: inline;
        content: "|";
        padding: 0 0.5em;
    }
    :last-child::after {
        content: "";
    }
`

export const MainNavigation = () => {
    return (<Nav>
        <Item><NavLink to="/">Home</NavLink></Item>
        <Item><NavLink to="/r/about">About</NavLink></Item>
        <Item><NavLink to="/r/list">List</NavLink></Item>
    </Nav>)
}

and tried again (commit 531aaf53494de63fbcb57b99bb924694b96d0d44):

Styled components instead of webpack CSS imports: 2.15 seconds

2.15 seconds for server-side rendering and deferring all scripts and using styled-components. Now that’s not bad. The web site renders immediately after loading the HTML file, and then again when all react components were successfully hydrated. I think I cannot do better than that right now.

Read / watch all parts of “Spring and Isomorphic React” here: