Turbolinks - The react way

As you may know, foreman doesn’t use turbolinks anymore, therefore on each page-transition (excluding react pages) a full page reload occurs and leads to:

  • Full rerendering of all HTML content
  • Reinitialize redux’s store - stateless state across pages
  • remounting vertical navigation and topbar component on every transition

All of the above cause a performance drawback, plus pages load without an indication (such as spinner or loading state), white flashes might happen due to layout rerendering and overall all this affects user experience.

Recently we have added React Router to foreman, which is basically a client routing solution and it needed for a clean transition between react pages. We do use it also for every page transition because react-router manages the history and the location objects of the browser, it keeps these objects as one source of truth, and of course, it is much more convenient to handle it in one place. Turbolinks also mutated the history object, which caused some issues when we combined it with react-router.

At the moment we have a fallback route mechanism, react-router detects that the requested page is not a react page, it triggers a full page reload (instead of Turbolinks.visit) with the new path. What if instead of full page reload, react-router can render a Legacy component which will be responsible for its content via ajax request. With that foreman’s store remains and we gain a true stateful application, with almost every page. I’ve opened a PR for this new mechanism.

Let’s dive in for some snippets from react-router switcher which determines the current page rendering, this also shows the simplicity of using a react component instead of a full-page reload for all contents.

The current react-router switcher:

As you can see handleFallbackRoute is responsible for a full-page reload and it keeps the location with a workaround (for making it stateful across pages)

 const handleFallbackRoute = () => {
   const nextPath = window.location.pathname;
   if (currentPath !== nextPath) {
     updateCurrentPath();
     visit(nextPath);
   }
   return null;
 };

 return (
   <Switch>
     {routes.map(({ render: Component, path, ...routeProps }) => (
       <Route
         path={path}
         key={path}
         {...routeProps}
         render={props => handleRoute(Component, props)}
       />
     ))}
     <Route render={handleFallbackRoute} />
   </Switch>

react-router with Legacy content:

The fallback route just rendering Legacy component - the “React Way”

    <Switch>
      {routes.map(({ render: Component, path, ...routeProps }) => (
        <Route
          path={path}
          key={path}
          {...routeProps}
          render={props => handleReactRoute(Component, props)}
        />
      ))}
      <Route render={props => <Legacy {...props} />} />
    </Switch>

Demonstration and User experience

Disclaimer: The next videos and data were taken from compiled javascript environment (no webpack dev-server) and with production flag

When the loading time is above 250 ms, a loading screen will be shown


As you can see, the feedback is significantly slower.

Performance

On the same scenario from above I’ve also captured performance profiles via performence tab in chrome’s devtools

Current

current-state

With Legacy Component

performance-react-router

Stateless vs Stateful (Redux’s store)

Notice the redux recreation, it’s an expansive operation

VS

And of course the PR is ready for review :slight_smile: and you are more than welcome to test it by yourself

6 Likes

Thanks @amirfefer for detailed writeup, I must say it is a visible improvement and sounds like a good change. I’d like to hear from others who understands the implications more than I do, if there are any concerns with this aproach.

I’ve heard two things recently that comes to my mind

what are your thoughts on this?

This is a great example of issues and bugs that might occur due to the location workaround which I presented here, The PR fixes this by the way :slight_smile:

I’m not sure what you meant here, can you please explain a bit further?