Boosting Performance with Next.js and React Server Components: A geekyants.com Case Study

Apr 12, 2024

Boosting Performance with Next.js and React Server Components: A geekyants.com Case Study

GeekyAnts upgraded to Next.js 13 with RSC for blazing-fast website performance & a seamless user experience. Learn the process & takeaways!

Subject Matter Expert

Sapna
SapnaSenior Software Engineer - III

We recently upgraded our website geekyants.com to the latest version of Next.js using React Server Components (RSC). The goal was to improve our website's performance and user experience.

After deployment, we greatly boosted Lighthouse and PageSpeed scores, enhanced SEO health reports, and created a smoother user experience. The health scores reached 90+ and all parameters passed optimal standards.

This article summarizes what we learned and the process we followed to upgrade the website.


Why Upgrade & Challenges in Transitioning

We were dealing with low page speed and Lighthouse scores. It was affecting our SEO rankings. The scores averaged around 50, a suboptimal number. Moreover, since our landing page is full of information and media, optimizing it was hard.

Switching to React Server Components also meant changing how we handle API calls, moving from client-side to server-side. It would require us to reorganize our data-handling approach.

Lighthouse Score Before Upgrade Versus After Next.js 13 Upgrade
Lighthouse Score Before Upgrade Versus After Next.js 13 Upgrade


How We Upgraded: Improving Page Performance

Hire Us Form

We followed a process that involved upgrading to the latest Next.js version and the adoption of many new best practices standards. The details are below.

1. Adopting React Server Components

Using RSC, we reduced the need for client-side processing, making the site respond faster. This change resulted in less JavaScript being sent to the browser, which sped up how quickly the page could be interacted with and viewed. For example- The React tree is selective and doesn't create React Client components for all DOM elements.

Before and After Comparison for the JavaScript Execution Time, Minimize Main-thread Work, and Excessive DOM Size
Before and After Comparison for the JavaScript Execution Time, Minimize Main-thread Work, and Excessive DOM Size

2. SVG Optimization

We optimized our SVG handling by using CDN URLs for the SVGs, which helped speed up page loading without any additional DOM size.

When incorporating SVGs into your Next.js app, consider the following options:

  • next/image Component: Next.js offers a powerful built-in feature for loading and rendering various image formats, including SVGs, with efficiency and performance optimizations. It extends the HTML image element, providing seamless integration and benefits such as automatic optimization.
  • next-images Package: Alternatively, you can leverage the third-party next-images package for importing images. This package supports loading images from local machines or CDNs, and it offers additional features like embedding images with small bundle sizes in Base64 encoding and caching images with content hash. However, it lacks built-in image optimization techniques.
  • SVGR Package: Another third-party option is SVGR, which converts SVG images into React components. By installing SVGR as a development dependency, you can import SVGs as React components within your Next.js application.

Keep in mind the trade-offs of using third-party packages, as they may add extra bundles to your application. While embedding SVGs inline is possible, it can complicate React components, increase their size, and negate the benefits of using the built-in next/image component.

In conclusion, Next.js' built-in next/image component provides comprehensive features for efficiently importing and utilizing images in a Next.js application, offering a balance of performance and convenience.

3. Hack to Add Interactivity to Server Components

We used wrapper client components to add interactive features without adding too much client-side JavaScript, keeping the site fast.

Using wrapper client components to add interactive features without adding too much client-side JavaScript, keeping the site fast.
We Used Wrapper Client Components to Add Interactive Features 

We aimed to make the menu header interactive, incorporating hover effects to show and hide items and a sticky header when scrolling, without converting the entire header into a client component. To achieve this, we've created a client component called ToggleHover.

ToggleHover.tsx

"use client";

import { usePathname } from "@/navigation";
import { useEffect, useState } from "react";

const ToggleHover = ({ children }: { children: React.ReactNode }) => {
  const pathname = usePathname();
  const [key, setKey] = useState(0);

  useEffect(() => {
    setKey((prevKey) => prevKey + 1);
  }, [pathname]);
  useEffect(() => {
    const element = document.getElementById("headerMenu") as HTMLElement | null;
    const handleScroll = () => {
      if (element && window?.scrollY > 870) {
        // Change to the desired scroll height
        element.style.display = "block";
      } else {
        if (element) element.style.display = "none";
      }
    };
    if (pathname === "/") {
      if (element && window?.scrollY < 870) element.style.display = "none";

      const addScrollListener = () => {
        window.addEventListener("scroll", handleScroll);
      };
      addScrollListener();
    } else {
      if (element) {
        element.style.display = "block";
      }
    }
    return () => {
      if (pathname === "/") {
        const removeScrollListener = () => {
          window.removeEventListener("scroll", handleScroll);
        };
        removeScrollListener();
      }
    };
  }, [key, pathname]);

  return <div key={key}>{children}</div>;
};

