Blog Posts
Frontend development insights
Frontend development insights
🚀 React Server Components In this new version, we can create components that run exclusively on the server. This allows us to perform operations that were previously impossible in standard React, such as writing database queries directly inside our React components. Example: Fetching Data on the Server This is not something you would expect to see in React. Functional components couldn't be asynchronous, and we weren't allowed to have side-effects directly inside the render like that. But the key thing to understand here is that Server Components never re-render. They run once on the server to generate the UI, and their rendered UI is sent to the client. As far as React is concerned, this output is immutable and will never change. We can't use state because state changes, and Server Components can't re-render. We also can't use useEffect because effects only run after the render on the client, and Server Components never make it to the client. This also means that unlike traditional React components—where we have to put side-effects inside a useEffect callback or an event handler so that they don't repeat on every render—with Server Components we no longer have to, since they only run once. To make it a bit more confusing, it should be noted that traditional React components (or Client Components) render on both the client and the server, and React Server Components are not a replacement for Server-Side Rendering. 🥦 Specifying Client Components In this new version, all components are assumed to be Server Components by default, so we add the "use client" directive. 🦚 Client Boundaries Let's say we have an Article component with a "use client" directive, and a child component HitCounter that updates the counter when Article re-renders. For this, React has added a rule that Client Components can only import other Client Components. When we add the "use client" directive to the Article component, we create a "client boundary". All of the components within the boundary are implicitly converted to Client Components, even when they don't have the "use client" directive themselves. They will still hydrate and render on the client in this particular situation. So we don't have to add "use client" to every single file that needs to run on the client. !Client Boundary 🗒 What if I need to use state high up in the application? In this setup, we need to use React state to allow users to switch between dark and light mode. To avoid making all of our components Client Components, we can always extract the code and turn it into its own component wrapper. And remove the "use client" directive from that page.
Prisma is a typesafe, schema-driven ORM for Node.js and TypeScript that generates a tailored client based on your structural definitions. 🐊 Key Capabilities Type Safety: Automatically generates TypeScript types matching your database schema. Declarative Schema: A single schema.prisma file serves as the source of truth for tables, relations, and configurations. Migration Engine: Declarative migration generation tracking state differences via plain SQL files. Prisma Studio: An embedded GUI for data inspection and manipulation (npx prisma studio). 🚀 Implementation Workflow 1. Project Initialisation & Dependencies Configure a Node.js workspace running native ES Modules (ESM). ☘ Environment Configurations Update your configuration files to support target environments 2. Initializing Prisma Generate the configuration layer and schema framework: Scaffolding Prisma Configure your local or remote target instance within your secure configuration file: 3. Schema Design & Data Modeling Define application models 4. Running Database Migrations Synchronize your target database with the declaration models. This maps the .prisma file 5. Client Singleton Architectural Pattern To optimize application connection pooling 6. Executing CRUD Actions Verify transaction lifecycles and relation joins natively using type-safe logic execution:
Next.js 16 anchors the App Router as the default architecture for full-stack React applications by introducing several key updates, including stabilising Turbopack as the default bundler, implementing Cache Components through Partial Pre-rendering (PPR), upgrading to React 19.2, and enforcing asynchronous data structures for routing contexts. High-Level Structural Differences | Feature | Pages Router (Traditional) | App Router (Modern Standard) | | :--| :--| :--| | Root Directory | /pages | /app | | Routing Strategy | File-based (about.tsx maps to /about) | Folder-based (about/page.tsx maps to /about) | | Component Defaults | Client-side (Hydrated on Client) | React Server Components (RSC) by Default | | Layout System | Global (app.tsx) / Custom patterns | Native, deeply nested (layout.tsx) | | Data Fetching | getServerSideProps, getStaticProps | Unified fetch(), Server Actions | | Next.js 16 Caching | Cache-Control headers, ISR | Explicit use cache, Cache Components, updateTag() | | Middleware / Proxy | Standard Edge middleware.ts | Replaced/streamlined via explicit proxy.ts | 1. Directory Structure and Routing Design The Pages Router is file-centric, meaning every file within /pages automatically becomes an accessible URL route. The App Router is folder-centric, meaning folders define the path segments of your URLs, and special files (page.tsx, layout.tsx, loading.tsx, error.tsx) define the UI and lifecycle of that route segment. This allows you to safely colocate tests, components, and stylesheets inside the same route folder without accidentally publishing them as valid URLs. File-System Architecture Comparison 2. Core Comparison & Concrete Code Examples A. Routing & Nested Layouts In the Pages Router, sharing layouts requires wrapping your root components inside app.tsx or defining custom layout properties on the page level. Navigating re-renders the entire page hierarchy, destroying local component states. The App Router handles layouts natively. A layout.tsx file wraps around nested child segments. When navigating between child pages, parent layouts do not re-render, preserving client-side states (like active sidebar dropdowns or video playback). Pages Router Layout Example App Router Layout Example B. Component Paradigm & Data Fetching In the Pages Router, components are shipped to the browser and hydrated by default. Data fetching requires exporting specialized, lifecycle functions at the page level. In the App Router, every component is a React Server Component (RSC) by default. They evaluate on the build server or the request runtime, rendering pure HTML. Zero client-side JavaScript is sent to the client for layout structures. Data fetching is simplified; components can be declared as async functions to fetch data inline. Pages Router Data Fetching App Router Data Fetching (Next.js 16 Standard) Next.js 16 Critical Change: Dynamic route parameters (params and searchParams) are now provided as Promises and must be explicitly handled using await. C. Client Interactivity Because App Router items are Server Components by default, you cannot use client-side hooks like useState, useEffect, or browser-only APIs directly in a base file. You must explicitly register the client boundary using the "use client" directive. App Router Interactivity 3. Advanced Performance Primitives in Next.js 16 Next.js 16 changes the runtime behavior of the App Router, moving completely away from the implicit caching systems of earlier versions to explicit, developer-driven performance models. Cache Components & Partial Pre-rendering (PPR) In the Pages Router, if you use getServerSideProps, the entire page is blocked from rendering until the server finishes downloading data from external APIs. In the Next.js 16 App Router, Cache Components combined with Partial Pre-rendering split your page dynamically. Static elements (shell, banners, navigation text) are compiled immediately and served straight from an edge CDN, while dynamic components are streamed as soon as their async fetches complete via React Suspense. Explicit Cache Primitives Next.js 16 replaces implicit behavior with fine-grained cache control: "use cache": An explicit, function-level directive telling Next.js to cache the output of a specific data block or component execution. updateTag() & refresh(): Fresh caching APIs introduced in Next.js 16 that offer true "read-your-writes" data updates, resolving consistency delays commonly found across distributed servers.
Arrays are one of the most commonly used data structures in JavaScript. While they offer many useful built-in methods to help developers manipulate and work with data, map(), filter(), and reduce() are probably the most important, as well as essential tools for functional programming in JavaScript. Functional Programming is a paradigm where a function’s output depends solely on its inputs, ensuring that calling it with the same arguments always yields the exact same result. It eliminates "side-effects" (mutating local or global states). Why use these methods? They manipulate lists (arrays) while leaving the original array completely intact (immutability). They prevent you from having to manage standard for loops, create empty arrays, or manually push items into them. 1. Map Concept: Used when you want to look at every single item in an array, modify or extract something from it, and return a new array of the exact same length. How it works: map takes a callback function. This callback automatically receives the current item, the index, and the original array. Examples Imagine we start with this original array of song objects: Example A: Extracting a single property If you only want an array of song names: Example B: Modifying objects inside the array If you want to return a new array where all song names are converted to lowercase: 2. Filter Concept: Used when you want to evaluate every item in an array and selectively choose which ones to keep based on a condition. How it works: It takes a callback function that must return either true or false. If true, the item is kept in the new array; if false, it is left out. Examples Using the same songs array from above: Example A: Filtering numbers (e.g., getting even numbers from an array) Example B: Filtering arrays of objects If you only want to see songs by the artist 'Mastodon': 3. Reduce Concept: Used when you want to take an array of values and combine ("reduce") them into a single output value (such as a single number, an object, or a flattened array). How it works: Unlike map and filter, its callback receives an accumulator as its first argument, followed by the current value. The accumulator updates with every loop. You can also pass an optional starting value for the accumulator. Examples Example A: Summing integers to find an average Example B: Building an object from an array (Counting occurrences) Transforming our songs array into an object that maps { artist: song_count }: 4. Chaining Methods Because map and filter return new arrays, you can string (chain) these methods together consecutively to perform highly complex operations in a clean, readable layout. Example: Combining everything Imagine you have a nested array of songs from different music platforms (Spotify and LastFM), and you want to get a comma-separated string of names for all songs that are longer than 3 minutes (180 seconds):
Images and <iframe> elements consume significant bandwidth and processing power. Deferring the loading of these off-screen elements (lazy loading) frees up bandwidth for critical assets, improving performance metrics like Largest Contentful Paint (LCP) for images and Interaction to Next Paint (INP) for iframes. 1. Native Lazy Loading with the loading Attribute Modern browsers natively support lazy loading using the loading attribute on <img> and <iframe> elements. It accepts two primary values: eager: Loads the resource immediately (the default browser behavior). lazy: Defers loading until the resource reaches a specific, browser-calculated distance from the visible viewport. Note for <picture> elements: The loading="lazy" attribute must be placed on the child <img> tag, not the <picture> container. 2. When NOT to Lazy Load You should never lazy load elements that are likely to appear in the initial viewport (above the fold), such as hero images or top-of-page elements. The LCP Cost: Lazy-loaded images must wait for the browser to finish downloading and parsing all CSS to compute the page layout and determine if the image is in the viewport. Applying loading="lazy" to a visible image delays its fetch, harming LCP performance. 3. Lazy Loading <iframe> Elements Iframes are full HTML documents that pull in their own heavy subresources (especially JavaScript), which can bottleneck the main page thread and hurt interaction responsiveness. Common Use Cases: Third-party embeds like social media plugins or embedded video players. Impact: Lazy loading a YouTube embed saves over 500 KiB of initial data; a Facebook Like button saves over 200 KiB. 4. Using Facades for Heavy Embeds For interactive elements like video players (YouTube, Vimeo) or chat widgets, a facade is highly effective. Instead of loading the iframe instantly, you display a lightweight static image or a fake button that mimics the widget. The actual heavy iframe is only requested and swapped in once the user intentionally interacts with it (e.g., clicks "Play" or hovers over the chat icon). Popular open-source tools for this include "lite-youtube-embed" and "React Live Chat Loader". 5. JavaScript-Based Lazy Loading Libraries Because native browser lazy loading is restricted to <img> and <iframe>, you must use JavaScript libraries (like lazysizes or yall.js) to lazy load unsupported elements: Use Cases: CSS background images, <video> elements, and video poster images. How They Work: These libraries leverage the Intersection Observer API to detect when an element approaches the viewport, then swap a temporary data attribute (like data-src) into the standard src attribute to trigger the network request. This is particularly useful for replacing large animated GIFs with lazy-loaded, muted autoplaying videos.
While optimizations like deferring JavaScript and lazy loading images improve the initial page load by minimizing early resource usage, they can inadvertently cause delays during subsequent user interactions. To strike a balance between saving bandwidth and delivering instant responsiveness, developers can preemptively load resources using three advanced techniques: Prefetching, Prerendering, and Service Worker Precaching. 1. Prefetching Prefetching hints to the browser that certain resources or pages will likely be needed in the near future, allowing the browser to download them ahead of time during idle periods. Resource Prefetching (<link rel="prefetch">) Downloads specific assets (like JS, CSS, or images) at the lowest priority. When the user eventually triggers an action needing that asset, it is served instantly from the disk cache. The HTML snippet above informs the browser that it can prefetch data-picker.js and data-picker.css once it is idle. Browser Support: Works in all modern browsers except Safari (where it is behind a experimental flag). Page Prefetching Downloads an entire HTML document and its subresources (<link rel="prefetch" href="/page" as="document">). Speculation Rules API A newer, JSON-based method for Chromium browsers to prefetch pages. Unlike standard prefetching which uses the HTTP cache, speculation rules process and store resources in the memory cache, making retrieval even faster. Important Caveats Avoid prefetching cross-origin documents (due to duplicate request bugs) or highly personalized/authenticated pages (which aren't cached anyway). Tools like Quicklink optimize this by only prefetching links that enter the user's viewport. 2. Prerendering Prerendering goes a step further than prefetching. Instead of just downloading the page files, the browser fully renders the page and executes its JavaScript in the background. When the user clicks the link, the page is instantly swapped into the foreground. Implementation: Handled via the JSON-configured Speculation Rules API. Important Caveat: Because prerendering executes JavaScript, it is computationally expensive and memory-intensive. It should be used sparingly and only when you are highly confident the user is about to navigate to that specific page. (Note: The older <link rel="prerender"> hint is deprecated in Chrome and now defaults to a "NoState Prefetch", which doesn't execute JS). 3. Service Worker Precaching This approach leverages a Service Worker to fetch and save static assets into the high-level Cache API (which is controlled by JavaScript and separate from the lower-level HTTP cache). How it Works: Precaching occurs during the Service Worker's installation phase. Once cached, assets are served instantly via a "cache-only" strategy, completely bypassing the network during subsequent page navigations. Workbox: The article recommends using Google's Workbox library to manage precaching. Workbox utilizes a "precache manifest" (a list of files and version hashes) to automatically handle complex versioning and clean up expired cache entries. Use Case Example: On an e-commerce platform, a product listing page can use a service worker to precache the CSS and JS required for the product detail page, providing instantaneous transition speeds. Key Performance Takeaways Network & Data Mindfulness: All three techniques consume bandwidth, storage, and CPU. Developers must be conservative, avoiding oversized prefetch manifests. User Preferences: Do not trigger these speculative behaviors if the user has enabled the Save-Data signal on their device. When in Doubt: Precache/prefetch too little rather than too much, and rely on runtime caching to populate data as the user browses.
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact, self-contained method for securely transmitting information between parties as a JSON object. While the core cryptographic principles of JWTs remain stable, the strategy for implementing them has radically shifted. With modern full-stack frameworks like Next.js shifting processing to the server, traditional client-side patterns are outdated. Storing tokens where client-side scripts can access them opens the door to devastating security vulnerabilities. This guide breaks down exactly what JWTs are, when you should use them, how the underlying process works, and how to implement them securely using modern framework architecture. What is a JWT (and what it's not) A JWT is a tool for verifying the ownership and integrity of data. When a server receives a valid token, it is cryptographically guaranteed that the data inside can be trusted because it was signed by a trusted source and has not been altered in transit. The Core Misconception: Signed vs. Encrypted A standard JWT guarantees integrity, not confidentiality. The data inside a JWT is merely serialized and Base64Url encoded—it is not encrypted. Anyone who intercepts the token can paste it into a decoder and read your entire payload in clear text. Because of this transparency: Never store confidential data (passwords, credit card numbers, or sensitive PII) in a JWT payload. Always enforce HTTPS across your entire application to protect tokens from being intercepted in transit. When to Use JSON Web Tokens JWTs are not a magic bullet for every scenario, but they excel in specific architectural designs: 1. Stateless API Authentication This is the most common use case. In a traditional session model, the server must look up a session ID in a database or cache on every single request. With JWTs, the authentication state is stored directly inside the token payload. The microservice or API endpoint can verify the token independently using a shared secret or public key without querying a central database, making it highly scalable. 2. Secure Information Exchange JWTs are an excellent way to securely transmit data between independent microservices or third parties. Because tokens can be signed using public/private key pairs, the receiver can verify that the sender is exactly who they claim to be and ensure the content hasn’t been tampered with. 3. Single Sign-On (SSO) Because JWTs are self-contained and small enough to be passed via HTTP headers or cookies, they are widely used in SSO architectures. Once a user logs into a central Identity Provider (like Auth0, Clerk, Okta, or Cognito), a token is generated that allows the user to navigate across different web domains without logging in again. The JWT Architecture and Flow To understand how JWT operates in production, it is important to trace the lifecycle of a request: 1. Authentication: The user submits their credentials (username/password) to the authorization server via a secure HTTPS request. 2. Token Issuance: The server verifies the credentials. If valid, it signs a JWT containing the user's identity details (claims) and returns it to the client. 3. Storage & Request: The client stores the token safely. For every subsequent request to a protected resource or API endpoint, the client attaches the JWT—typically inside the Authorization header using the Bearer schema: 4. Verification: The target server extracts the token, verifies the cryptographic signature against its own secret/public key, and checks if the token is expired. If valid, the server fulfills the request. The Structural Anatomy of a JWT A JWT visually appears as a single string broken into three distinct parts separated by dots (.): header.payload.signature 1. The Header The header identifies the token metadata, primarily detailing the token type (JWT) and the cryptographic signing algorithm being used (such as HS256 or RS256). This JSON object is Base64Url encoded to form the first part of the token. 2. The Payload The payload houses the claims. Claims are statements about an entity (typically the user) and any additional operational context metadata. Registered claims: Predefined, recommended claims like sub (subject), exp (expiration time), and iss (issuer). Public/Private claims: Custom claims you define to pass data to your application UI or backend services. This object is Base64Url encoded to form the second part of the token. 3. The Signature The signature is used to verify that the message wasn't altered along the way. To generate it, the system takes the encoded header, the encoded payload, a secret key, and runs them through the algorithm defined in the header. If a malicious actor alters the payload, the recalculation of the signature on the server will fail to match, and the request will be rejected. 🔒 Token Storage: The Modern Security Standard Where you store the token on the web client is the single most critical security decision you will make. ❌ The Legacy Vulnerability: localStorage Many older tutorials suggest saving JWTs inside localStorage or sessionStorage because it is highly convenient for frontend JavaScript to access. Do not do this. If an attacker manages to execute any script on your page via a Cross-Site Scripting (XSS) vulnerability—whether through a compromised third-party script, an analytics package, or un-sanitized user input—they can run localStorage.getItem() and steal your user's identity instantly. ✅ The Modern Safe Approach: HttpOnly Cookies To completely neutralize XSS-based token theft, you must store JWTs inside an HttpOnly cookie. When a cookie is marked as HttpOnly, browser-side JavaScript is completely blocked from reading or manipulating it. The browser will automatically attach the cookie to outgoing HTTP requests to your domain, allowing your server to authenticate the request while keeping the token hidden from client-side code. Production Implementation: Next.js (App Router) Modern Next.js applications run server-side code natively alongside UI code. This allows you to handle authentication entirely on the server side, eliminating token exposure to the browser altogether. 1. Authenticating & Issuing the Token (Server Action) When a login form is submitted, we handle the authentication securely inside a Server Action. If valid, we sign the token and drop it directly into a secure cookie. 2. Guarding App Routes (Next.js Middleware) To block unauthorized users from viewing private routes, Next.js Middleware can intercept requests at the edge, handle cryptographic verification, and redirect users seamlessly before any page layout ever renders. 3. Reading Token Payload Data (React Server Component) Because the cookie is handled on the server, you can cleanly parse token claims to serve customized views to your users directly inside React Server Components without any loading state flashes.
OOP is a paradigm focused on modeling systems as collections of objects, where each object represents a specific aspect of the system's data and behaviour. Everything is an object. This assertion underlies Object-Oriented Programming (OOP). In this paradigm, objects are well-defined data structures, each with its own properties (attributes) and the capability to execute its own procedures (methods). Some of the primary benefits of this approach include well-structured codebases and programs that are easier to modify, debug, maintain, and reuse. In JavaScript, the class syntax is simply syntactic sugar over function constructors and Prototypal Inheritance. When we share data or functionality between objects —for example, a child object accessing a parent object’s methods or properties —we are utilizing the built-in prototype system. How the Prototype Chain Works When you access a property, the JavaScript interpreter first looks at the instance object itself. If the property is not found, it travels up the prototype chain using the proto pointer. It searches the linked prototype objects until it finds the property or reaches Object.prototype (the root). When a new object is created in JavaScript, we are instantiating a reference to a specific location in memory and its corresponding prototype. The Four Principles of OOP The core of OOP is built upon four pillars: Encapsulation, Abstraction, Inheritance, and Polymorphism. 1. Encapsulation Encapsulation ensures that all properties and methods of an object are kept self-contained and protected from outside interference. Within an object, we can define both private and public members. While private variables and methods are inaccessible to other objects, public ones provide a controlled interface for interaction. This prevents the internal state of an object from being modified unexpectedly by external code. 2. Abstraction Abstraction helps isolate the impact of code changes; if an update is made, it only affects the internal implementation details of a class rather than the external code. Think of a music player: you see a "Play" button, but the complex circuitry required to decode the audio is hidden inside. We interact with the technology on the surface without needing to understand its internal mechanics. By hiding internal complexity and exposing only essential features, we prevent "leaking" implementation details that the rest of the program does not need to manage. 3. Inheritance Through the inheritance mechanism, a class can derive features, properties, and methods from another class. In JavaScript, this is achieved through the prototype chain. This promotes reusability and ensures that shared functionality is not duplicated in memory. 4. Polymorphism Polymorphism (meaning "many shapes") allows different objects to be treated as instances of the same general type through a shared interface. It helps eliminate long if/else or switch statements by allowing each unique object to implement its own specific version of a common method. When that method is called, the object performs the action according to its own logic. Example: Key Benefits of OOP Encapsulation: Reduces complexity and increases reusability. Abstraction: Isolates the impact of changes by hiding implementation details. Inheritance: Eliminates redundant code by sharing logic across objects. Polymorphism: Refactors messy conditional logic into clean, object-specific behaviour.
Agile Definition Agile is a term describing approaches to software development that emphasize incremental delivery, team collaboration, continual planning, and continual learning. Agile methods, often called frameworks, provide comprehensive structures for the various phases of the DevOps lifecycle: Planning Development Delivery Operations These frameworks -Scrum being the most popular among them— prescribe methods for accomplishing work through clear guidance and shared principles. !Agile What is Agile development Agile development is a term used to describe iterative software development that shortens the DevOps lifecycle by completing work in short increments. These increments, usually called sprints, typically last between one and four weeks. Delivering production-quality code every sprint requires the development team to maintain an accelerated, sustainable pace. All coding, testing, and quality verification must be completed within each and every sprint to ensure the product is always in a "shippable" state. Backlog Refinement An Agile team works from a backlog of requirements, often referred to as user stories. The backlog is prioritized so that the most valuable items remain at the top. The Product Owner manages the backlog, adding, changing, or reprioritizing stories based on evolving customer needs. The Product Owner’s primary responsibility is to ensure that engineers have clearly defined requirements. The stories at the top of the backlog should always be "ready" for the team to begin—a process known as backlog refinement. Continuos Integration and Delivery (CI/CD) Implementing CI/CD is one of the first tasks a team tackles when starting a new project. Through automation, teams avoid slow, time-intensive, and error-prone manual deployment processes. Several key CI/CD activities are critically important for effective Agile development: 1. Unit Testing: Integrate unit testing into every build to catch bugs early. 2. Build Automation: The system should automatically pull code and tests directly from the source repository. 3. Branch and Build Policies: Configure policies to trigger builds automatically upon code changes or Pull Requests. 4. Deployment to Environments: Set up a release pipeline that automatically deploys successful builds to designated testing or production environments.
In order to achieve a smooth dropdown animation without a dedicated JavaScript library, we have to address a common CSS limitation: the height property cannot transition between a fixed value like 0 and auto. CSS cannot transition between: This limitation often leads to "snapping" menus or causes developers to reach out for Framer Motion or GSAP just to open a simple dropdown. 1. HTML Solution This implementation uses three specific techniques to achieve a smooth, weighted feel: I. Custom Timing function Instead of a generic ease, we use a custom Bézier curve: ease-in-out (cubic-bezier(0.4,0,0.2,1). This mimics real-world weight-starting fast, and settling with a smooth deceleration II. CSS Grid for dynamic height We animate grid-template-rows instead of the height property. In CSS Grid, animating from 0fr to 1fr only works, as long as the direct child of the grid item has min-height:0 (see below) and the container users overflow:hidden. Closed: grid-rows[0fr] Open: grid-rows[1fr] The browser calculates the height of the internal content dynamically, and transitions between states correctly. III. The Staggered effect To create a cascading reveal effect, the links cannot be static. They must slide up into place while the menu rolls down, and so we use transition-delay classes. 2. Component Logic with React/NextJS Ensure the menu wrapper follows this structure: CodePen (HTML/TailwindCSS) To see this work in a CodePen click See Codepen. Or copy/paste the code below into the HTML body of your page, and add the tailwind CDN.
Server-side rendering (SSR) Server-side rendering is the process of rendering an application on the server to send HTML, rather than JavaScript, to the client. When the server generates the HTML content for a page, the page is considered server-side rendered. !image Server-side rendering Server-side rendering was the original method of delivering web content long before the term "SSR" was coined. The term only came into common use once JavaScript became capable of running on both the browser and the server. This has been the traditional model for serving content since the 1990s. Generally, server-side rendered pages achieve a faster First Contentful Paint (FCP) —the point at which requested content becomes visible in the browser during the initial load. Every time a new request is received, the server generates and delivers the HTML content. This process can be handled by various languages and frameworks, such as .NET, Java, PHP, or Node.js (JavaScript), and often involves fetching data from an external API. In contrast to client-side rendering, which requires the browser to load and execute JavaScript before displaying content, SSR provides users with a fully formed HTML page immediately. This approach also results in significantly better Search Engine Optimization (SEO), as web crawlers can easily read the pre-rendered content.
Which sites can leverage static-site generation With Static Site Generation (SSG), HTML content is pre-generated and deployed as static files. The output is a set of immutable HTML, CSS and JS files. The server does zero computation per request. Whenever the code or content changes for one or more pages, a build process runs to regenerate the HTML. Normally, when working with static site generation, we create an automated workflow that builds and deploys the site whenever the content changes. Pure SSR however; it's not suitable with large-scale dynamic data. In a site with thousands of products, the problem becomes the build-to-deploy latency. !Static Site Generation Some frameworks have already introduced new features to minimize build times and handle data updates more efficiently: GatsbyJS Incremental builds This feature optimizes performance by building only the pages that have changed since the last deployment. NextJS Incremental Static Regeneraton (ISR) It allows you to update static pages after you’ve built your site. It builds and caches pages at runtime as requests come in, providing the speed of static with the flexibility of dynamic. Benefits of statically generated sites Fast Server Response: Since files are pre-built, the server delivers them instantly without processing time. Traffic Resilience: These sites handle traffic spikes with ease because serving flat files requires minimal resources. Reduced Overhead: They deliver content without the need for real-time computation or database queries. Compute Load: By removing the "compute" step from the user request, you eliminate the most common performance bottlenecks: server CPU/RAM usage and latency. Disadvantages of static sites No Dynamic Content: Because pages are pre-generated, they cannot serve user-specific content. Delayed Updates: Content is not updated instantly; for very large sites, the regeneration task may be slow to reflect changes. For rapidly changing content, a standard build process may not be fast enough. In these situations, Server-Side Rendering (SSR) or Client-Side Rendering (CSR) should be leveraged to ensure users see the most up-to-date information.