š„ Mastering NextJs

Table of contents
- Key Difference Between Next.js and React
- Dynamic Imports
- Understanding Next.js Rendering Strategies ā SSG, CSG, SSG, and ISR
- How to Client-Side-Render a Component in Next.js
- Serverless Functions in Next.js
- You must use middleware like this in Next.js
- Key Considerations for Next.js App Router Files
- When Should I Use Server Action (Next.js 14)
- How I Made a Next.js App Load 10x Faster
- Conclusion
- References

Next.js is a React-based framework that allows you to build server-side-rendered applications with ease. With Next.js, you can create dynamic and fast-loading web pages that are optimized for search engines and social media platforms. Some of the key benefits of using Next.js include:
Automatic code splitting for faster page loads.
Server-side rendering for improved SEO and performance.
Built-in support for CSS modules and styled components.
Easy deployment with Vercel, the platform that was built specifically for Next.js.
Today, Iām going to explain some advanced concepts of Next.js that most developers donāt know. You can use them to optimize your App and improve the Developer experience.
Key Difference Between Next.js and React
Rendering Methods: client-side vs server-side
When we talk about the rendering method in React, React relies mainly on rendering with the client-side rendering (CSR) method. Therefore, both the logic and the structure of the web page will be handled by the browser (client). Though this method is commonly used, it has some downside effects like slower initial page load.
Next.js, on the other hand, supports both SSR and CSR because it was built on top of React. Web pages are rendered on the server, and both the logic and structure of the page are handled by the server. This enables faster page loading and improves SEO.
Performance Considerations
In terms of performance considerations, Next.js is often preferred because it offers multiple rendering options, including server-side rendering (SSR), static site generation (SSG), Incremental static regeneration (ISR), and client-side rendering(CSR). In contrast, React primarily provides a single rendering approach: client-side rendering.
SEO Implications
React is less SEO-friendly because search engines may struggle to index content that requires Javascript to render.
On the other hand, Next.js is more SEO-friendly than React because it renders content on the server, providing search engines with fully rendered content for easier indexing.
Scaleability and Project Complexity
In terms of scalability and project complexity, Next.js is generally better than React. Next.js provides built-in features that enhance the scalability of your project. These include:
Server-side rendering and static site generation for better performance and SEO.
A built-in API route features for creating serverless functions seamlessly.
A file-based routing system that simplifies the organization of larger projects.
In contrast, with React, you are responsible for setting up and maintaining the structure for scalability. For larger projects, this often requires adding additional tools such as:
State management libraries (for example, Redux, Recoil, and so on).
Routing libraries (for example, React Router).
These tools are necessary to enhance Reactās scalability and address project complexity, but they also increase the overhead and effort needed to set up and manage the application.
In summary, here is a tabular breakdown:
Factors | React | Next.js |
Scalability | It is possible, but to increase scalability, it requires additional tools and a custom setup | It is scalable and already has built-in tools that increase the scalability. |
Performance | It provides only one rendering option, which is client-side rendering (CSR) | It offers multiple rendering options, including SSR, SSG, ISR, and CSR. |
SEO | It is less SEO-friendly because search engines may struggle to index content that requires JavaScript execution to render. | It is more SEO-friendly than React because it renders content on the server, providing fully-rendered HTML to search engines for easier indexing. |
Use Case | Mostly used in smaller or unique projects | Mostly used in large-scale projects and enhances performance and SEO |
Dynamic Imports
Dynamic imports are a modern Javascript feature that enables developers to split the code into smaller bundles, permitting the loading of various parts (chunks) of your application on demand. This feature can significantly improve your app performance by reducing load time, especially for large and complex applications.
import dynamic from 'next/dynamic';
// Dynamically import the component
const DynamicComponent = dynamic(
() => import('../components/DynamicComponent'),
{
ssr: false, // Disable server-side rendering for this component
}
);
const PageComponent = () => (
<div>
<p>This is a static content</p>
<DynamicComponent />
</div>
);
export default PageComponent;
In the above example, the DynamicComponent
will only be loaded when the PageComponent
is rendered, thus significantly decreasing the initial loading time for your application.
Understanding Next.js Rendering Strategies ā SSG, CSG, SSG, and ISR
One of the advantages of using Next.js is the versatility in regard to how it builds the page on the userās browser. If you can comprehend how these strategies work, you will have an easier time building a faster and more efficient site.
Next.js has four rendering strategies:
Server-Side Rendering (SSR)
Client-Side Rendering (CSR)
Static-Site Generation (SSG)
Incremental-Statis Regeneration (ISR)
I will explain each strategy, including the process behind the scenes, these use cases, and the pros and cons.
Server-side Rendering
How it works:
The Js file is sent to the server
When the user (browser) requests, the server will run the
getServerSideProps()
function (Next 12) orfetch(āhttps://...', { cache: āno-storeā });
(Next 13)After the data is fetched, it will be built on the server (including the data from the API)
The server will send the HTML to the userās browser.
Use cases:
if your site data is updated frequently
At the same time, SEO is an essential factor as well
Pros
No loading
Real-time data
Good for SEO
It can be used for personalized content
Cons
The user needs to wait for the HTML to be built on the server side
Too much burden on the server => every userās request, the server needs to be rebuilt.
Client-side Rendering
How it works
Build Process => The HTML is sent to the server
When the user requests, the server sends the HTML file, and then the client (browser) requests the data from the API server.
At the time the client is requesting the data from the API server, the browser will display the loading state (in most cases)
After the data is fetched from the API server, the loading state will be turned off, and the screen will be updated with the data from the API
Use case:
if your siteās data is updated frequently.
SEO is not an essential factor.
Pros
Real-time data
It can be used to personalize content
The burden is not too big for the server
Cons
There will be a loading state
Not good for SEO => Since HTML is built from the server, it does not include the data from the API
Static-Site Generation (SSG)
How it works:
The build and fetch data processes happen at the same time =>
getStaticProps()
function (Next 12) orfetch()
function (itās set as default in Next 13) is running at build timeAfter the HTML + JSON is built (the data from API is included), it will be sent to the server.
When the user (browser) requests, the server will send the HTML + JSON file, so the user doesn't need to wait (no loading).
Use case:
if your site data is definite (fetch one, and thatās it)
Example: site for Al-Qurāan, logs/history data, or old archived documents ā which wonāt be changed no matter what
Pros
Overall, the fastest method
No loading
The burden is not too big for the server
Good for SEO
Cons
Donāt have the trigger to update the data from API(fetch once, and thatās it) unless itās redeployed
It canāt be used as personalized content => because this method doesn't have any way to update the built HTML file from the server (so whenever the user requests, it will remain the same)
Incremental Static Regeneration (ISR)
How it works:
Pretty much the same with SSG, but with the capability to update the data
If you set the revalidated data on the
getStaticProps()
function (Next 12) orfetch(ā
https://.../data', { next: { rev
alidate: 10 } });
(Next 13), The server will revalidate the data from the API and see whether there is any change.If there is any update from the API (DB), then the built HTML file will be updated (the new one will override the current one)
But it will only be updated after the set time in revalidate props time is passed
Example: If you setrevalidate: 10
=> It will do the revalidation and update the HTML after 10 seconds have passed.
Use case:
- Best practice for most static sites that donāt need a real-time update
Pros
- Basically, itās SSG (fast + good for SEO!) but with an additional tweak to be able to update the siteās data
Cons
It canāt be used as personalized content.
It canāt be used if you have a real-time feature on your site.
How to Client-Side-Render a Component in Next.js
Whether something in Next.js is client-side or server-side rendered can be easily determined. We will work with this simple component:
<>
<p>Hello World!</p>
</>
Using Next.js default server-side-rendering, the generated HTML looks like this:
You can see the componentās XML is rendered on the server.
Meanwhile, using client-side-rendering, the initial HTML response looks like this:
Of course, the āHello World!ā still appears in the DOM, but it takes some time because it is rendered through client-side Javascript.
Here are 3 ways to achieve CSR in Next.js:
Method 1: Timing with useEffect
If you use Next.jsās new App Router, every component is a server component by default and canāt use React hooks. Therefore, we declare it as a client component on top by stating āuse clientā
.
'use client'
import { useEffect, useState } from 'react'
export default function Index() {
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
setIsMounted(true)
}, [])
if (!isMounted) {
return <p>loading...</p>
}
return (
<>
<p>Hello World!</p>
</>
)
}
Method 2: Dynamic components
Using a dynamic import, a component can also be rendered only on the client side. The reason is simple: The component is imported once the wrapping component is rendered ā therefore, all the work is happening on the client.
import dynamic from 'next/dynamic'
const HelloWorld = dynamic(() => import('../components/HelloWorld'), {
ssr: false,
})
export default function Index() {
return <HelloWorld />
}
This approach works both for clients and for server-side components.
Method 3: Use the window object (Only Pages Router)
Hint: This approach doesnāt work in the new App Router
This trick is so simple: When we render something on the server, the window object isnāt available in our code. Why? well, because the window object is exclusive to the browser, of course.
Since we can access this special object in Next.js, we check if we are on the server or not. Therefore, we can save if server-side rendering is happening in a variable like this:
const SSR = typeof window === 'undefined'
SSR is true if server-side-rendering of our JSX is happening. To only client-side render something, use this variable:
const SSR = typeof window == 'undefined'
export default function Index() {
return <>{!SSR ? <p>Hello World!</p> : null}</>
}
Serverless Functions in Next.js
Serverless functions are a great way to implement backend functionality without managing a server. Next.js provides built-in support for serverless functions using the API directory.
To create a serverless function in Next.js, create a file in the pages/api
directory with the following format.
// pages/api/hello.js
export default function handler(req, res) {
res.status(200).json({ message: 'Hello, world!' });
}
This function will respond to requests to /api/hello
with a JSON message.
You can also use serverless functions to handle data from forms or other input fields. Hereās an example of a serverless function that handles a form submission:
// pages/api/contact.js
import nodemailer from 'nodemailer';
export default function handler(req, res) {
const { name, email, message } = req.body;
// Send email using nodemailer
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
user: 'your_email@gmail.com',
pass: 'your_password',
},
});
const mailOptions = {
from: email,
to: 'recipient_email@example.com',
subject: `New message from ${name}`,
text: message,
};
transporter.sendMail(mailOptions, (error, info) => {
if (error) {
console.log(error);
res.status(500).send('Error sending message');
} else {
console.log(`Email sent: ${info.response}`);
res.status(200).send('Message sent');
}
});
}
In this example, we are using a nodemailer
library to send an email with the form data. This serverless function can be triggered by a form submission in your application.
You must use middleware like this in Next.js
Middleware in Next.js is often overlooked, but itās a game-changer once you understand its potential. If you are not using it yet, you might be missing out on one of the most powerful tools Next.js has to offer.
What is middleware in Next.js?
Letās break down the essentials of middleware in Next.js:
Itās simply a function - at its core, middleware is just a function
Executed on the edge - middleware runs on the edge, closer to the user
Runs for specified pages - you decide which pages it affects through middlewareās configuration.
Executes before page load - middleware runs before the user receives the page.
Takes in the request - It accepts the pageās GET request object as a parameter.
Middleware doesnāt impact the rendering pattern, and most importantly, it doesn't interfere with the pageās rendering approach.
This image perfectly illustrates the concept of middleware from a networking perspective.
The problem
Imagine you have a āMy Profileā page that should only be accessible to authenticated users. If someone isnāt logged in, we want to redirect them to the Sign-In page.
How do we handle this? Checking for a token in the local storage or a NextAuth session on the client side might seem straightforward, but hereās the catch:
User experience suffers ā An authenticated user has to wait for the page to load and the client-side code to execute before being redirected. During this delay, they see a loading state, which is less than an idea. Even authenticated users end up seeing this loading state unnecessarily, leading to a frustrating user experience.
Bundling and efficiency issues ā Using client-side checks means adding extra codes to all private pages. This approach can work in simple applications but becomes inefficient if additional libraries are required, resulting in a larger bundle size.
Alternatively, you might consider handling the session check on the server side. While this can streamline the process, it forces every protected page to become dynamic, which presents a challenge if that is not your goal.
Now, imagine if there was a way to detect user authentication as soon as they request the page and redirect them to the sign-in page without impacting the rendering pattern. Thatās where an efficient solution comes into play.
Once I discovered the power of middleware, I immediately integrated it into my project. But before diving into the code, let me clarify a few key points. The NextAuth library conveniently places all relevant information from the token directly into the req
object, allowing us to access it effortlessly via req.nextauth.token
. This means I can extract not only authentication details but also user roles and authentication statuses.
Now, letās take a look at how this all comes together:
import { withAuth } from "next-auth/middleware";
const authRoutes = [AppRoutes.signIn, AppRoutes.signUp, AppRoutes.forgotPassword]
export default withAuth(
async function middleware(req) {
const user = req.nextauth.token?.user
const isSigned = Boolean(user)
const isResetPasswordRoute = req.nextUrl.pathname.startsWith(
AppRoutes.resetPassword("")
)
const isAuthRoute = authRoutes.includes(req.nextUrl.pathname) || isResetPasswordRoute
const isAdminRoute = req.nextUrl.pathname.startsWith("/admin")
const isSubscriptionPlansRoute = req.nextUrl.pathname === AppRoutes.subscriptionPlans
const isAdminUser = req.nextauth.token?.user.role === USER_ROLE.ADMIN
const accountStatus = req.nextauth.token?.user.accountStatus ?? ""
const isPaid = accountStatusesWithAppAccess.includes(accountStatus) || isAdminUser
const hasSubscription =
accountStatusesThatHasSubscription.includes(accountStatus) || isAdminUser
const isMustGoPlansRoute = !hasSubscription && isSigned
const isRenewSubscriptionRoute = req.nextUrl.pathname === AppRoutes.renewSubscription
const mustGoRenewSubscription = hasSubscription && !isPaid && isSigned
// if user have trial or subscription, but by some reason it is paused then redirect to renew it
if (mustGoRenewSubscription && !isRenewSubscriptionRoute) {
return NextResponse.redirect(new URL(AppRoutes.renewSubscription, req.url))
}
// if user shouldn't go to renew subscription, then restrict access to it
if (!mustGoRenewSubscription && isRenewSubscriptionRoute) {
const redirectRoute = isSigned ? AppRoutes.main : AppRoutes.signIn
return NextResponse.redirect(new URL(redirectRoute, req.url))
}
// if user signed and has no subscription or trial then he must go and choose plan
if (!isSubscriptionPlansRoute && isMustGoPlansRoute) {
return NextResponse.redirect(new URL(AppRoutes.subscriptionPlans, req.url))
}
// if user shouldn't go to chose plan, then restrict access to it
if (isSubscriptionPlansRoute && !isMustGoPlansRoute) {
const redirectRoute = isSigned ? AppRoutes.main : AppRoutes.signIn
return NextResponse.redirect(new URL(redirectRoute, req.url))
}
// if user signed and paid and trying to go auth route redirect main
if (isAuthRoute && isSigned && isPaid) {
return NextResponse.redirect(new URL(AppRoutes.main, req.url))
}
// if not signed user trying to go to protected route redirect sign-in
if (!isSigned && !isAuthRoute) {
return NextResponse.redirect(new URL(AppRoutes.signIn, req.url))
}
// if user is not admin and trying to go admin route redirect main or sign-in
if (isAdminRoute && !isAdminUser) {
const redirectRoute = isSigned ? AppRoutes.main : AppRoutes.signIn
return NextResponse.redirect(new URL(redirectRoute, req.url))
}
},
{
secret: envServer.NEXTAUTH_SECRET,
callbacks: {
authorized: () => true,
},
}
)
Uh-huh, functions seem a bit too large, doesn't it?
Iām well aware of that, and I recognize the need for improvement. Inspired by a solution I encountered in a Vue.js project, I set out to create something similar.
Advanced Approach
I was thinking about the most convenient way for me to handle whether the user can access the route or not. I have the same condition for several routes, but sometimes routes have the same condition as another one but with one more condition. Also, some conditions have more priority than others, so we need to handle this too.
Would it be great to define conditions for each route separately? Letās try to do this.
First of all, letās create the desired structure. I prefer an array of objects with routes and conditions, something like this:
[
{route: "/onboarding", condition: (token: JWT, url: string) => {}},
{route: "/profile", condition: (token: JWT, url: string) => {}}
]
Ok, it works, but what should this condition return?
In case the condition needs to redirect the user, then it must return a redirect.
Otherwise, let's return just null.
[
{
route: "/onboarding",
condition: (token: JWT, url: string) => {
if (!token?.user) return null;
return NextResponse.redirect(new URL("/sign-in", url))
}
},
{
route: "/profile",
condition: (token: JWT, url: string) => {
if (!token?.user) return null;
return NextResponse.redirect(new URL("/sign-in", url))
}
},
]
Great! It works, but we have 2 issues here.
How are we gonna run it?
The same condition is duplicated
Letās start with the first one.
import type { NextRequestWithAuth } from "next-auth/middleware";
export type RouteConfig = {
condition: (token: JWT) => NextResponse<any> | null;
url: string;
};
export function runRoutesMiddleware(
req: NextRequestWithAuth,
config: RouteConfig[]
): NextResponse<any> | null {
const currentRouteConfig = config.find((route) =>
matchPath(route?.url, props?.nextUrl)
);
if (!currentRouteConfig) return null;
return currentRouteConfig.condition(req.token);
}
export default withAuth(
async function middleware(req) {
return runRoutesMiddleware(req, routesRulesConfig);
},
{
secret: env.NEXTAUTH_SECRET,
callbacks: {
authorized: () => true,
},
}
);
Here, I use a small hack called āmatchPathā. Itās the same function that we have in the react-router. but returns a boolean if the URL matches the pattern. matchPath(ā/user/:idā, āuser/123ā) -> true.
Letās solve the duplicated condition issue by separating the condition from the config. Also, we need to have some reusable functions that will allow us to create complex functions from small pieces. Let's call this small function āRuleā. It will have the same type we saw before for condition.
type Rule = (token: JWT) => NextResponse<any> | null;
We need some functions to call these rules until it gets a response from one of them. Here it is.
export function executeRules(
rules: Rule[],
url: string,
token: JWT,
ruleIndex: number = 0
): ReturnType<Rule> | void {
if (ruleIndex > (rules?.length || 0) - 1) return;
const result = rules?.[ruleIndex]?.(token, url);
if (!result) {
return executeRules(rules, url, token, ruleIndex + 1);
} else {
return result;
}
}
And adjust our runRoutesMiddleware
function
export function runRoutesMiddleware(
req: NextRequestWithAuth,
config: RouteConfig[]
): NextResponse<any> | void {
const currentRouteConfig = config.find((route) =>
matchPath(route?.url, props?.nextUrl)
);
if (!currentRouteConfig) return;
return executeRules(
currentRouteConfig?.rules,
req.url,
req.nextauth.token
);
}
Letās create our rule
export const isAuthenticatedRule: Rule = (token, url) => {
if (!token?.user) return null;
return NextResponse.redirect(new URL("/sign-in", url))
}
Then use it in our config
[
{
route: "/onboarding",
rules: [isAuthenticatedRule]
},
{
route: "/profile",
rules: [isAuthenticatedRule]
},
]
Thatās it! Now we have a very simple flow for creation, editing, deletion and reprioritisation of our rules for every page. Sounds good, doesnāt it?
Key Considerations for Next.js App Router Files
The Next.js App Router, introduced in Next.js App 13, represents a shift from the previous pages directory approach. It uses folders in the app directory to define routes.
This new system supports layout, loading states, and error handling at the route level. It also allows the seamless mixing of client and server components, offering improved performance and a more flexible development experience for the first-load approach.
Now, letās dive into each of these special files by checking out this article.
When Should I Use Server Action (Next.js 14)
I started experimenting with the new Next Server Actions, I quickly learned to love it. They felt like a clean and efficient solution for most of my server requests.
But as I started to play with it and implement it more, I started noticing I was introducing performance issues, unnecessary roundtrips between the client and the server, and even some dangerous API security issues.
I quickly learned that Server Actions werenāt always the most efficient choice, especially for straightforward data fetching.
So, letās dive in. In the next few paragraphs, I will explain the different use cases for Server Actions and show you how they can be a game changer in your Next.js app, especially when used in the right context.
What are Server Actions
Server Actions (soon to be named Server Functions in React 19) are a relatively new feature introduced by React to simplify data handling and move more logic to the server side. Next.js has quickly incorporated its own twist in Next.js 14, offering good complementarity with Server-Side Components.
In the following lines, we will focus only on the Next.js implementation:
When to use Server Actions
Event handling / Performing Database Mutations
Server actions allow you to perform server operations and database mutations securely without exposing database logic or credentials to the client. They drastically reduce and simplify your code because they remove the need to write a specific API route for your operations.
'use server'; export async function handleServerEvent(eventData: any) { // Process any server event const res = await someAsyncOperation(eventData); if (!res.ok) { throw new Error('Failed to handle event'); } return { res, message: 'Server event handled successfully' }; }
Handling form submissions
Similar to the first point, Server Actions are particularly useful for processing form inputs that need server-side handling. They provide a straightforward way to handle form data on the server, ensuring data validation and integrity without exposing the logic to the client and without having to implement elaborate API endpoints.
'use server'; export async function handleFormSubmit(formData: FormData) { const name = formData.get('name') as string; const email = formData.get('email') as string; const message = formData.get('message') as string; // Process the form data const res = await saveToDatabase({ name, email, message }); if (!res.ok) { throw new Error('Failed to process the form data'); } return { res, message: 'Form submitted successfully' }; }
Fetching data from client components
Server Actions can also be useful for quick data fetching, where a clean developer experience (DX) is crucial. It can simplify the fetch process by typing data access directly to a component without the need for intermediary API layers. Moreover, when using Typescript, Server Actions make using types seamless because everything is within the same function boundary.
// Simple server action to fetch data from an API 'use server'; export async function fetchData() { const res = await fetch('https://api.example.com/data'); if (!res.ok) { throw new Error('Failed to fetch data'); } return res.json(); }
Working with Next.js and its server component, you already have this very practical way of using server-side code to fetch data and pre-render the page on the server.
But Server Action now introduces a brand new way to also do that from your client-side components! It can simplify the fetch process by typing data access directly to the component that needs it, without the need to use
useEffects
hooks or client-side data fetching libraries.Moreover, when using Typescript, Server Actions make typing seamless because everything is within the same function boundary, providing a great developer experience overall.
Potential Pitfalls with Server Actions
Donāt use server actions from your server-side components
The simplicity and great DX of Server Actions could make it tempting to use them everywhere, including from a server-side component, and it would work! However, it doesn't really make any sense. Indeed, since your code is already running on the server, you already have the means to fetch anything you need and provide it as props to your page. Using Server Actions here would delay data availability as it causes extra network requests.
For client-side fetching, Server Actions might also not be the best option. First of all, they always automatically use POST requests, and they can not be cached automatically like a GET request. Secondly, if your app needs advanced client-side caching and state management, using tools like TanStack Query (React Query) or SWR is going to be way more effective for that. However, I havenāt tested it myself yet, but itās apparently possible to combine both and use TanStack Query to call your server actions directly.
Server Actions Do Not Hide Requests
Be extremely careful when using server actions for sensitive data. Server Actions do not hide or secure your API requests. Even though Server Actions handle server-side logic, under the hood, they are just another API route, and POST requests are handled automatically by Next.js.
Anyone can replicate them by using a Rest Client, making it essential to validate each request and authenticate users appropriately. If there is sensitive logic involved, ensure you have
proper authentication and authorization checks
within your Server Actions.Note: Additionally, consider using the very popular next-safe-actions package, which can help secure your actions and also provide type safety.
Every Action Adds Server Load
Using Server Actions might feel convenient, but every action comes at a cost. The more you offload onto the server, the greater the demand for server resources. You may inadvertently increase your appās latency and cloud cost by using Server Actions when client-side processing would suffice. Lightweight operations that could easily run on the client, like formatting dates, sorting data, or managing small UI state transitions, should stay on the client side to keep your server load minimal.
Classic API Routes Might Be More Appropriate
There are cases when sticking with traditional API routes makes more sense, particularly when you need your API to be accessible to multiple clients. Imagine if you need the same logic for both your web app and a mobile app, duplicating the same Server Action logic into an API route will only double the work and maintenance. In these situations, having a centralized API route that all clients can call is a better solution, as it avoids redundancy and ensures consistency across your different clients.
Next.js Dependency and the Moving Target
Itās important to note that Server Actions are closely integrated with Next.js, and both Next.js and React are evolving rapidly. This pace of development can introduce compatibility issues or breaking changes as these frameworks continue to update. If your application prioritizes stability and long-term support, relying heavily on cutting-edge features like Server Actions could result in unwanted technical debt. Weighing the stability of traditional, well-established methods against the appeal of new features is always advisable.
How I Made a Next.js App Load 10x Faster
Ditch Useless useEffect
Calls š«
Letās talk about useEffect()
ā the go-to hammer for devs who see every problem as a nail.
I had components using useEffect()
for things that could be done statically or with server-side rendering.
Removing unnecessary useEffect
calls instantly reduced my client-side Javascript loads.
The Wrong Way
This forces the client to fetch the data after the page loads. Slow and unnecessary.
The Right Way (Server Components FTW)
No useEffect()
, no state management, just pure server-side rendering (SSR). The page loads with data pre-fetched.
Use Image Optimization (next/image
) š·
If you are still using <img>
tags like itās 2010, you are missing out.
I had multiple large images slowing down my page.
Switching to Next.jsās next/image
reduced my Largest Contentful Paint (LCP).
The Old Way (Bad Performance)
This loads the full image, even if the user is on a small screen. Wasteful
The Next.js Way
This automatically:
Optimizes image size
Serves WebP format when possible
Lazy-loads non-critical images
One area of confusion is how to handle images in Next.js. The process differs depending on whether you are working with local or remote images.
For local images, you donāt need to specify a width and height. Next.js will automatically identify the dimensions. Simply import the image and render it using the next/img
component.
import Image from "next/image";
import localImage from "public/hoy.png";
export default function MyComponent() {
return <Image src={localImage} alt="Local Image" />;
}
For remote images, you need to provide a blur placeholder and specify the width and height to prevent layout shifts. You can use the placeholder=blur
prop to show a blurred version of the image until the full image loads.
To generate the blur data URL for remote images, you can use the sharp
and placeholder
packages:
import Image from "next/image";
import getBase64 from "./utils/getBase64";
export default async function MyComponent() {
const blurDataUrl = await getBase64(remoteImageUrl);
return (
<Image
src={remoteImageUrl}
width={600}
height={600}
alt="Remote Image"
placeholder="blur"
blurDataURL={blurDataUrl}
/>
);
}
The getBase64
utility function fetches the remote image, converts it to an ArrayBuffer, and then generates the base64 representation using the placeholder
package.
Reduce Javascript (Move Logic to the Server)
Every extra KB of Javascript adds execution time on the client.
The tricks.
Offload work to the backend or Edge functions.
I used to process data on the client, which was dumb because the same logic could run on the server because sending the response.
Bad: Processing on the Client
This means fetching everything first and then filtering on the client. Unnecessary load.
Good: Processing on the Server
Now, only relevant data reaches the client.
Less Javascript = faster rendering
Use Static Generation When Possible (getStaticProps
)
If your data doesn't change often, donāt fetch it dynamically.
Instead, pre-generate pages at build time using getStaticProps()
.
export async function getStaticProps() {
const res = await fetch("https://api.example.com/posts");
const posts = await res.json();
return { props: { posts }, revalidate: 60 }; // Refresh every 60s
}
export default function Blog({ posts }) {
return <div>{posts.map((p) => <p key={p.id}>{p.title}</p>)}</div>;
}
This generates HTML ahead of time, meaning zero wait time when users visit the page.
Optimize Fonts & Reduce CLS (Cumulative Layout Shift)
Google hates layout shifts.
If your font loads late, users will see a āflash of invisible textā (FOIT).
I fixed this by:
Using system fonts (
sans-serif
,serif
)Preloading custom fonts
Setting
font-display: swap
This prevents text from ājumpingā when fonts load. Smooth experience.
Conclusion
In this article, we have covered several advanced Next.js concepts. By mastering these concepts, you will be able to build powerful, performant web applications with Next.js. Whether you are building a small blog or a large-scale e-commerce platform, Next.js has the tools and features you need to deliver a seamless user experience.
References
https://www.freecodecamp.org/news/nextjs-vs-react-differences/
https://javascript.plainenglish.io/next-js-client-side-rendering-56a3cae65148
https://blog.devgenius.io/advanced-next-js-concepts-8439a8752597
https://blog.stackademic.com/you-must-use-middleware-like-this-in-next-js-64d59bb4cd59
https://yohanlb.hashnode.dev/when-should-i-use-server-action-nextjs-14?ref=dailydev
Subscribe to my newsletter
Read articles from Tuan Tran Van directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Tuan Tran Van
Tuan Tran Van
I am a developer creating open-source projects and writing about web development, side projects, and productivity.