export default ToggleHover;

And then, we wrapped the DesktopMenu, which contains server-rendered HTML, under ToggleHover.

Header.tsx

import DeskTopMenu from "@/components/common/Header/DesktopMenu";
import ToggleHover from "./ToggleHover";

const Header = () => {
  return (
    <>
      <ToggleHover>
        <DeskTopMenu/>
       </ToggleHover>
    </>
  );
};
export default Header;

4. Device-Specific Content Delivery

We started delivering optimised content for the device being used, reducing load times, especially for mobile users.

We aimed to ship the desktop menu and mobile menu separately. To achieve this, we fetched the user agent in the middleware and set it in the header, allowing us to utilize it in the layout.

Middleware.tsx

import createMiddleware from 'next-intl/middleware';
import { NextRequest, userAgent } from 'next/server';


export default async function middleware(request: NextRequest) {
  const { device } = userAgent(request);
  const viewport = device.type === 'mobile' ? 'mobile' : 'desktop';
  request.headers.set('viewport', viewport);
}

Whenever we wish to ship the component based on the device, we can retrieve the header and apply the necessary conditions.

Layout.tsx

import Footer from "../[locale]/sections/Footer";
import Header from "../[locale]/sections/Header";
import { headers } from "next/headers";

export default async function RootLayout({
  children,
  params: { locale }
}: {
  children: React.ReactNode;
  params: { locale: string };
}) {

  const headersList = headers();
  const viewport = headersList.get("viewport") || "";

  return (
    <html >
      
      <body>
        <Header viewport={viewport} />
        {children}
        <Footer/>
      </body>
    </html>
  );
}

Header.tsx

import MobileMenu from "@/components/common/Header/MobileMenu";
import DeskTopMenu from "@/components/common/Header/DesktopMenu";

const Header = ({ viewport }: any) => {
  return (
    <>
      {viewport == "mobile" ? ( 
        <MobileMenu/>
      ) : (
          <DeskTopMenu/>
      )}
    </>
  );
};
export default Header;

5. Third-Party Scripts Optimization

We changed how we load third-party scripts to reduce their impact on load times

@next/third-parties is presently an experimental library undergoing active development. We've utilized it for loading Google's third-party libraries such as Google Tag Manager and YouTube embeds.

(1) Using Next.js's advanced loading strategies

Layout.tsx

import Footer from "../[locale]/sections/Footer";
import Header from "../[locale]/sections/Header";
import { headers } from "next/headers";
import { GoogleTagManager } from '@next/third-parties/google';

export default async function RootLayout({
  children,
  params: { locale }
}: {
  children: React.ReactNode;
  params: { locale: string };
}) {

  const headersList = headers();
  const viewport = headersList.get("viewport") || "";

  return (
    <html >
      
      <body>
        <Header viewport={viewport} />
        {children}
        <Footer/>
        <GoogleTagManager gtmId="GTM-XXXXXX" />
      </body>
    </html>
  );
}

(2) Using setTimeout lazy loading of Chat
We created a distinct client component for loading the chat script with a delay and then utilized it in the Footer.tsx, which serves as a server component. This improved our TTFB.

ChatScript.js

"use client";
import { useEffect } from 'react';

const ChatScript = () => {
    useEffect(() => {
        const timer = setTimeout(() => {
          // Load the script after 10 seconds
          const script = document.createElement('script');
          script.type = 'text/javascript';
          script.id = 'hs-script-loader';
          script.async = true;
          script.defer = true;
          script.src = '//js.hs-scripts.com/225995xx.js';
          document.body.appendChild(script);
        }, 10000); // 10000 milliseconds = 10 seconds
    
        // Clean up the timer to avoid memory leaks
        return () => clearTimeout(timer);
      }, []); // Empty dependency array ensures this effect runs only once after initial render
    
  return (
    <></>
  );
};
export default ChatScript;

6. Code Reduction

We cut down our JavaScript and TypeScript code from 82,926 to 43,294 lines, which helped with faster build times, easier maintenance, and better performance.

