Improving my website's performance (Part - 6) Optimizing JS load time
In the last part of this series, I discussed how I fixed the LCP load time of my website and you might have observed that the Element Render Delay was around 630ms in the dev environment.
So in this part, I will discuss how I optimized the JS load time of my website to improve the overall performance.
Optimizing JS load time
In the dev environment of my site which is running on Next 15, I made some changes to optimize the JS load time.
JavaScript Bundle Size
To check the JS bundle size, I used the Next.js Bundle Analyzer.
You can read on how to set it up in my previous article here.
Here is the comparison of the JS bundle size before and after optimization:
This is the screenshot of the JS bundle size before optimization :
Techniques used to optimize JS load time
-
Dynamic Imports: I used dynamic imports to load components only when they are needed. This helped in reducing the initial load time.
-
Lazy Loading: I implemented lazy loading for non-critical components and images. This ensured that only the necessary resources are loaded initially.
-
Reduce Third-Party Libraries: I reviewed the third-party libraries used in my project and removed any that were not essential. This helped in reducing the overall bundle size.
Dynamic Imports Example:
import dynamic from 'next/dynamic';
const HeavyComponent = dynamic(() => import('../components/HeavyComponent'), {
ssr: false,
});
Lazy Load Imports :
So I had this component which was loading a lot of JS and was not required in the initial load of the page.
In the page layout file, I was using Aurora component like this:
import Aurora from '../components/Aurora';
interface PageLayoutProps {
children: ReactNode;
}
const PageLayout: React.FC<PageLayoutProps> = ({ children }) => {
return (
<div>
<Aurora>{children}</Aurora>
</div>
);
};
export default PageLayout;
So this was adding a lot of JS to the initial bundle size.
After optimization, I changed the code to load the Aurora component dynamically inside a useEffect hook, so that it only loads when the component is mounted on the client side.
"use client";
import { useEffect, useState } from "react";
import { ReactNode, Suspense, lazy } from "react";
interface PageLayoutProps {
children: ReactNode;
}
const PageLayout: React.FC<PageLayoutProps> = ({ children }) => {
const [Aurora, setAurora] = useState<React.ComponentType<{ children: ReactNode }> | null>(null);
useEffect(() => {
const loadAurora = async () => {
const { default: LoadedAurora } = await import('../components/Aurora');
setAurora(() => LoadedAurora);
};
loadAurora();
}, []);
return (
<div>
{Aurora ? (
<Suspense fallback={<div>Loading...</div>}>
<Aurora>{children}</Aurora>
</Suspense>
) : (
<div>{children}</div>
)}
</div>
);
};
export default PageLayout;
This ok, but there was a flicker when the Aurora component was loading, so to avoid that I created a LazyLoad component using Intersection Observer API to lazy load components when they come into the viewport.
"use client";
import { ReactNode, Suspense, lazy } from "react";
const Aurora = lazy(() => import("../components/Aurora"));
interface PageLayoutProps {
children: ReactNode;
}
const PageLayout: React.FC<PageLayoutProps> = ({ children }) => {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<Aurora>{children}</Aurora>
</Suspense>
</div>
);
};
export default PageLayout;
So this solved the flicker issue and also reduced the initial JS bundle size.
Fixing Guestbook component
Before :
I have guestbook component, which was loading a lot of JS and was not required in the initial load of the page.
home/page.tsx
import dynamic from "next/dynamic";
const Guestbook = dynamic(
() => import("@/app/components/Guestbook/guestbook-comp"),
{ ssr: true }
);
export default async function Home() {
// was calling this supabase client directly
const supabase = createClient();
const {
data: { user },
} = await (await supabase).auth.getUser();
return (
<main>
<Guestbook user={user} />
</main>
);
}
guestbook-comp.tsx
interface User {
id: string;
user_metadata: { avatar_url: string; name: string };
}
export default function Guestbook({
user,
}: User | null) {
return (
<div>
{user ? (
<p>Welcome, {user.user_metadata.name}</p>
) : (
<p>Please log in to sign the guestbook.</p>
)}
</div>
)
};
So now I changed the code to load the supabase client dynamically inside a useEffect hook, so that it only loads when the component is mounted on the client side.
Instead of passing the user as a prop from the server side, I fetch the user inside the component itself.
guestbook-comp.tsx
export default function Guestbook() {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
(async () => {
const { supabase } = await import("@/app/supabase/client"); // dynamic import only when needed
// const supabase = createClient();
const { data } = await supabase.auth.getUser();
if (data && data.user) {
console.log("data user >>>", data.user);
setUser({
id: data.user.id,
user_metadata: {
avatar_url: data.user.user_metadata.avatar_url,
name: data.user.user_metadata.name,
},
});
}
})();
}, []);
return (
<div>
{user ? (
<p>Welcome, {user.user_metadata.name}</p>
) : (
<p>Please log in to sign the guestbook.</p>
)}
</div>
);
Now the supabase client is only loaded when the Guestbook component is mounted on the client side, reducing the initial JS bundle size.
Using Intersection Observer for Lazy Loading
So I created a LazyLoad component using Intersection Observer API to lazy load components when they come into the viewport.
"use client";
import { useEffect, useRef, useState } from "react";
interface LazyLoadProps {
children: React.ReactNode;
className?: string;
}
export const LazyLoad = ({ children, className }: LazyLoadProps) => {
const [isVisible, setIsVisible] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{
rootMargin: "50px",
}
);
if (ref.current) {
observer.observe(ref.current);
}
return () => {
observer.disconnect();
};
}, []);
return (
<div ref={ref} className={className}>
{isVisible ? (
children
) : (
<div className="animate-pulse flex space-y-4">
<div className="flex-1 space-y-4 py-1">
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="space-y-2">
<div className="h-4 bg-gray-200 rounded"></div>
<div className="h-4 bg-gray-200 rounded w-5/6"></div>
</div>
</div>
</div>
)}
</div>
);
};
Now I can use this LazyLoad component to wrap any component that I want to lazy load.
import { LazyLoad } from '../components/LazyLoad';
return (
<LazyLoad>
<Guestbook />
</LazyLoad>
);
This will ensure that the Guestbook component is only loaded when it comes into the viewport, further reducing the initial JS bundle size.
Results
This is the screenshot of the JS bundle size after optimization :
After implementing these optimizations, I was able to reduce the JS bundle size significantly, which in turn improved the overall performance of my website.
Lighthouse Report
LCP Improvement
First Test
Second Test
The LCP load time improved from 629ms to 120ms in the dev environment.
LCP score is 121 ms in dev environment.
| Metric | (before) | (after) |
| ----------------- | ------------------- | ------------------ |
| TTFB | 30 ms | 26 ms |
| RLD | 0 ms | 0 ms |
| RLT | 0 ms | 0 ms |
| ERD | 629 ms | 100 ms |
That's an improvement of 84.1% in the Element Render Delay.
Stay Tuned for more performance optimizations
By implementing dynamic imports, lazy loading, and reducing third-party libraries, I was able to significantly improve the JS load time of my website. This not only enhanced the user experience but also positively impacted the SEO ranking of my site.







