Dashboard and Website optimizations
A couple of weeks ago, Sagarpreet Chadha and I wrote about testing frameworks for frontend engineering . Today, Lokprakash Babu , Sagarpreet Chadha and I will walk you through some of the optimizations we have done on our Xflow frontend surfaces and what the impact of these optimizations was.
At Xflow , we have 3 frontend surfaces:
Our journey in the beginning was not unlike that of a lot of other startups. We had a lot to build and deliver to our users. Consequently, we focused on shipping at top speed so that we could get a user feedback loop going. For example, we used (and continue to use) frameworks like Vercel 's Next.js and NestJS . These frameworks are popular and very reliable (they power at-scale experiences like Jio Cinema !). These frameworks enabled us to reduce bootstrap code and came with out of the box optimizations like image optimization , font optimization and link prefetching . Our focus on shipping can also be measured using the changelog page that we maintain here ??
However, the inevitable finally occurred; the website and Dashboard performance started to suffer. Sagarpreet Chadha found that our first-load Javascript (JS) chunk sizes were now ~4 MB. Lokprakash Babu found similar issues with the website.
We knew from our experience that high first-load JS chunk sizes and Largest Contentful Paint (LCP) meant that users over slower internet connections were probably bouncing off the Dashboard and website and that our frontend surfaces are taking too long to be interactive.
And so, we decided to deep dive and investigate further. Our investigation led us to optimize several levers, which we have articulated for you below.
#1 First-load Javascript size & Largest Contentful Paint (LCP) time
Problem statement
The first-load Javascript size directly impacts the time for a web page to become interactive. The JS size for the Dashboard had steadily crept up to 3.84 MB (non-compressed). This meant that if you’re trying to create an invoice in the Xflow Dashboard (that’s right, we support invoices natively!), you were probably getting a sluggish experience. Similarly, the Xflow website homepage’s initial Javascript load was 213 kB. On a slow internet connection, this was preventing users from signing up as the Javascript needed had still not been downloaded. ??
Using the Next.js Bundle Analyzer , we noticed large libraries like react-pdf , lodash , etc. getting bundled into the first-load JS bundle. Along with these external libraries, we also found that numerous SVG’s getting added to the first-load JS. All of these contributed to increasing the JS size.
In addition, the Xflow website had several animations on its homepage for which we were using the popular framer-motion library. This library constituted 90 kB of the 213 kB first-load. Over time, we’d become overly dependent on this library even for simple animations (which we ideally should have implemented with CSS or no Javascript at all). So now that we knew the problem and the root cause, we got down to solutioning and implementation!
Solution
#1 In the Xflow Dashboard and the website, we added dynamic imports for external libraries so that these libraries were downloaded only when required. For example, in the Dashboard, we stopped importing the react-pdf by default and only loaded it when required. Similarly in the website, there was no need to load the library required for powering the Request Demo button unless of course the user initiated this action. Here's an example below:
#2 We started using a subset of functions from libraries like lodash . So instead of loading the entire library and using 3-4 features, we now import only the required 3-4 functions from the library. Here’s an example:
#3 Although it's possible to selectively import specific components from libraries like framer-motion using tree-shaking (example: if you are using 1 of the 10 features in a library, using tree-shaking, you can remove the 9 features not being used from your application's final build), we found that the ”motion” component, central to our animations, accounted for approximately 90 kB in our bundle size. Upon further investigation, we identified a lighter alternative, the ”m” component, which sufficiently meets our animation requirements.
#4 We removed unused Javascript code which referenced old analytics tool integrations, SVG images, etc
#5 We replaced a number of images in SVG format with that of PNG and used next/image component to load the asset only when it is required. This further reduced the first load JS size
#6 We were using the framer-motion library to power simple animations like animating an SVG. However, animating SVGs can be powered by native HTML tags and does not need any external animation library. Hence, we removed the dependency on this library by using native HTML and this further reduced the overall first-load Javascript bundle size. For example, we used the animate native HTML tag for the below animation instead of powering the same via a library
The end-result: our efforts brought down the first-load Javascript chunk size for the Dashboard from 3.84 MB to 748 kB! ??
By using the same principles articulated above, we reduced the first-load Javascript chunk size on the website as well:
Our effort to bring down the first-load Javascript chunk size reduced the Largest Contentful Paint (LCP) metric for the Xflow Dashboard and the website. The LCP is a metric we track closely given that it is the time taken for the largest content (image or text) to appear on the screen:
#2 Build and Deployment time
Problem statement
Most of our infrastructure runs entirely on Amazon Web Services (AWS) . On investigation, we found that the build image size for the Dashboard was now edging closer to ~600 MB. In addition, the time taken to deploy the Dashboard application in the K8s cluster was close to 5 minutes.
领英推荐
Node modules are notorious for how they can take up a ton of space and memory on your machine.
The same happened for us during build time where we were blindly copying all node_modules into the final Docker, Inc image just because we wanted a few of those node modules (like the next package itself) to run the Next.js server, which resulted in a huge Docker, Inc image size.
On investigation, we were able to crystallize the root cause to the approach we were taking to installing the node_modules. Doing a simple “yarn install” installed all packages in an app but most of the installed packages were not required to run the Next.js node server because either that package was (i) used only in a development environment or (ii) already bundled into the Javascript assets which are served via a CDN
Solution
Next.js offers a standalone mode which allows the creation of an independent app with minimal files (including files from the “node_modules“) needed to run a server.
Using the standalone mode, we reduced the Dashboard image size to ~71 MB. This in turn reduced the deployment time to < 30 seconds as most of the time taken earlier was to download the large image from the Amazon Web Services (AWS) Elastic Container Registry .
#3 Disable prefetching of JS assets
Problem statement
The Dashboard homepage has (a lot of) links to various other pages and the default behavior of Next.js is to prefetch assets related to those links automatically if they are in view. As a result, we were fetching close to 12-15 Javascript assets on the load of the Xflow Dashboard homepage. Consequently, if a user clicked on the Create Invoice link on a slow internet connection, they’d get a sluggish experience because the user’s browser would be busy prefetching assets.
On investigation, we found that by default, Next.js prefetches assets for all links that are visible in the viewport. Given the Dashboard homepage has links to various different pages, we were providing a sluggish user experience on the homepage.
Solution
We disabled prefetching for pages that we know were not used frequently by making the prefetch property false in @next/link component (you can read more problems about the default prefetching behavior here ). As a general rule of thumb, it makes sense to prefetch assets only if they are in the viewport of the browser. Even then, we need to be considerate in case there are a ton of links on our website. For example, consider these 2 different examples and see how prefetching can ruin or enhance your user experience:
#4. Making Use of Suspense for Better UX
Problem statement
The pages on our website, such as team , careers , blogs , and faqs need data to be fetched from the Content Management System (CMS) before they are server rendered and delivered to the browser. Till this happens, the page displays a white screen.
On investigation, we found that the Next.js server waits for the entire logic to be executed on the server (like fetching data from API), then passes the fetched data as a prop to the main page component which finally renders the HTML page on the server, and returns the HTML document to the browser. When Next.js is doing all this work on the server, the user sees no action even-though the user clicked on the team's link from the navigation bar.
Solution
We upgraded the website to Next.js 13.4 and started using the app directory of Next.js which allowed us to use server components . Server components unlocked streaming and suspense functionality for Xflow . Using server components, we have localized the data fetching logic to the component instead of the entire page. While the Next.js server is fetching data, the HTML document is still returned to the browser and the component responsible for rendering the data displays a loader. Once the data is fetched at the server component level, it is rendered and the contents are streamed to the browser. Hence, instead of showing an unresponsive screen, we are now showing a loader almost instantly.
The "Suspense" component in React lets you show a loading state till the component gets rendered in the nextjs server. Here's a really good example by Alex Sidorenko on how the “Suspense” component works.
Ok, so now that we’ve gone over all of the work to improve the performance for both the Xflow website and the Xflow Dashboard, let us also tell you about the plan to make sure that we never land in a place of degraded performance again. We’ve put in place the following mechanisms to ensure this.
#1 We plan to automate runs of Google Lighthouse using Github Actions for each Pull Request (PR). We will not merge a PR if Lighthouse scores for Performance, Accessibility, Best Practices, and SEO dip below a specific threshold. For example, here is a PR failing as it adds regression to specific web vitals:
#2 We use New Relic for Application Performance Monitoring (APM). We have created Dashboards for web vitals like LCP, INP (Interaction to Next Paint), and CLS (Cumulative Layout Shift) that show how the Xflow Dashboard is behaving in production on real-world devices. If any of these metrics (averaged over a defined period) breach the threshold, the frontend on-call engineer will be paged for further investigation.
#3 As a rule of thumb in the frontend team, we have now started to carefully examine external libraries before using and we also make sure that the library is appropriately sized, tree-shakeable and can be dynamically imported.
Dafolle - Ton agence de design en illimité
6 个月sounds like xflow is really stepping up their game! can't wait to see the improvements firsthand.
building trmeric, the all-in-one platform for modern tech teams
6 个月So much to learn from these blogs, thanks Ashwin & the remarkable team at Xflow