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
With Legacy Component
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 and you are more than welcome to test it by yourself