For the last couple of years we have been slowly transitioning our application frontend application from Angular JS v1 to React JS while adding several performance enhancements to the our application. In the last couple of months we managed to put into production our first fully React JS frontend page (with the help of our new monorepo architecture)and we wanted to share how we did it and the results that we have obtained!
A little bit about LumApps
If you do not know about LumApps, we are a global tech company with R&D teams in France that provides our customers a SaaS Digital Workplace solution, which creates a holistic workspace, integrated with several suites and collaboration tools. Want to know a little bit more about us? Head over to LumApps.com and take a look!
Where we needed to improve
The migration from Angular JS v1 to React was one of the key and most important subject of our migration. We wanted to use a newer framework that provided us the tools to fully reuse and compose our UI, while providing an API that is easy to use and fully handles the complex scenarios that LumApps has on the product level.
But this was not the only thing that we wanted to fix in our new frontend version. There were other important aspects that needed to be attended to:
- Bundle size was one of the key performance bottlenecks that we had, since we were downloading a high percentage of code that we did not use in our first render of the page, or the code that was not used due to features being disabled.
- As for our API calls, our first API call which retrieves the initial state of the application was executed from the js bundle, which meant that we lost some precious time between the HTML was parsed and the bundles were downloaded, where we could potentially retrieve that initial state. Furthermore, we wanted to remove as many API calls as possible, since some of the information that we looked up was not needed for the first render of the page.
- Improving our caching strategies for our bundles, assets and API calls was also another point that we wanted to improve, since many of those resources did not implement the appropriate caching mechanisms that would allow our application to provide a different user experience.
- Setup a series of mechanisms that will allow our API calls to be executed with a different levels of priorities, so we can manage that flow of data and allow the non-blocking API calls to be executed at a second stage of the application's life cycle.
How we did it
Split those bundles!
For our JS bundles, we decided to take the philosophy of "only-download-what-you-need".
Basically, the idea here is to split our code into different bundles that will be downloaded only if the feature or the page is currently displayed, or if we want that code to be quickly available for the next navigation.
And in order to make this process simple, we decided to create a small series of questions in order to help our the developers during the process of adding code to the page. By answering these questions, you will definitely know how you need to add your code to the application:
Do we need the code for the first render of the page?
If we do, we need to add it to the main bundle of the page, so importing that module will be done as usual, with a simple import statement. It will be then downloaded with a high priority.
If no, then we need to ask ourselves the following question:
When and where are we going to use that code?
If it is code that will be used to display another page, we are going to lazily load that code using React.Suspense and we will only download it when we need it, which will be on navigation. If it is an extremely used page (as a in a page that is highly likely to be visited during the user's session) we can prefetch the page's code so we can have that code already available before navigating without blocking the main render of the page.
What about specific features on the same page?
This is related to certain features or AB tests that could or could not be there depending on the user's current configuration. If the feature needs a certain flag to be enabled, we should download the code only if that flag is enabled. We should also take the same approach with mobile vs desktop versions, so if there is a feature that only displays on desktop, we should only download that code for the desktop version.
We first took the time to remove or replace the API calls that did not affect the first render of the page, applying the philosophy of “only-download-what-you-need”. Content that would be display upon clicking on a button or opening a dropdown is no longer downloaded with the initial state and only downloaded when that specific data is requested.
We also changed the priority of our API calls, by adding our initial request directly into our HTML, thus triggering this request before the JS bundles start downloading.
And finally, we decided to experiment a little bit with requestIdleCallback and allow certain API calls to be executed once the browser has entered an idle period. With this approach, we managed to have the notifications API call as the last API call that we do, allowing other APIs to be executed before it.
Finally, we decided to tune our caching policies across the application, from changing several Cache Control headers returned from several APIs to creating caching mechanisms on the frontend side in order to save information during navigations or refreshes. We implemented two caching strategies that would save the data in localStorage or in memory:
- If we are using a slow API (more than 500ms to respond) and if the data is cold enough, we started to use a stale-while-revalidate strategy and save the information on localStorage. That way, we can quickly respond and show a cached version of the data and at the same time update the content that we have.
- If we are triggering multiple times an API call and response should not change between those API calls, we used a cache-first strategy while saving this information in memory.
The end result
With all of these changes, we managed to obtain some really interesting results:
- We managed to reduce bundle size by 50%, passing from downloading 13 files for our first render, to 2 files (with an another additional prefetched file for rendering future navigations) 🚀
- API calls were reduced by 85% for the 1st render of the page.
- Our new full React application starts rendering 2.5x faster than our Angular JS app, with an astonishing 5x faster for start rendering the page on the second visit 😍
- Also with our new skeleton and loading state rework, we are now 2x faster for FCP and 2x faster for LCP. This also helped a lot for another Web Core vital called Cumulative Layout shift, where our application averaged a score of 0.004 📊
What is next
Even if we have this big milestone already in place, we still have a long way to go when it comes to our migration to React:
- We still have several pages that need to be fully migrated to React. Today we are redirecting to the legacy application, and we would love to have those pages in our new React application to improve User Experience and keep our users in our SPA
- Maintain the quality of our new frontend. We are investigating several utilities that will help us maintain the level of performance that our current product has, with automatic checks that validate not only the performance of our site, but the use of best practices, accessibility standards and several other key frontend aspects.