Dashboard and Website optimizations

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:

  1. The Xflow website (https://www.xflowpay.com/ ): The website is our 1-stop shop which showcases our products, solutions, API docs, open jobs and how you can sign up to using Xflow (in under 5 minutes!). At the moment, the website is mostly static and served via a Content Delivery Network (CDN)
  2. The Dashboard (https://dashboard.xflowpay.com/ ): The Dashboard is an expression of the Xflow API (in other words, using the Xflow API, you could create your own Dashboard!). It wouldn’t be too far off from the truth to call the Dashboard a beast in comparison to the website. Our Dashboard code spans tens of thousands of lines of code and has a fully-featured Backend for Frontend (BFF)
  3. The Checkout: This is an online payment solution that we offer and is specifically designed for cross-border payment acceptance. We will keep this surface out of scope for this specific post

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.

First-load Javascript chunk size up to nearly 4 MB!

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:

Importing react-pdf library dynamically

#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:

Importing only required functions from the loads library

#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.

Using lighter-weight components

#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

This animation is powered via the native HTML animate tag!

The end-result: our efforts brought down the first-load Javascript chunk size for the Dashboard from 3.84 MB to 748 kB! ??

First-load Javascript chunk size for the Dashboard is down to 748 kB

By using the same principles articulated above, we reduced the first-load Javascript chunk size on the website as well:

  • Homepage: From 213 kB → 146 kB
  • Pricing Page: 177 kB → 110 kB
  • Landing Pages: 184 kB → 117 kB

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:

  • Xflow Dashboard LCP → 4.9s to 2.6s
  • Website LCP → 2.7s to 0.5s

#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.

IYKYK: Every frontend engineer that works with Node knows this about node_modules :)

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 .

Standalone mode to the rescue!

#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.

Xflow Dashboard Homepage

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:

  1. If the user has scrolled all the way down to the footer of your page, there is more probability that the user might click the “link” on the footer so hence we should pre-fetch this link alone and cache it beforehand
  2. Now, consider an e-commerce website that has millions of categories each having a link to a different page. While scrolling through the category list, we should not prefetch assets for different category pages. If we do so, we will be downloading hundreds of assets for pages that will never be opened by the user

#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.

Suspense component example in React



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:

Automating PR failures due to web vitals hygiene failure

#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.

Using APMs to monitor web vitals

#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.

Clara Champion

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.

回复
Siddharth Bohra

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

要查看或添加评论,请登录

社区洞察

其他会员也浏览了