52% Reduction in JavaScript and TypeScript Lines of Code After Next.js 13 Upgrade
52% Reduction in JavaScript and TypeScript Lines of Code After Next.js 13 Upgrade

7. Using Dynamic Imports

Traditionally, applications load all the components and the CSS required by the application in the initial load. Dynamic import allows you to split your code into small chunks and load them on demand. This can be a huge performance boost, especially on mobile devices. This will also reduce the initial load time and the overall bundle size of the application.

We are utilizing the react-syntax-highlighter package to display code highlights on our article page. Previously, the build size was substantial; however, implementing dynamic import has significantly reduced the build size and contributed to performance improvement.

Before implementation of dynamic import
Before Implementing Dynamic Import
After Implementing Dynamic Import
After Implementing Dynamic Import


Key Takeaways and Best Practices

Our experience shows that improving a website's performance involves looking at all aspects, from asset handling to content delivery. If you're thinking about making similar changes, here are some tips:

Look at how you manage assets, like SVGs, to reduce load times.

  • Use creative solutions, like wrapper components, to keep your site interactive without slowing it down.
  • Deliver content based on the user's device to improve their experience.
  • Manage third-party scripts wisely to avoid slowing down your site.

These changes have improved our scores and made the user experience on GeekyAnts.com much better, setting a new standard for our site's performance and efficiency.

Lighthouse Performance Score Reached 93 After the Next.js Version 13 Upgrade
Lighthouse Performance Score Reached 93 After the Next.js Version 13 Upgrade


Whatโ€™s Next? Universal!

The next goal is to use Universal Components from gluestack-ui without compromising these lighthouse scores and ship a mobile app using the same codebase.

See you in the next experiment! ๐Ÿ‘จโ€๐Ÿ”ฌ

Subscribe to Our Newsletter

More from the engineering frontline.

Dive deep into our research and insights on design, development, and the impact of various trends to businesses.
Insight
What a PHP-to-NestJS Banking Migration Taught Us About Architecture, Security, and Trust
Sep 8, 2026

What a PHP-to-NestJS Banking Migration Taught Us About Architecture, Security, and Trust

This blog explores the architecture, security, performance, and documentation lessons from migrating a legacy PHP/Laravel banking platform to NestJS.

Insight
Building Production-Grade Video Thumbnail Scrubbing in the Browser: HLS, Frame Extraction, Caching, and Performance Trade-offs
Sep 7, 2026

Building Production-Grade Video Thumbnail Scrubbing in the Browser: HLS, Frame Extraction, Caching, and Performance Trade-offs

This blog explains how to build responsive video thumbnail scrubbing in the browser for local files and HLS streams, covering frame extraction, caching, and performance trade-offs.

Insight
The Agent Can See Your App. How Often Can It Look?
Sep 4, 2026

The Agent Can See Your App. How Often Can It Look?

AI coding agents can now interact with mobile apps, but their effectiveness depends on iteration speed. This blog explores how React Native architecture influences feedback loops and AI-driven developer productivity.

Insight
Building Interactive Cards from Design JSON Without Killing Your Feed: Overlays, Video, Mute/Unmute, and Lag-Free Lists
Sep 1, 2026

Building Interactive Cards from Design JSON Without Killing Your Feed: Overlays, Video, Mute/Unmute, and Lag-Free Lists

Learn how to turn design JSON into interactive, video-enabled cards using overlays, smart media controls, caching, and virtualization without slowing down high-cardinality feeds.

Insight
The Bug That Doesn't Show Up in Code Review: Why Your Flutter Web App Reloads on Safari
Aug 19, 2026

The Bug That Doesn't Show Up in Code Review: Why Your Flutter Web App Reloads on Safari

A real-world look at how oversized images can trigger Safari reloads and iOS crashes in Flutter apps and how smarter image decoding prevents them.

Insight
From Prompting to Process: What Changed When Flutter Shipped Agent Skills
Aug 19, 2026

From Prompting to Process: What Changed When Flutter Shipped Agent Skills

This blog explores how Flutter Agent Skills improve AI-assisted development by combining official framework workflows with project-specific guidance for more consistent development.

Insight
Why Everything Your AI Builds Looks the Same
Aug 19, 2026

Why Everything Your AI Builds Looks the Same

This blog explores why AI-generated interfaces often look alike and explains how design systems, product context, and reusable engineering practices help teams build distinctive, scalable

The Right Conversation Can

Save You Six Months.

Book a call