LangChain in Action: How to Build Intelligent AI Applications Easily and Efficiently ?
React Best Practices - Part 5

This article explores advanced React Best Practices to enhance scalability, performance, and maintainability in modern web applications. It covers techniques like Next.js optimizations, comparisons with Vanilla JavaScript, and strategies for building efficient, SEO-friendly, and dynamic React projects.
NextJS
Next.js, built on top of React, brings several performance-boosting features that make it a powerful framework for building modern web applications. Here’s what Next.js brings to the table to improve the performance of React:
- Server-Side Rendering (SSR): Renders React components on the server and sends fully-rendered HTML to the client. Faster initial load times, especially for large apps or pages with dynamic content. Better SEO because search engines can index pre-rendered HTML.
Best for: Dynamic content that needs to be updated on every request (e.g., dashboards, e-commerce). - Static Site Generation (SSG): Pre-renders pages at build time, creating static HTML files. Lightning-fast load times as the HTML is served directly from a CDN. Reduces server load since the content is pre-generated.
Best for: For pages with content that doesn’t change often, like blogs, marketing pages, or documentation. - Incremental Static Regeneration (ISR): Combines SSG with dynamic revalidation, regenerating static pages at runtime without rebuilding the whole site. Ensures content freshness while maintaining the speed of static pages. Reduces server strain by regenerating only specific pages.
Best for: For applications with frequently updated content, like product catalogs or news sites. - API Routes: Provides built-in serverless functions to create APIs directly within the Next.js app. Eliminates the need for a separate backend, reducing latency. Runs in a serverless environment for scalability and cost-efficiency.
Best for: Lightweight data handling, like forms or dynamic queries. - Automatic Code Splitting: Automatically splits JavaScript bundles by route. Only loads the code needed for the current page, reducing the initial bundle size. Improves perceived performance by avoiding overloading the browser with unnecessary scripts.
- Optimized Image Handling: Built-in next/image component optimizes images with lazy loading, responsive sizes, and WebP support. Improves page load times by only loading images as needed. Reduces bandwidth usage with efficient image formats.
Best for: On any site with images. - React Server Components (RSC): Fetches and renders data on the server, sending lightweight HTML to the client. Reduces JavaScript sent to the client, improving load times. Makes hydration faster as fewer scripts need execution.
Best for: On large applications with heavy data fetching requirements. - Automatic Static Optimization: Automatically detects pages that can be statically generated and serves them as static files. No configuration needed to get static page benefits. Optimizes mixed environments with both static and dynamic pages.
Best for: - Edge Functions: Runs server-side logic at the edge, closer to users, for faster responses. Reduces latency for users by processing logic at a nearby CDN edge location.
Best for: Personalization, A/B testing, or geo-specific content delivery. - Built-In Analytics: Provides insights into performance metrics like Largest Contentful Paint (LCP) and Total Blocking Time (TBT). Identifies bottlenecks and helps optimize critical paths.
Best for: Continuous performance monitoring and optimization.
Next.js enhances React’s performance through features like SSR, SSG, ISR, code splitting, and image optimization. These tools help deliver faster, more efficient applications while reducing developer overhead. Incorporating Next.js into your React projects ensures both improved user experience and scalability.
VanilaJS vs React/NextJS
The performance comparison between Vanilla JavaScript and React + Next.js depends on the specific use case, complexity of the application, and development goals. Here’s a detailed breakdown:
Initial Load Time
Initial Load Time Faster initial load for simple applications due to no framework overhead. Direct DOM manipulation avoids extra abstractions. React + Next.js Slightly slower for basic apps because of React's virtual DOM and Next.js's pre-rendering processes. Next.js with SSG or ISR can match or outperform Vanilla JS by pre-rendering pages and serving them from a CDN, resulting in lightning-fast static content delivery.
Vanilla JavaScript
Requires manual coding to fetch and render dynamic content. Efficient but can become cumbersome and error-prone as complexity increases. React + Next.js Built-in tools for SSR (Server-Side Rendering), ISR (Incremental Static Regeneration), and CSR (Client-Side Rendering) handle dynamic content efficiently. Improves perceived performance by preloading, lazy loading, and batching updates.
Scalability
Vanilla JavaScript Performance drops significantly as the application grows in complexity, making manual DOM updates harder to manage. React + Next.js Scales better for large apps by offering reusable components, state management, and optimized rendering techniques (e.g., React Server Components).
Developer Productivity
Vanilla JavaScript Suitable for small, simple projects but becomes increasingly difficult to maintain as complexity grows. Manual management of state, events, and updates can slow down development. React + Next.js Offers tools, abstractions, and conventions that speed up development and reduce boilerplate code. Features like API Routes, automatic code splitting, and React's state management streamline development for medium-to-large apps.
SEO and Accessibility
Vanilla JavaScript SEO requires manual handling (e.g., ensuring content is rendered server-side or pre-rendered). Accessibility and semantic HTML require meticulous attention to detail. React + Next.js Next.js provides SEO-friendly features like SSG and SSR, making it easy to deliver fully-rendered pages to search engines. Handles routing and meta tag updates seamlessly for better accessibility and discoverability.
Use Cases
When to Use Vanilla JavaScript Small, Static Websites: Landing pages, portfolios, or informational sites with minimal interactivity. Performance-Critical Apps: When every millisecond counts, and there’s no need for complex state management or advanced rendering techniques. Learning or Prototyping: Ideal for beginners learning core web technologies. When to Use React + Next.js Dynamic, Interactive Applications: Dashboards, e-commerce sites, social media platforms, or any app requiring frequent state changes. SEO and Content Updates: Blogs, news websites, or any content-driven site that benefits from ISR or SSG for performance and SEO. Scalable, Maintainable Projects: Apps expected to grow in complexity, requiring reusable components and optimized rendering.
Final Verdict
Vanilla JavaScript performs better for small, static, or very simple applications due to its minimal overhead.
React + Next.js excels in dynamic, scalable, or SEO-critical applications by leveraging powerful features like SSG, SSR, and ISR to balance performance and development efficiency.
When to Choose Each
Use Case | Choose Vanilla JS | Choose React + Next.js |
---|---|---|
Small static websites | ✅ | ❌ |
Performance-critical, minimal apps | ✅ | ❌ |
Large-scale, dynamic applications | ❌ | ✅ |
SEO and frequently updated content | ❌ | ✅ |
Scalable, reusable component systems | ❌ | ✅ |
Example of Web Component
class FetchTestComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
// Attach a static template to the shadow DOM
this.shadowRoot.innerHTML = `
<style>
.data {
font-family: Arial, sans-serif;
margin: 10px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
.loading {
color: gray;
}
</style>
<div id="content" class="loading">Loading...</div>
<template id="data-template">
<div class="data">
<h2 class="title"></h2>
</div>
</template>
`;
}
connectedCallback() {
this.fetchData();
}
async fetchData() {
const content = this.shadowRoot.getElementById("content");
try {
const response = await fetch("https://jsonplaceholder.typicode.com/todos/1");
if (!response.ok) {
throw new Error("Network response was not ok");
}
const data = await response.json();
// Clear the loading text
content.textContent = "";
// Clone the template and populate it with data
const template = this.shadowRoot.getElementById("data-template");
const clone = template.content.cloneNode(true);
clone.querySelector(".title").textContent = data.title;
// Append the populated template to the content
content.appendChild(clone);
} catch (error) {
content.textContent = `Failed to fetch data: ${error.message}`;
}
}
}
customElements.define("fetch-test", FetchTestComponent);
This web component, written in vanilla JavaScript, demonstrates a modern approach to dynamically fetching and rendering data using the Shadow DOM and <template> element. Instead of relying on innerHTML, it utilizes the <template> tag to define a reusable structure for the data, ensuring clean, secure, and efficient DOM manipulation.
Key Features:- Vanilla JavaScript: No external frameworks or libraries, showcasing the power of native web technologies.
- Shadow DOM: Encapsulates the component's styles and structure, isolating them from the rest of the document for better maintainability and avoiding style conflicts.
- Template Usage: Uses the <template> tag to define a reusable, lightweight structure that is dynamically cloned and populated with data.
- Improved Performance: Using <template> avoids reconstructing the DOM with innerHTML, making updates more efficient.
- Enhanced Security: Eliminates the risk of XSS attacks that can arise from interpolated HTML strings.
- Cleaner Code: Separates the static structure (defined in <template>) from dynamic logic (handled in JavaScript), making the component easier to read and maintain.
- Reusability: The <template> can be cloned multiple times without redefining its structure, improving scalability.
Tree shaking
In React (and JavaScript in general), any component or function that has side effects, even if it is not directly used, will still be included in the final JavaScript bundle. This happens because modern bundlers (like Webpack or ESBuild) analyze imports but often cannot tree-shake side-effectful code.
// Unused Component File: HeavyComponent.js
import React, { useEffect } from "react";
console.log("⚡ Side effect: Heavy component is included!");
const HeavyComponent = () => {
useEffect(() => {
console.log("📢 This effect runs even if the component is not rendered!");
}, []);
return <div>Heavy Component</div>;
};
export default HeavyComponent;
===================================================
// App.js
import React from "react";
import "./HeavyComponent"; // 🔥 Unused but still included in the bundle
const App = () => {
return <h1>Hello, React!</h1>;
};
export default App;
Side effects occur when a function modifies anything outside its local scope, such as the DOM, network requests, state mutations, or global variables. Below is a complete list of side-effect features in React and JavaScript.
Feature | React | JavaScript |
---|---|---|
useEffect Hook | ✅ Yes | ❌ No |
useLayoutEffect | ✅ Yes | ❌ No |
useRef with DOM Manipulation | ✅ Yes | ❌ No |
Event Listeners (addEventListener) | ✅ Yes | ✅ Yes |
DOM Manipulation | ✅ Yes | ✅ Yes |
Fetching Data | ✅ Yes | ✅ Yes |
setTimeout / setInterval | ✅ Yes | ✅ Yes |
Global State Change | ✅ Yes | ✅ Yes |
localStorage / sessionStorage | ✅ Yes | ✅ Yes |
console.log | ✅ Yes | ✅ Yes |
Context API / Global State | ❌ No | ✅ Yes |
Modifying Global Variables | ❌ No | ✅ Yes |
Changing window.location | ❌ No | ✅ Yes |
Modifying Objects/Arrays Mutably | ❌ No | ✅ Yes |
WebSockets (socket.io) | ❌ No | ✅ Yes |
alert() / confirm() | ❌ No | ✅ Yes |
Dynamic Imports & Lazy Loading Still in Bundle (Tree Shaking Issue)
📌 Tree shaking removes unused code, but some cases prevent this, such as dynamic imports (import()/lazy loading).
Bad Example:
import React, { useState, lazy, Suspense } from "react";
const App = () => {
const [show, setShow] = useState(false);
return (
<div>
<h1>React Tree Shaking Issue</h1>
<button onClick={() => setShow(true)}>Load Component</button>
<Suspense fallback={<p>Loading...</p>}>
{show && lazy(() => import("./HeavyComponent"))}
</Suspense>
</div>
);
};
export default App;
Since HeavyComponent is not always used, tree shaking should remove it from the final bundle until needed.
Actual Behavior (Included in Bundle): Even if the button is never clicked, HeavyComponent might still be included in the bundle due to: Dynamic Imports Still Being Analyzed – Webpack cannot safely remove the import() call. Side Effects in the Imported File – If HeavyComponent.tsx has side effects (console.log() or global variable changes), it prevents tree shaking.
Good Example:
import React, { useState, lazy, Suspense, useEffect } from "react";
// ✅ Move dynamic import outside (helps tree shaking)
const loadHeavyComponent = () => import("./HeavyComponent");
const LazyHeavyComponent = lazy(loadHeavyComponent);
const App = () => {
const [show, setShow] = useState(false);
const [Component, setComponent] = useState<React.ComponentType | null>(null);
// ✅ Load component once when `show` becomes true
useEffect(() => {
if (show && !Component) {
loadHeavyComponent().then((mod) => setComponent(() => mod.default));
}
}, [show, Component]);
return (
<div>
<h1>React Tree Shaking Optimization</h1>
<button onClick={() => setShow(true)}>Load Component</button>
<Suspense fallback={<p>Loading...</p>}>
{show && Component && <Component />}
</Suspense>
</div>
);
};
export default App;
In conclusion, avoid inline lazy(() => import()) inside JSX. Move dynamic import (import()) outside the component for better tree shaking. Use useState and useEffect to manage when the component loads. Using the optimized approach ensures better performance, fewer re-renders, and correct tree shaking behavior.
Dynamic VS Named Exports
📌 Tree shaking removes unused code, but it works best with named exports. If you use default exports or dynamic imports (import()), bundlers may include unused code in the final bundle.
Bad Code (Default Export)
// ❌ utils.ts (default export - BAD for tree shaking)
export default {
add: (a: number, b: number) => a + b,
subtract: (a: number, b: number) => a - b,
multiply: (a: number, b: number) => a * b,
};
// ❌ App.tsx (Imports the whole module even if only `add` is used)
import utils from "./utils";
console.log(utils.add(2, 3)); // ⚠️ `subtract` and `multiply` are still included in the bundle!
🚨 Bad Example (Default Export - Harder to Tree Shake)- ❌ Tree shaking may fail because the bundler doesn’t know which parts are unused.
- ❌ If only one function is needed, the entire module might still be included.
// ✅ utils.ts (Named exports - GOOD for tree shaking)
export const add = (a: number, b: number) => a + b;
export const subtract = (a: number, b: number) => a - b;
export const multiply = (a: number, b: number) => a * b;
// ✅ App.tsx (Only `add` is included in the final bundle)
import { add } from "./utils";
console.log(add(2, 3)); // 🚀 Only `add` is included, `subtract` and `multiply` are removed!
- ✅ Tree shaking works efficiently because only used functions are imported.
- ✅ Unused exports (like subtract, multiply) are removed from the final bundle.
All Components Included in Bundle
🚨 In the example below, even though only the Button component is used, all the components (Input, Checkbox) will still be included in the final bundle.
Bad Example:
// components/Button.js
export default function Button() {
return <button>Click me</button>;
}
// components/Input.js
export default function Input() {
return <input type="text" />;
}
// components/Checkbox.js
export default function Checkbox() {
return <input type="checkbox" />;
}
// App.js
import Button from "./components/Button";
function App() {
return (
<div>
<h1>React Tree Shaking Issue</h1>
<Button />
</div>
);
}
export default App;
❌ Problems with this approach:
Default exports prevent tree shaking → The bundler cannot eliminate unused components (Input, Checkbox).
Increases final bundle size → Unused components are still in the output file.
Wasted performance → Extra JavaScript is loaded, affecting page speed.
The optimized version uses named exports, allowing the bundler to remove unused components.
Good Example (Optimized for Tree Shaking):
// components/index.js
export const Button = () => <button>Click me</button>;
export const Input = () => <input type="text" />;
export const Checkbox = () => <input type="checkbox" />;
// App.js
import { Button } from "./components"; // ✅ Only `Button` is included in the bundle
function App() {
return (
<div>
<h1>React Tree Shaking Optimization</h1>
<Button />
</div>
);
}
export default App;
✅ Why is this better?
Tree shaking removes Input and Checkbox → Since they are not imported, they are excluded from the final build.
Reduces bundle size → Only necessary code is included.
Better performance → The app loads faster with minimal unused JavaScript.
Large Dataset Included in Bundle
📌 Even when tree shaking is enabled, some large imports can still make it into the final bundle even if they are used minimally. One common pitfall is importing large datasets or configurations directly into a module, which can increase bundle size unnecessarily.
🔴 Bad Code (Importing Large Data):
// ❌ data.js (Large dataset)
export const data = Array.from({ length: 1000000 }, (_, i) => i); // 1 million numbers
// ❌ main.js
import { data } from "./data";
console.log(data[0]); // ❌ Even though only one element is used, the entire array is bundled!
❌ Problems with this approach:
Entire dataset is included in the bundle → Even if only a small portion is used.
Slows down page load → Large JavaScript files increase time-to-interactive (TTI).
Tree shaking won’t remove data → Since it's imported directly, the bundler assumes it’s needed.
// ✅ data.js (Large dataset)
export const getData = () => Array.from({ length: 1000000 }, (_, i) => i); // Lazily created data
// ✅ main.js (Lazy load the data only when needed)
import("./data").then(({ getData }) => {
const data = getData();
console.log(data[0]); // ✅ The dataset is NOT included in the initial bundle!
});
✅ Why is this better?
The dataset is only loaded when needed → No impact on initial load time.
Keeps the main bundle small → Avoids shipping unnecessary data.
Better performance → The browser loads JavaScript on demand, reducing blocking time.
Best Practices for Effective Tree Shaking
- ✅ Use ES6 Modules: Prefer import and export statements for better static analysis.
- ✅ Avoid Side Effects: Write side-effect-free modules to ensure accurate tree shaking.
- ✅ Prefer Named Exports: Named exports are generally more effective for tree shaking than default exports.
- ✅ Minimize Dynamic Imports: Use static imports when possible to ensure all unused code can be removed.
- ✅ Refactor Large Modules: Break down large modules or data to minimize the impact of unused code.
Specolative Api
Speculative API calls (also known as speculative execution) involve making API requests before they are explicitly needed to improve performance and user experience. This technique is useful in scenarios where a high probability exists that a user will request specific data soon.
import { useQuery, useQueryClient } from "@tanstack/react-query";
const fetchPosts = async (page: number) => {
const res = await fetch(`/api/posts?page=${page}`);
return res.json();
};
const Posts = () => {
const [page, setPage] = useState(1);
const queryClient = useQueryClient();
const { data, isLoading } = useQuery({
queryKey: ["posts", page],
queryFn: () => fetchPosts(page),
});
// Prefetch next page when current page loads
useEffect(() => {
queryClient.prefetchQuery({
queryKey: ["posts", page + 1],
queryFn: () => fetchPosts(page + 1),
});
}, [page, queryClient]);
if (isLoading) return <p>Loading...</p>;
return (
<div>
{data.posts.map((post) => (
<p key={post.id}>{post.title}</p>
))}
<button onClick={() => setPage((p) => p + 1)}>Next Page</button>
</div>
);
};
💡 How It Works:- When the user loads page 1, the next page (2) is prefetched.
- When they click Next Page, page 2 loads instantly.
- This creates a seamless, fast experience!
Best Practices for Speculative API Calls
Do's- ✅ Use speculative execution only when necessary (e.g., hover, scrolling, pagination).
- ✅ Cache responses to prevent unnecessary API calls.
- ✅ Limit speculative requests to prevent unnecessary server load.
- ✅ Use React Query, SWR, or similar libraries for efficient speculative execution.
- ❌ Don't prefetch too much data—it may overload the server.
- ❌ Don't trigger speculative API calls too frequently, or you'll increase API costs.
- ❌ Don't ignore cache expiration—stale data can lead to bad UX.
Speculative API calls can drastically improve UX by reducing perceived latency, especially in pagination, hover effects, and search predictions. However, overusing speculative requests can increase server load, so it’s important to implement it wisely.
Conclusion: Mastering Advanced React & Next.js Techniques
In this fifth installment of the React Best Practices series, we’ve explored cutting-edge strategies to enhance performance, scalability, and maintainability in modern React applications.
✅ Key Takeaways:
- Leveraging Next.js: Techniques like SSR, SSG, ISR, API Routes, Edge Functions, and optimized image handling allow developers to build high-performance applications that are SEO-friendly and cost-efficient.
- Speculative API Calls: By prefetching probable requests, we can reduce perceived latency and improve UX, especially in pagination, search suggestions, and hover-based preloading.
- Tree Shaking & Lazy Loading: Minimizing bundle size is critical for fast load times. Using named exports, dynamic imports, and avoiding side effects ensures optimal code-splitting.
- Vanilla JS vs. React/Next.js: While Vanilla JavaScript is still a strong choice for small, static websites, Next.js shines in dynamic, scalable, and interactive applications due to automatic optimizations, server components, and built-in analytics.
📌 The Future of React & Next.js
As React continues evolving with features like React Server Components, Suspense, and concurrent rendering, mastering these best practices ensures that your applications remain fast, scalable, and maintainable. By leveraging Next.js optimizations, efficient API management, and modern JavaScript techniques, you can build robust applications ready for the future of web development.