
JavaScript provides several ways of iterating over a collection, from simple for loops to map() and filter(). Iterators and generators bring the concept of iteration directly into the core language and provide a mechanism for customizing the behaviour of ‘for...of loops’. Iterators and generators usually come as a secondary thought when writing code, but if you can take a few minutes to think about how to use them to simplify your code, they'll save you from a lot of debugging and complexities.
When we have an array, we typically use the ‘for loop’ to iterate over its element.


The ‘for...of the loop’ can create a loop over any iterable object, not just an array.
The following values are iterable –
Plain objects are not iterable and hence the 'for...of' uses the Symbol.iterator.
The Symbol.iterator is a special-purpose symbol made especially for accessing an object's internal iterator. So, you could use it to retrieve a function that iterates over an array object, like so –

Generator functions once called, returns the Generator object, which holds the entire Generator iterable and can be iterated using next() method. Every next() call on the generator executes every line of code until it encounters the next yield and suspends its execution temporarily.
Generators are a special type of function in JavaScript that can pause and resume state. A Generator function returns an iterator, which can be used to stop the function in the middle, do something, and then resume it whenever.
Fun Fact – async/await can be based on generators
Generator functions/yield and Async functions/await can both be used to write asynchronous code that 'waits', which means code that looks as if it was synchronous, even though it is asynchronous. ... An async function can be decomposed into a generator and promise implementation which is good to know stuff.
Generator functions are written using the function* syntax –

Additionally, generators can also receive input and send output via yield. In short, a generator appears to be a function but it behaves like an iterator.
A generator is a function that returns an object on which you can call next(). Every invocation of next() will return an object of shape —

Output

Apart from returning values, the yield can also call a function –

Output


Output

As a Title –

Output

This is an evaluation model that delays the evaluation of an expression until its value is needed. That is, if the value is not needed, it will not exist. It is calculated on demand.
A direct outcome of Lazy Evaluation is that generators are memory efficient.
The only values generated are those that are needed. With normal functions, all the values must be pre-generated and kept around case they need to be used later.
We have learned the following things about iterators and generators –
.avif)
As computers are getting more powerful it is getting easier to achieve complex animations without compromising the fluidity and user experience. There are various JavaScript animation libraries available and most of them are pretty good. We at QED42 tried some of them and for the most part, we used GSAP which we think is kind of becoming an industry standard. GSAP is highly configurable and is just the right tool if you want to have scroll-based animations.
One thing that is eye-catching and easy to achieve, is smoothly changing background colour while scrolling. To implement this, we will change the wrapper component’s background colour as its child components move in and out of the viewport.
.avif)
Source:qed42-js.netlify.app
Step 1: Create a context that will wrap our components and provide the necessary functionality

Step 2:We will then import child components (named First, Second and Third), GSAP, and AnimationContext in our main app component. We are monitoring “currentBg” in the “useEffect” hook and whenever its value changes, the GSAP function gets executed. GSAP will change the background in 1 second which gives the fade-in effect.

Step 3: To implement scroll-based triggers for the second and third components we will use GSAP.
The second component returns the following JSX, having a reference to wrapper element and text.


Next, we will create a GSAP timeline and use a scroll trigger to target an element, and set the start and endpoint for animation.

ScrollTrigger has onEnter and onLeaveBack function which gets triggered when the trigger element passes through the scroll markers. That is where we change our background using context.
Note: The same effect can be achieved using gsap.to(), gsap.from() and other methods instead of gsap.timeline().

The code for the above demo can be found at the following Github link.
What we learned from our hands-on experience is that animations should enforce, enhance the user behaviour and experience. The animations should not hinder action items in a way that it takes more time for users to interact with them. Therefore the background animation we saw above is very subtle and doesn’t cause any such hindrance.
.webp)
It's 2 AM on a Saturday, and your phone buzzes. The app is down. After three hours of debugging, you finally discover the issue: someone changed `userId` to `authorId` in the backend last week. Your frontend was still looking for `userId`. Everything compiled without errors, and TypeScript showed green checkmarks all around. Does this sound familiar?
This blog aims to ensure that this kind of problem never happens again by establishing true end-to-end type safety.
By the end of this guide, you'll be able to:
Imagine your backend and frontend as two separate islands, each with its own TypeScript ecosystem. Both define a type, and both feel secure in their type-safety. However, they're divided by an ocean, with no shared source of truth. A small change on one island can silently break the other.

Both the backend and frontend define their own version of the User type.
They look identical, and everything compiles perfectly.
TypeScript shows all green, life is good.

The backend team updates the User to store dob instead of age. Everything compiles fine because TypeScript checks each island separately, and the change goes unnoticed. Both sides are “type safe,” yet your data silently drifted apart.
Monday morning, the support inbox lights up: "Why doesn't the app show user ages anymore?" This is the fundamental problem that end-to-end type safety solves. No more islands. No more silent mismatches.
So, how do we stop our backend and frontend from becoming disconnected?
Simple, we need to ensure they communicate effectively, and make them speak the same language. That language is your GraphQL schema, the single source of truth for your entire app.
The Flow: End-to-End Type Safety

Every type, field, and structure in your application is defined a single time in the schema, which then automatically generates the corresponding types for both the backend and frontend. This eliminates duplicate definitions and the uncertainty of whether they align.

Now the mismatch is caught instantly during compile time, not at 2 AM on Saturday. Your schema drives your code, your code drives your app, and your types stay in sync effortlessly.
Now that we understand how the GraphQL schema acts as the single source of truth, the next question is — how do we actually achieve end-to-end type safety?
The answer is GraphQL Code Generator (or Codegen for short).
Codegen is a tool that reads your schema and queries, then automatically generates TypeScript types and React hooks for you.
No more writing types by hand or worrying if your frontend and backend have drifted apart — Codegen ensures every part of your stack speaks the same language.
Now that we know what Codegen does, let’s bring it into your workflow.

Once configured, Codegen runs in two places:
Let's see it step by step in action.
Already have a GraphQL server? Skip to Step 1.
Need one fast? Here's a minimal Apollo Server with a `schema.graphql` file:
npm install @apollo/server graphql
npm install --save-dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-resolvers
Note: startStandaloneServer is designed for prototyping and simple use cases — for production, integrate Apollo Server with Express
Create a `schema.graphql` file with:
type Todo {
id: ID!
text: String!
done: Boolean!
}
type Query {
todos: [Todo!]!
}
Create `server.ts`:
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import fs from 'fs';
import path from 'path';
const typeDefs = fs.readFileSync(path.join(__dirname, 'schema.graphql'), 'utf-8');
const todos = [
{ id: '1', text: 'Learn GraphQL', done: false },
{ id: '2', text: 'Build a Todo app', done: true },
];
const resolvers = {
Query: {
todos: () => todos,
},
};
const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, { listen: { port: 4000 } });
console.log(`🚀 Server ready at ${url}`);
Run the backend server:
npx tsx server.ts
Backend Types? Generate Them
Create `backend/codegen.ts`:
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: './schema.graphql',
generates: {
'./src/generated/graphql.ts': {
plugins: ['typescript', 'typescript-resolvers'],
},
},
};
export default config;
Run backend codegen on schema changes:
npm run codegenNow lets move forward, In frontend project run:
npm install --save-dev @graphql-codegen/cli \
@graphql-codegen/typescript \
@graphql-codegen/typescript-operations \
@graphql-codegen/typescript-react-apollo
npm install @apollo/client graphql
Create `codegen.ts`:
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: 'http://localhost:4000/graphql',
documents: ['src/**/*.{ts,tsx,graphql}'],
generates: {
'./src/generated/graphql.ts': {
plugins: [
'typescript',
'typescript-operations',
'typescript-react-apollo',
],
config: { withHooks: true },
},
},
};
export default config;
Add these scripts in `package.json`:
"scripts": {
"codegen": "graphql-codegen",
"codegen:watch": "graphql-codegen --watch"
}
Note: Watch mode requires @parcel/watcher as an additional dev dependency — run npm install --save-dev @parcel/watcher once.
Create a query file, e.g. `src/queries/getTodos.graphql`:
query GetTodos {
todos {
id
text
done
}
}
Run:
npm run codegen
Generated hooks like `useGetTodosQuery` will be created in `./src/generated/graphql.ts`.
import { useGetTodosQuery } from '../generated/graphql';
export function TodoList() {
const { data, loading, error } = useGetTodosQuery();
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data?.todos.map(todo => (
<li key={todo.id}>
<input type="checkbox" checked={todo.done} readOnly />
{todo.text}
</li>
))}
</ul>
);
}
At This Point
- Backend and frontend share a single source of truth schema.
- Backend resolvers use type-safe typings.
- Frontend queries and hooks are fully typed.
- Any schema changes propagate type errors during development, preventing runtime bugs.
You’ve just seen how a single GraphQL schema can power your entire app, backend to frontend, while maintaining type safety throughout the process.
Let’s recap what we built:
GraphQL Schema → Codegen → TypeScript Types + React Hooks → React Components
Each layer communicates in the same language, automatically—fewer duplicated types, mismatched field names, and runtime 'undefined' errors."
End-to-end type safety isn’t just about preventing bugs—it’s about building trust between every part of your stack. When your schema defines the truth, your entire stack aligns.

React Native is undergoing one of its most significant transformations since its launch with the adoption of the New Architecture. Built around Fabric, TurboModules, and the JavaScript Interface (JSI), this shift changes how JavaScript communicates with native platforms.
Earlier versions relied on a bridge that serialised data between threads, which often introduced latency in complex applications. The New Architecture removes that limitation by enabling direct communication between JavaScript and native code, resulting in faster rendering, better responsiveness, and lower memory usage.
For production-scale applications, this is more than a technical upgrade. It improves long-term scalability and stability. Fabric brings React Native closer to modern React concurrency, allowing smoother UI updates during heavy processing, while TurboModules improve startup performance by loading native modules only when required.
As businesses increasingly expect native-level performance from cross-platform frameworks, the New Architecture makes React Native more capable of supporting enterprise-grade mobile experiences.

Performance has historically been one of the most discussed concerns in cross-platform development, and React Native’s adoption of the Hermes JavaScript engine marks a decisive step toward solving it. Hermes is optimised specifically for mobile environments, focusing on faster startup times, lower memory usage, and predictable execution performance. Unlike general-purpose JavaScript engines, Hermes precompiles JavaScript into optimised bytecode during build time, reducing runtime processing when the app launches.
This improvement is especially noticeable on mid-range and low-end devices where resource constraints are more visible to users. Faster time-to-interactive metrics and smoother navigation flows directly translate into better user retention and engagement. Beyond performance gains, Hermes also improves debugging and profiling capabilities, giving development teams deeper insight into runtime behaviour. As mobile applications grow more complex, having a JavaScript engine tailored for mobile workloads helps organisations deliver consistent experiences across diverse hardware ecosystems.
The alignment between React Native and React 18 introduces a new paradigm in how mobile interfaces handle rendering workloads. Concurrent rendering allows React to prepare UI updates without blocking user interactions, making applications feel significantly more responsive. Instead of freezing the interface during heavy updates, React can prioritise urgent interactions while processing background changes incrementally.
This shift is particularly impactful for applications with dynamic data, such as dashboards, social feeds, and collaborative tools. Users experience smoother scrolling and faster interaction feedback, even when complex state updates occur behind the scenes. For engineering teams, concurrency also encourages better architectural patterns by separating urgent UI updates from non-critical computations. As React Native continues to align closely with the broader React ecosystem, developers benefit from shared innovation across web and mobile platforms, reducing fragmentation and improving long-term maintainability.
Modern mobile applications increasingly rely on fluid animations and gesture-driven interactions to deliver premium user experiences. Recent advancements in libraries like React Native Reanimated have significantly changed how animations are executed. By leveraging worklets and running animation logic directly on the UI thread, React Native applications can achieve native-level smoothness without being limited by JavaScript thread performance.
This architectural improvement allows animations to remain responsive even during network requests or heavy computation. Complex transitions, gesture-based navigation, and interactive components now behave consistently across platforms. For product teams, this means design ambitions no longer need to be compromised due to performance constraints. The ability to deliver high-fidelity animations within a cross-platform framework strengthens React Native’s position in industries where user experience directly influences business outcomes.
Recent React Native releases have focused heavily on improving developer experience, recognising that productivity directly impacts product delivery timelines. Enhancements to the Metro bundler, faster refresh cycles, and improved error reporting have reduced iteration time during development. Developers now spend less time waiting for builds and more time refining features, which becomes increasingly valuable in large-scale projects.
Tooling modernisation has also improved compatibility with current Android and iOS ecosystems, reducing friction caused by outdated dependencies. Better TypeScript integration has encouraged safer codebases and easier refactoring, particularly in enterprise environments where multiple teams collaborate on shared applications. These improvements collectively signal a maturation phase for React Native, where stability and reliability are prioritised alongside innovation.
Expo has evolved from a beginner-friendly abstraction layer into a production-ready platform capable of supporting complex applications. With the introduction of modern build systems and deeper native integration capabilities, developers can now access advanced features without abandoning Expo’s streamlined workflow. This shift reduces setup complexity while preserving flexibility for custom native development when required.
For organisations seeking faster onboarding and reduced DevOps overhead, Expo’s ecosystem simplifies environment configuration and deployment pipelines. Continuous integration, over-the-air updates, and simplified asset management allow teams to iterate quickly while maintaining production quality. The growing compatibility between Expo and React Native’s New Architecture further strengthens its role as a viable solution for both startups and enterprises.
As the mobile development landscape expands with alternatives like Flutter and Kotlin Multiplatform, React Native continues to maintain strategic relevance due to its ecosystem maturity and alignment with web technologies. Companies already invested in React benefit from shared knowledge, reusable architectural patterns, and easier cross-team collaboration between web and mobile developers.

The framework’s ongoing modernisation demonstrates a commitment to long-term evolution rather than short-term trends. Improvements in performance, architecture, and tooling show that React Native is adapting to industry demands while preserving developer productivity. For organisations balancing development speed with scalability, React Native remains a practical and forward-looking choice capable of supporting both rapid innovation and sustainable growth.

Content-heavy sites built on Next.js and Strapi often hit the same wall: static pages perform well, but keeping them current without triggering a full rebuild is harder than it should be.
Building a content-heavy site with Next.js and Strapi, we kept running into the same problem: static pages performed well, but every content update triggered a full rebuild and redeploy. Costly, slow, and frustrating, it didn't scale.
The challenge wasn't performance. It was keeping static sites current without paying for it every time something changed.
With Static site generation or SSG, pages are pre-rendered into HTML at build time and served from a Content Delivery Network or CDN. No server processing on each request. That's what makes them fast. It's also what makes updating them a problem.
For mostly static content like blogs, documentation, and marketing pages, this works well. But for sites where content changes frequently, the cracks show quickly. Even a minor edit forces a full site rebuild, which slows down publishing and burns unnecessary compute
Key benefits of SSG:
Challenges with Traditional SSG:
This leads to a "problem" where managing frequently changing content with a purely static approach becomes cumbersome and inefficient.
ISR addresses the core limitation of SSG. Instead of rebuilding the entire site on every change, it updates only the pages that need it, regenerating them in the background.
How ISR solves the problem:
When ISR isn't a fit:
To make this work with Strapi as the content source, we built a five-component pipeline:
The first component in the ISR workflow is the Strapi Webhook. When content changes in Strapi, it automatically sends an HTTP POST request to a predefined endpoint. This is what connects your CMS to the rest of the build pipeline.

How it works:
Strapi provides a straightforward way to configure webhooks directly within its admin interface. As a content editor, when you perform actions such as:
Strapi detects these events. For each configured event, it automatically sends an HTTP POST request to a predefined URL that you specify. This URL typically points to an external endpoint such as an API Gateway or a serverless function designed to receive and process these notifications.
What the webhook sends:
This POST request carries a payload, a block of structured data (usually JSON) that contains information about the content change that just occurred. This payload can include:

Its role in the ISR workflow:
The Strapi webhook serves as the initial trigger for the entire regeneration process. Rather than constantly polling Strapi for changes, which is inefficient and resource-heavy, we let Strapi notify our system the moment something is updated.
This notification carries enough information to identify exactly what changed, which content type was affected, and which entry was modified.
That specificity is what makes targeted page regeneration possible. Instead of rebuilding the entire site, the system knows precisely which pages need to be refreshed, keeping the Next.js site current with the latest content from Strapi while maintaining the speed benefits of static hosting.
Once the Strapi webhook sends its content change notification, the next component in the pipeline is the API Gateway. It sits between Strapi and the rest of the system, ensuring that only valid and properly formatted requests make it through to the backend for processing.
Key functions of the API gateway in this workflow:
After the API Gateway securely receives and validates the Strapi webhook payload, the next step in our ISR workflow involves sending that payload to an AWS Simple Queue Service (SQS) Queue. SQS handles asynchronous communication and is fundamental to building a reliable and scalable content update pipeline.
SQS is a fully managed message queuing service that enables you to decouple and scale microservices, distributed systems, and serverless applications. In our context, it performs several vital functions:
The result is a system where every content change is eventually processed without risk of overload or data loss.
After content update events are queued in SQS, the Cron Job is what decides when and how to act on them. Rather than reacting to every single content save and potentially overwhelming the build infrastructure, the Cron Job batches updates and processes them at scheduled intervals.
How the Cron Job Operates:
The Cron Job is a scheduled task that runs at predefined intervals, every 15 minutes by default, though this can be configured based on how frequently content changes. Its primary responsibilities include:
.webp)
Benefits of this batching approach:
In summary, the Cron Job acts as the intelligent orchestrator, ensuring that content changes from Strapi are efficiently and precisely translated into the necessary Next.js site updates, choosing the most optimal build strategy (partial or full) for maximum speed and efficiency.
The final stage in our custom ISR workflow is the Build System, where the actual regeneration of your Next.js site takes place. Guided by the decisions made by the Cron Job, the build system determines whether to perform a partial build or a full regeneration. This choice is fundamental to achieving both speed and consistency.
A partial build is the preferred and most efficient method when content changes are localized.
What it Rebuilds: A partial build rebuilds only the affected route or page. It doesn't touch the parts of your site that haven't been modified.
When it's Used: This approach is ideal for page-level changes such as:
Updating a blog post: If you edit the content of a specific blog article, only that individual blog post's page needs to be regenerated. Modifying a product page: If a product description or image changes, only that specific product page is rebuilt.
Speed and Cost-Effectiveness: Partial builds are fast and cost-effective. They consume minimal resources because they only process a small portion of your site. For a site with 1500+ pages, a partial build could take approximately 9 seconds. This means your content goes live almost instantly with no downtime for the user.
While partial builds are ideal for localized changes, a full build becomes necessary when the modifications affect global elements or require a complete regeneration of the site.
What it Rebuilds: A full build rebuilds the entire site. Every page, route, and static asset is regenerated from scratch.
When it's Used: This is typically reserved for global updates such as:
Changing global navigation: If your website's main menu or footer content is updated, this impacts every page, requiring a full rebuild to ensure consistency across the entire site. Layout or theme changes: Any fundamental changes to the site's overall layout, CSS, or shared components may also trigger a full build.
Speed and Resource Usage: Full builds are slower and more resource-heavy compared to partial builds. For the same 1500+ page example, a full build could take around 40 seconds. While slower, it's sometimes a necessary step to ensure site-wide consistency.
Once the build logic determines what type of build is needed, it triggers the actual build process.
Here's what happens during the final deployment phase: The site is built using the updated content. The output is pushed into the serve directory or the target static host folder. This directory is what your CDN or server uses to serve the site. Once deployed, the updated content becomes live without affecting other parts of the site. In partial builds, only the specific page or route is updated, meaning no downtime or full-site redeploy. This ensures fast updates, high availability, and a smooth user experience. This setup works smoothly without much manual effort. It's reliable, efficient, and easy to maintain. Ultimately, we've enabled a workflow that makes content changes fast, safe, and scalable without relying on full redeployments every time.
The problem we started with was straightforward: static sites are fast but updating them shouldn't require rebuilding everything from scratch every time a content editor makes a change. The five-component pipeline we built with Next.js and Strapi webhooks solves exactly that. Changes are captured the moment they happen, queued reliably, and processed in batches. The build system handles the rest, whether that means regenerating one page in 9 seconds or the entire site in 40.
The result is a workflow that gives content teams the freedom to publish without waiting, and gives developers a system that doesn't buckle under the weight of frequent updates. Static performance stays intact. Content stays current. And the pipeline runs without much manual effort once it's in place.
Ever clicked a button in a web app, and the UI just froze? Maybe a timer stopped, animations stuttered, or clickable elements stopped responding. This happens because JavaScript is single-threaded, meaning it can only do one thing at a time.
When a heavy computation runs on the main thread, it blocks the UI, creating a poor user experience.
In this blog, we’ll explore Web Workers, a simple and effective way to offload heavy computations to a background thread in a React app, keeping your UI smooth and responsive.
JavaScript runs on a single thread that handles both:
This means if you run a heavy task, everything else stops until it finishes. Here’s a classic example:
// Blocks UI for several seconds
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const result = fibonacci(44);
console.log(result)
If this runs on the main thread, timers, animations, and clicks stop responding.
Web Workers allow us to run JavaScript code in a separate background thread. This way:
Note: Workers cannot access the DOM directly; they are only for computations.
Worker File (public/worker.js):
self.onmessage = function (e) {
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const result = fibonacci(e.data);
self.postMessage(result);
};
Main Thread (React Component):
const workerRef = useRef(null);
useEffect(() => {
workerRef.current = new Worker("/worker.js");
workerRef.current.onmessage = (e) => {
alert('worker finished! Result : ',e.data);
};
return () => {
workerRef.current.terminate();
};
}, []);
const runWorkerTask = () => {
workerRef.current.postMessage(44);
};
Setting up a Next.js Project with Web Workers
1. Create Next.js App

2. Project Folder Structure
Here’s a minimal structure we’ll use:

Create WithoutWorker Component File (app/WithoutWorker.js)
'use client';
import { useEffect, useState } from 'react';
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
export default function WithoutWorker() {
const [timer, setTimer] = useState(0);
const [color, setColor] = useState('lightblue');
const [size, setSize] = useState(100);
useEffect(() => {
const interval = setInterval(() => setTimer((t) => t + 1), 1000);
return () => clearInterval(interval);
}, []);
const handleHeavyTask = async () => {
console.log('handleHeavyTask started...');
const result = fibonacci(44); // Heavy enough to lag most browsers
console.log('handleHeavyTask ended... : ', result);
};
const animateBox = () => {
// Try clicking this during a heavy task, no response
setColor((prev) => (prev === 'lightblue' ? 'lightcoral': 'lightblue'));
setSize((prev) => (prev === 100 ? 550 : 100));
};
return (
<div>
<h2>Without Web Worker</h2>
<p>Timer: {timer}</p>
<button
onClick={handleHeavyTask}
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
>
Run Heavy Task
</button>
<div
onClick={animateBox}
style={{
marginTop: '20px',
width: `${size}px`,
height: `${size}px`,
backgroundColor: colour,
cursor: 'pointer',
transition: 'all 0.3s ease',
}}
>
Click Me
</div>
</div>
);
}
Observation: Click the box while running the heavy task , nothing happens. The timer also freezes.

Create Worker File (public/worker.js)
self.onmessage = function (e) {
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const result = fibonacci(44);
self.postMessage(result);
};
Create WithWorker Component File (app/WithWorker.js)
"use client"
import { useEffect, useRef, useState } from "react";
export default function WithWorker() {
const [timer, setTimer] = useState(0);
const [color, setColor] = useState("lightgreen");
const [size, setSize] = useState(100);
const workerRef = useRef(null);
useEffect(() => {
const interval = setInterval(() => setTimer(t => t + 1), 1000);
return () => clearInterval(interval);
}, []);
useEffect(() => { // Create the worker on mount
workerRef.current = new Worker("/worker.js"); // Creates a new background thread by loading our worker script.
workerRef.current.onmessage = (e) => { // Listens for the result from the worker.
console.log('handleHeavyTask ended... : ',e.data)
};
return () => {
workerRef.current.terminate(); // Cleans up the worker on component unmount.
};
}, []);
const runWorkerTask = () => {
console.log('handleHeavyTask started...')
workerRef.current.postMessage(44); // Sends a message to the worker to begin processing.
};
const animateBox = () => { // This should stay responsive even during heavy task
setColor((prev) => (prev === "lightgreen" ? "orange" : "lightgreen"));
setSize((prev) => (prev === 100 ? 550 : 100));
};
return (
<div>
<h2>With Web Worker</h2>
<p>Timer: {timer}</p>
<button
onClick={runWorkerTask}
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800"
>Run Heavy Task</button>
<div
onClick={animateBox}
style={{
marginTop: "20px",
width: `${size}px`,
height: `${size}px`,
backgroundColor: colour,
cursor: "pointer",
transition: "all 0.3s ease"
}}
>
Click Me
</div>
</div>
);
}
.png)
Observation: Click the box while the worker runs, the timer keeps going, and the box responds instantly!
You can attach an onerror listener when creating the worker:
useEffect(() => { // Create the worker on mount
workerRef.current = new Worker("/worker.js"); // Creates a new background thread by loading our worker script.
workerRef.current.onmessage = (e) => { // Listens for the result from the worker.
console.log('handleHeavyTask ended... : ',e.data)
};
workerRef.current.onerror = (err) => { // Error handler
console.error("Worker error:", err.message);
alert("Something went wrong in the worker: " + err.message);
};
return () => {
workerRef.current.terminate(); // Cleans up the worker on component unmount.
};
}, []);
In your worker.js, wrap computation in a try/catch:
self.onmessage = function (e) {
try {
const n = e.data;
if (typeof n !== "number" || n < 0) {
throw new Error("Invalid input for Fibonacci");
}
function fibonacci(x) {
if (x <= 1) return x;
return fibonacci(x - 1) + fibonacci(x - 2);
}
const result = fibonacci(n);
self.postMessage(result);
} catch (err) {
// Send error back to main thread
self.postMessage({ error: err.message });
}
};
Web Workers keep React apps responsive by moving heavy computations off the main thread, ensuring smooth user experiences even under load. They are already valuable in finance, simulations, and real-time communication, and their role will expand as WebAssembly, progressive web apps, and edge computing mature.
Countries like Singapore, South Korea, and Estonia are leading in digital infrastructure, while global research explores energy-efficient and privacy-focused applications. Future directions include integration with in-browser AI, distributed multi-worker systems, and secure collaboration protocols.
In my view, Web Workers are still underused in React, but they are on track to become a standard tool for performance-driven development.

Many businesses still operate on JavaScript-heavy platforms built years ago. They work on the surface, but under the hood, they slow down under load, fail Core Web Vitals checks, and cost more to maintain than they should.
The rules have shifted. Google now uses Interaction to Next Paint (INP) as a ranking factor, flagging any interaction above 200 milliseconds as poor (Google Web.dev). That means slow responsiveness is no longer just a technical issue; it is a visibility and revenue issue. Users expect instant performance across devices and abandon products that cannot keep up.
At the same time, AI is transforming delivery pipelines. Teams that embed it are cutting release cycles by up to 30 per cent and reducing technical debt remediation by 40 to 50 per cent (McKinsey). Security and compliance expectations are stricter than ever (CISA). Budgets are under pressure, and product leaders are asked to deliver more with less.
The framework choice is secondary. The real question is whether JavaScript delivery produces visibility, lowers costs, and reduces risk.

Applications that miss Core Web Vitals lose visibility and customers. Research shows that every 100ms delay in load time can reduce conversions by up to 7 per cent (Akamai). Many older single-page apps, designed for a different era, simply cannot pass INP thresholds. This is not just about faster websites; it is about protecting top-line revenue.
A Stripe study revealed that developers spend 42 per cent of their time dealing with technical debt. Every hour spent patching legacy code is an hour not spent on features that drive growth. The compounding effect is brutal: delays in new launches, growing maintenance overhead, and higher developer turnover. Businesses that fail to address this stall their ability to innovate.
High-profile supply chain attacks such as SolarWinds and malicious npm packages show how easily vulnerabilities slip into production. With regulations tightening globally (GDPR, HIPAA, PCI DSS), compliance failures can result in heavy fines and reputational damage. Security must be embedded in pipelines and runtime, not bolted on later.
Running multiple brand websites on separate stacks is expensive and inefficient. Each new launch duplicates effort, inflates hosting bills, and slows expansion. In one case, consolidating 12 fragmented sites into a modular JavaScript platform reduced infrastructure costs by 28 per cent and shortened launch timelines from months to weeks. Businesses that ignore infra optimisation risk ballooning OPEX.
Without standardised design systems, teams rebuild the same components repeatedly. This leads to inconsistencies across platforms, longer delivery times, and higher costs. Gartner has identified design systems as a key strategy for scaling product delivery effectively (Gartner). For global businesses, fragmented experiences are more than a design issue; they directly impact customer trust and brand equity.

React, Angular, Vue, and Svelte are used to build modular, performant apps. Component-driven architecture and asset budgets deliver 80+ Lighthouse scores on mobile as a baseline.
Example: A financial services platform adopted this approach, combined with AI-assisted testing, and reduced regressions significantly, making releases more predictable and performance stable.
Storybook, Tailwind, Radix UI, and Atomic Design principles accelerate delivery by 25–40 per cent while ensuring consistent branding across regions and devices.
Example: A hospitality company used this approach to launch three regional websites in half the time, without diluting experience or brand identity.
Node.js, NestJS, and Express power robust, secure APIs. Serverless computing reduces infrastructure overhead while ensuring scalability. GraphQL improves developer productivity and system interoperability.
JAMstack approaches with Strapi, Ghost, or Contentful, combined with Next.js, deliver faster content updates and lower infra costs. Example: A global non-profit delivering climate hazard data to governments used headless architecture to serve real-time interactive dashboards with faster performance and easier updates.
GenAI is integrated directly into delivery: scaffolding, testing, and remediation. Human oversight ensures quality while reducing delivery cycles by 20–30 per cent and cutting technical debt remediation time by 40–50 per cent (Harvard Business Review).
Example: A media company embedded AI into QA workflows, reducing regression issues by half and improving release velocity.
React Native and Flutter enable mobile apps with one codebase. Feature parity across iOS and Android is faster, with lower maintenance overhead.
Example: A quick-commerce company integrated a headless CMS with React Native, enabling marketers to publish content directly to mobile apps without developer bottlenecks. This reduced turnaround time and increased release cadence.
CI/CD with GitHub Actions or GitLab CI, observability with Datadog and Prometheus, and Infrastructure as Code ensure predictable, secure deployments. Autoscaling reduces costs by up to 30 per cent while maintaining reliability.

Consider a global beverage brand. A new product launch requires detailed information such as nutritional values, packaging visuals, and compliance notes. That content is first published on the corporate website, then re-entered into the mobile ordering app, reformatted for in-store digital displays, and again adapted for marketing campaigns.
Every update, even something as simple as a revised label or a new allergen disclosure, has to pass through different teams and channels. This creates bottlenecks, introduces the risk of inconsistencies, and slows down time to market.
With a headless CMS, the product information authored once can flow through APIs into the website, the app, the in-store displays, and the campaigns. Updates made at the source immediately reach every channel, reducing duplication and keeping the brand consistent everywhere it appears.
A headless CMS stores content in a single repository and delivers it to different platforms through APIs. Unlike traditional systems, it is not bound to a fixed website layer.
This separation between content and design means businesses can choose the frameworks, tools, and delivery channels that best fit their needs.
A set of product specifications in manufacturing, a set of treatment guidelines in healthcare, or a catalogue of items in retail can be authored once and then published across the corporate site, mobile apps, in-store displays, and campaigns. Developers gain freedom to shape each channel as required, while the content team works from one trusted source.
The shift to Headless is clearer when compared with other CMS models:
React remains the foundation of modern web development. React 19 is now stable and available on npm, bringing significant changes to how developers build applications.
The release solves long-standing complexity issues in React development. Tasks like fetching product data, syncing user preferences, and updating inventory previously required extensive boilerplate code with useEffect hooks and loading states. React 19's Server Components and built-in data fetching handle these operations automatically.
Early adopters report 40% faster development cycles and 25% smaller bundle sizes. Companies including Airbnb and Netflix have started migrations, with some teams seeing 60% improvements in time-to-interactive metrics.
React 19 introduces automatic batching improvements, enhanced Concurrent Features, and better TypeScript integration. These updates represent the framework's biggest evolution since hooks launched in 2018, benefiting everything from startups to enterprise applications serving millions of users.
The React Compiler is a new JavaScript compiler introduced in React 19 that automatically optimises your components by:
The React Compiler looks at your component code at build time, understands how values change, and generates optimised output that skips re-rendering parts of your component tree unless needed.
Before React 19 (manual optimisation):
import { useMemo, useCallback } from 'react';
function ExpensiveComponent({ items, onItemClick }) {
// Manual memoization required
const expensiveValue = useMemo(() => {
return items.reduce((sum, item) => sum + item.value, 0);
}, [items]);
// Manual callback memoization
const handleClick = useCallback((id) => {
onItemClick(id);
}, [onItemClick]);
return (
<div>
<p>Total: {expensiveValue}</p>
{items.map(item => (
<button key={item.id} onClick={() => handleClick(item.id)}>
{item.name}
</button>
))}
</div>
);
}
With React 19 compiler (automatic optimisation):
function ExpensiveComponent({ items, onItemClick }) {
// No manual memoization needed - compiler handles it!
const expensiveValue = items.reduce((sum, item) => sum + item.value, 0);
const handleClick = (id) => {
onItemClick(id);
};
return (
<div>
<p>Total: {expensiveValue}</p>
{items.map(item => (
<button key={item.id} onClick={() => handleClick(item.id)}>
{item.name}
</button>
))}
</div>
)
}
Compatible with:
The React Compiler is the future of how React apps will be written ,with less boilerplate, better performance, and simpler mental models.
The Actions API in React 19 is a powerful new feature that simplifies asynchronous operations, especially for form submissions, mutations, and server-side logic ,without needing client-side useEffect, fetch calls, or state management for each action.
It’s a core part of React Server Components, but it works seamlessly with client components too.
The Actions API allows you to define server functions (aka Server Actions) that are directly callable from forms or JS, and React automatically handles:
Example: basic server action with form
1. Define a server action
// app/actions.ts
'use server'
export async function sendMessage(_, formData) {
const name = formData.get('name');
const message = formData.get('message');
// Save to DB or send email...
console.log(Message from ${name}: ${message}`);
return { status: 'ok', message: 'Message sent!' };
}
formData is automatically parsed, no manual wiring.
2. Use it in a client component
'use client'
import { useActionState } from 'react'
import { sendMessage } from './actions'
export default function ContactForm() {
const [state, formAction, isPending] = useActionState( sendMessage,{ status: null, message: });
return (
<form action={formAction}>
<input
name="name"
placeholder="Your name"
required
/>
<textarea
name="message"
placeholder="Your message"
Required
/>
<button
type="submit"
disabled={isPending}
>
{isPending? 'Sending...' : 'Send'}
</button>
{state?.message && <p>{state.message}</p>}
</form>
);
}
1.No API routes
2.No client-side fetch()
3.Clean async form state handling
Advanced: combine with useTransition()
'use client'
const [isPending, startTransition] = useTransition();
function handleSubmit (formData) {
startTransition (async () => {
await sendMessage(null, formData);
});
}
Example: form submission with actions
import { useActionState } from 'react';
async function updateName(prevState, formData) {
const name = formData.get('name');
try {
await fetch('/api/update-name', {
method: 'POST',
body: JSON.stringify({ name }),
});
return { success: true, message: 'Name updated successfully!' };
} catch (error) {
return { success: false, message: 'Failed to update name' };
}
}
function NameForm() {
const [state, formAction, isPending] = useActionState(updateName, null);
return (
<form action={formAction}>
<input name="name" placeholder="Enter your name" />
<button type="submit" disabled={isPending}>
{isPending? 'Updating...' : 'Update Name'}
</button>
{state?.message && (
<p style={{ color: state. success? 'green' : 'red' }}>
{state.message}
</p>
)}
</form>
);
}
Optimistic Updates with Actions:
import { useOptimistic } from 'react';
function TodoList({ todos, addTodo }) {
const [optimisticTodos, addOptimisticTodo]=useOptimistic(todos,
(state, newTodo) => [...state, {...newTodo, pending: true }]
);
}
async function handleAddTodo (formData) {
const title = formData.get('title');
const newTodo = { id: Date.now(), title, completed: false };
// Optimistically add the todo
addOptimisticTodo (newTodo);
// Perform the actual update
await addTodo (newTodo);
return (
<div>
<form action={handleAddTodo}>
<input name="title" placeholder="Add a todo" />
<button type="submit">Add Todo</button>
</form>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.pending? 0.5: 1 }}>
{todo.title}
</li>
))}
</ul>
</div>
);
}
In React 19, Server Components get a major upgrade, making them more powerful, flexible, and production-ready. They are now tightly integrated with React's new features like the Actions API, metadata support, and React Compiler.
Server Components are React components that:
They improve performance by reducing bundle size and moving work off the client.
Server Components are one of the biggest changes in React 19, providing a new way to render components on the server and deliver a faster, more efficient user experience. The improvements to React Server Components in version 19 bring significant performance benefits:
Example: Simple server component
// app/pages/about/page.tsx
export default async function AboutPage() {
const data = await fetch('https://api.example.com/team')
.then(res => res.json());
return (
<>
<title>About Us</title>
<h1>Our Team</h1>
<ul>
{data.members.map(member => (
<li key={member.id}>{member.name}</li>
))}
</ul>
</>
)
}
No client-side JS
Can use async/await
Supports <title> natively
Where to Use Server Components
Use Server Components for:
Composing with client components
// Server Component
import ClientWidget from './ClientWidget';
export default async function DashboardPage() {
const stats = await getStats();
return (
<>
<h1>Dashboard</h1>
<ClientWidget stats={stats} />
</>
);
}
ClientWidget is marked with "use client" and can handle interactivity.
Enhanced streaming in React 19
React 19 improves streaming rendering, which means:
Server Components can stream chunks of HTML progressively.
<Suspense> works for async server logic.
Reduces Time-to-First-Byte (TTFB).
<Suspense fallback={<Loading />}>
<ExpensiveServerComponent />
</Suspense>
Server Components:
To use Enhanced Server Components in React 19:
React 19’s Server Components enable a leaner, faster, and more scalable React architecture ,especially for large apps, content-heavy sites, and hybrid rendering.
use is a React 19 built-in hook that lets you:
Example 1: Async data fetching (server component)
import { use } from "react";
const userPromise = fetch("https://api.example.com/user/123")
.then((res) => res.json());
export default function ProfilePage() {
const user = use(userPromise);
return (
<>
<title>{user.name}</title>
<h1>Welcome, {user.name}</h1>
</>
);
}
React suspends automatically until the promise resolves
No loading states or effects needed unless you want them
Example 2: Reading context in server component
// ThemeContext.ts
export const ThemeContext = createContext("light");
// Layout.tsx (Server Component)
import { use } from "react";
import {ThemeContext } from "./ThemeContext";
export default function Layout({ children }) {
const theme = use (ThemeContext);
return (
<div className={`theme-${theme}`}>
{children}
</div>;
)
}
No useContext() needed
Works only on the server (for now)
Advanced: use() with suspense
<Suspense fallback={<Loading />}>
<ExpensiveComponent />
</Suspense>
Example : ExpensiveComponent.tsx
export default function ExpensiveComponent(){
Const data = use(fetch(‘/api/heavy’)
.then(res => res.json());
return <div>{data.value}</div>;
}
React will suspend ExpensiveComponent until the fetch resolves, showing the fallback in the meantime.
The JSX transform receives major enhancements that make it not only more ergonomic but also faster and smarter during compilation and bundling.
The JSX transform receives significant enhancements in React 19, including:
1. Using ref as a Prop directly (no more forwardRef everywhere)
Before (React 18 and earlier)
To pass a ref to a child component, you needed to wrap it in React.forwardRef:
const MyInput = React.forwardRef((props,ref)=>{
return <input ref={ref} {...props} />;
})
Now in React 19:
You can pass ref directly as a JSX prop, and React automatically handles it via the compiler:
function MyInput(props){
return <input {...props} />;
}
export default function Form(){
Const ref = useRef();
return <MyInout ref={ref} />;
}
React Compiler will rewrite this safely behind the scenes, no need for boilerplate!
New ref Prop usage:
function CustomInput ({ref,...props}) {
return <input ref={ref} {...props} />
}
function App() {
const inputRef = useRef(null);
return (
<div>
{/* ref can now be passed as a regular prop*/}
<CustomInput ref={inputRef} placeholder="Type here…" />
<button onClick={()=>inputRef.current?.focus()}>
Focus Input
</button>
</div>
)
}
Before React 19 (forwardRef required):
import {forwardRef} from 'react'
//Previously required forwardRef wrapper
const CustomInput = forwardref(function CustomInput(props,ref){
return <input ref={ref} {...props} />
});
function App() {
const inputRef = useRef(null);
return (
<div>
<CustomInput ref={inputRef} placeholder=”Type here…” />
<button onClick={()=>inputRef.current?.focus()}>
Focus Input
</button>
</div>
)
}
2. Performance improvements in JSX compilation
JSX in React 19 is now:
Example :
<MyComponent prop1={x} prop2={y} />
If x and y are stable, React skips re-rendering entirely without needing memo().
You write JSX normally, but get the performance of fine-tuned memoisation.
In React 19, adding support for using async functions in transitions to handle pending states, errors, forms, and optimistic updates automatically.
useTransition lets you mark a state update as low-priority. React can then interrupt or delay it to keep the UI responsive.
When you want to defer a heavy update (like filtering a big list) so that more urgent updates (like typing in an input) stay fast and responsive.
List filter With transition
import { useState, useTransition } from "react";
function SearchComponent({ data }) {
const [input, setInput] = useState("");
const [filteredData, setFilteredData] = useState(data);
const [isPending, startTransition] = useTransition();
const handleChange = (e) => {
const value = e.target.value;
setInput(value); // High-priority update
startTransition(() => {
const filtered = data.filter(item => item. includes (value));
setFilteredData(filtered); // Low-priority update
});
};
return (
<>
<input value={input} onChange={handleChange} />
{isPending && <p>Updating list...</p>}
<ul>
{filteredData.map((item, i) => <li key={i}>{item}</li>)}
</ul>
</>
)
}
useTransition() - Defer non-urgent state updates isPending - Shows loading state during transition
startTransition(fn) - Marks a state update as low-priority
In React 19, the compiler optimisations and improved scheduling make useTransition more powerful:
No, useTransition doesn’t run code on a separate thread like a Web Worker. It just tells React:
“This update isn’t urgent, schedule it when the main thread isn’t busy.”
If you want true parallelism, use Web Workers ,but combine both for advanced apps.
In React 19, useActionState() is a new hook introduced to work with Server Actions, enabling you to manage the state of a form or async action that is submitted to the server.
It combines server-side actions with client-side state, similar to how you'd manage a form and submission result (success, error, pending) ,all in one place.
Syntax
const [state, formAction, isPending] = useActionState(actionFn, initialState);
actionFn: an async Server Action function (defined using the new async function model).
initialState: default state value (e.g., { message: '' })
state: current state from the action (response).
formAction: submit handler you can use in a <form action={formAction}>.
isPending: boolean indicating if the action is running.
Before React19
// actions.ts (React Server Component file)
"use server";
export async function loginAction (prevState, formData) {
const username = formData.get("username");
const password = formData.get("password");
if (username === "admin" && password === "1234") {
return { success: true, message: "Login successful!" };
} else {
return { success: false, message: "Invalid credentials" };
}
}
After React19
// LoginForm.tsx (Client Component)
"use client";
import { useActionState } from "react";
import { loginAction} from "./actions";
export default function LoginForm() {
const [state, formAction, isPending] = useActionState( loginAction,
{ success: null, message: "" }
);
return (
<form action={formAction} className="login-form">
<input type="text" name="username" placeholder="Username" required />
<input type="password" name="password" placeholder="Password" required />
<button type="submit" disabled={isPending}>
{isPending? "Logging in..." : "Login"}
</button>
{state.message && (
<p className={state.success ? "text-green" : "text-red"}>
{state.message}
</p>
)}
</form>
);
}
useActionState() helps you manage form state + server interaction in a unified way.
Works perfectly with React Server Actions introduced in React 19.
Best used in forms for login, signup, submit, etc. where the backend logic runs on the server.
useFormStatus() is a new hook designed to be used inside a form that uses Server Actions (React’s new server-side form handling). It helps track the status of a form submission ,whether it’s pending, successful, or failed.
It lets you read the submission status of a form from inside the form, which is very helpful for:
const status = useFormStatus();
status includes:
“use client”;
import { useFormStatus } from “react-dom”;
function SubmitButton() {
Const {pending} = useFormStatus();
return (
<button type=”submit” disabled={pending}>
{pending ? “Submitting…” : “Submit”}
</button>
)
}
Full Example with useFormStatus + useActionState
// Server Action
// action.ts (server file)
"use server";
export async function submitFormAction(prevState, formData){
await new Promise ((res) => setTimeout(res, 1000));
return {message: "Form submitted!" };
}
// Client Component
"use client";
import { useActionState, useFormStatus } from "react";
import {submitFormAction } from "./actions";
function SubmitButton() {
const {pending} = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? “Submitting…” : “Submit”}
</button>
)
}
export default function ContactForm() {
Const [state, formAction] = useActionState(submitFormAction,{ Message: " "});
return (
<form action={formAction}
<input type="text" name="name" placeholder="Your name" Required />
<SubmitButton />
{state.message && <p>{state.messgae}</p>}
</form>
)
}
Are we moving towards a future where we write less code or where we write code more intelligently?
At this year's React Nexus, held at IISc Bengaluru, felt like a bit of both. The sessions and conversations portrayed that React 19 and AI are transforming our development processes, and the way we build software is evolving fast.
By automating repetitive tasks and minimising boilerplate code, these tools allow us to concentrate on what really matters, creating performance-driven architectures, enabling inclusive user experiences, and building systems that endure over time.
For the 700 attendees, this was more than just another tech event. With 29 brilliantly sharp speakers sharing their insights, it became a space to really pause and think about where React is going, and where our whole industry might be headed.
Personally? It was unforgettable. My first-ever tech conference, and oddly enough, it didn’t feel like a formal event at all. It felt more like a reunion.
For the past 3 years, I’ve been working remotely at QED42, bonding with my team through Slack threads, Google Meet calls, and GitHub pull requests. Getting to meet my teammates from the Pune office again was something else entirely.


The first day was an incredible opportunity to understand fundamentals that scale. From exploring the React Compiler to understanding Fiber internals, each session was packed with insights you could immediately apply to real-world problems..
The event was run really well. Registration was quick, volunteers were on point, and everything just flowed smoothly.
The breaks for tea and lunch weren’t just a breather; they gave us a perfect chance to network with fellow developers and check out booths from companies like ImageKit, Asgardio, GoDaddy, Vonage, and Zoho Catalyst.
In her session on building better forms with React 19, Nipuni Paaris demonstrated how React 19 is transforming our approach to forms. With the introduction of new form actions and hooks, the era of tedious form boilerplate may finally be behind us. It's a relief for developers, seamlessly integrated into the framework.
Akash Hamirwasia shared what it’s like to adopt the new React Compiler. If performance tuning has ever taken over your workflow, this will feel like a breath of fresh air. The compiler now takes care of optimisations like memoisation, letting you stay focused on writing clear, expressive code.
Wadad Parker’s session on context was informative and full of personality. While prop drilling can feel like a workout, React 19’s revamped Context API streamlines state sharing, making it more efficient and reliable, with fewer unnecessary re-renders.
Padam Jeet Singh and Shruti Bansal from GoDaddy shared practical strategies for making micro frontends work at scale. Their session focused on building shared infrastructure, setting up strong communication between teams, and putting solid governance in place. With the right structure, micro frontends become a reliable way to grow large applications without losing control.
Alok Kumar Singh from Cashfree Payments gave a clear and thoughtful breakdown of React Fiber internals. He explained how React handles rendering through incremental, interruptible updates and how lane-based prioritisation helps manage what gets rendered and when. It offered a deeper look at the mechanics behind React’s responsiveness and performance.
Harshit Budhraja from ImageKit.io delivered a session full of practical tips for improving Lighthouse scores, especially for media-heavy applications. He walked through techniques like using responsive images and AI-powered transformations through ImageKit.io. The live demo showed how these optimisations can make a real difference in both speed and quality.
Apurv Khare from Adobe explored the difference between knowing something broke and understanding why it happened. His session on frontend observability covered how to use sampling-based telemetry, real-time profiling, and structured logs to gain deeper insights. The focus was on moving from reactive fixes to proactive improvements that keep systems healthy and predictable.
I tested every cursor trick, so you don’t have to!
In her session, Tanisha Sabherwal talked about how the cursor isn’t just an editor; it’s your coding companion, demonstrating how to leverage context-aware prompts and other cursor features to receive AI support that truly understands your project's structure.
Give your code editor real superpowers
In this lightning talk with Apoorv Taneja was incredibly engaging, highlighting how MCPs can transform your editor with amazing new capabilities.
After a full day of learning, we navigated the infamous Bengaluru traffic to enjoy a wonderful team dinner, energised by the ideas we had discussed throughout the day.

While Day 1 laid the groundwork, Day 2 propelled us into a future where AI is seamlessly integrated into development processes.
Chaitanya Deorukhkar from Razorpay showed how their design system, Blaze, works with an MCP server to generate React components directly from Figma. This setup brings design and development much closer, turning design files into production-ready code almost instantly.
Sanket Sahu from GeekyAnts walked us through the evolution of visual builders and introduced ShaperStudio, a new tool designed to connect design environments like Figma with developer tools like VS Code. It’s built to make collaboration between designers and developers smoother and more intuitive. We were so impressed, we caught up with him after the session and managed to get early access to try it out ourselves.
Building accessible UI with copilot – Navya Agarwal
Showed how Copilot can be an accessibility partner if you ask the right way. Copilot can help us address accessibility issues. Concluding with a powerful quote: “Accessibility isn't more work, the work was incomplete.”
Input accessibility deep dive with Shrilakshmi Shastry emphasised the importance of using proper ARIA labels and understanding when and how to apply them. She warned that incorrect use of ARIA can do more harm than good, stating, “No ARIA is better than bad ARIA.” Through clear examples, she demonstrated the proper application of these attributes.
These sessions fundamentally changed how I think about Accessibility and our ethical responsibilities as front-end developers.

The highlight of Day 2, and perhaps the entire conference for my team, was a moment of immense pride watching our colleague, Archana Agivale, Tech Lead at QED42, take the stage. Her talk, "Static Regeneration: Supercharging Next.js with Strapi Webhooks," was a masterclass in solving a real-world problem we all face.
She presented an elegant strategy for keeping static sites fresh without constant rebuilds by batching updates from Strapi via webhooks and using Next.js's Incremental Static Regeneration (ISR).
The result? Static sites with dynamic superpowers blazingly fast, always up-to-date, and incredibly efficient. It was a brilliant showcase of practical innovation, and seeing her share that expertise with the wider community was a fantastic experience.
It was captivating to listen to developers from Razorpay and Adobe discuss how AI is transforming our coding practices. The panel shared honest takes on both the potential and pitfalls of AI. While tools like Cursor, Copilot, and ChatGPT can enhance our productivity, they also require careful oversight and guidelines.
The day rounded out with more framework mastery from Tapas Adhikary on Next.js caching and Soumya Ranjan Mohanty on using Web Workers for smoother UIs, before Akshay Kumar U gave a demonstration of running a language model entirely on the client-side using React and WebLLM, paving the way for innovative, private AI features that operate directly on devices.
As I look back on my first-ever tech conference, a few core ideas stand out:
As Day 2 came to a close, so did one of the most memorable experiences of my career so far.
After the final session, our team met for dinner before my colleagues returned to Pune. It was the perfect pause to reflect on everything we had learned, discussed, and experienced over the past two days. The energy, the ideas, and the people all came together in a way that felt both inspiring and grounded.
React Nexus 2025 wasn’t just a showcase of what’s possible with React 19 and AI. It was a celebration of how far we’ve come and a glimpse into where we’re heading. More importantly, it was a reminder that the community sits at the heart of everything we do. Behind every new feature or tool is a group of people asking thoughtful questions, sharing what they’ve learned, and helping each other grow.
While i had met them before, this was the first time i truly got to spend meaningful time with them. After three years of remote collaboration, sitting around the same table and sharing stories felt incredibly meaningful.
As we wrapped up and said our goodbyes, I left feeling more connected, more curious, and more excited about what we get to build next.

In modern web development, automation plays a crucial role in enhancing productivity and ensuring applications run smoothly. One common and effective tool for automation is the cron job, which allows developers to schedule specific tasks to run at set intervals.
For content-driven applications, automation can simplify workflows such as publishing articles, clearing outdated entries, or syncing external data. Strapi, a popular headless CMS built with Node.js, supports cron jobs natively using straightforward JavaScript syntax, making it easier to set up recurring tasks, like scheduling blog posts to go live at a certain time or cleaning up draft content every night.
A cron job is a time-based task scheduler commonly used in Unix-like operating systems. It allows you to automate the execution of scripts or commands at specific times or intervals, such as every minute, hour, or day.
You can think of it as setting an alarm clock for your server, helping it perform routine tasks like sending email reports, clearing logs, or updating content without manual effort.

To configure and run cron jobs in Strapi:
💡 Tip: Optionally, cron jobs can be directly created in the cron.tasks key of the server configuration file.
Strapi supports defining cron jobs using two formats:
To define a cron job using the object format:
1. Create or edit the file at:
./config/cron-tasks.js // For Strapi v5
2. Use the following structure inside that file:
./config/cron-tasks.js
module.exports = {
/**
* Simple example.
* Every Monday at 1am.
*/
myJob: {
task: async({ strapi }) => {
// Add your own logic here (e.g. send a queue of email, create a database backup, etc.).
},
options: {
rule: "0 0 1 * * 1",
},
},
};
In this format:
The following cron job runs on a specific timezone:
./config/cron-tasks.js
module.exports = {
/**
* Cron job scheduled with timezone support.
* This task runs every Monday at 1:00 AM (Asia/Dhaka timezone).
* Reference for valid timezones:
* https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
*/
myJob: {
task: ({ strapi }) => {
// Add your own logic here (e.g. send a queue of email, create a database backup, etc.).
},
options: {
rule: '0 0 1 * * 1',
tz: 'Asia/Dhaka', // Timezone in which the job should execute
},
},
};
The following cron job is run only once at a given time:
./config/cron-tasks.js
module.exports = {
myJob: {
task: ({ strapi }) => {
/* Add your own logic here */
},
// only run once after 10 seconds
options: new Date(Date.now() + 10000),
},
};
The following cron job uses start and end times:
./config/cron-tasks.js
module.exports = {
myJob: {
task: ({ strapi }) => {
/* Add your own logic here */
},
options: {
rule: "* * * * * *", // Runs Every second
// start 10 seconds from now
start: new Date(Date.now() + 10000),
// end 20 seconds from now
end: new Date(Date.now() + 20000),
},
},
};
To define a cron job using the key format:
1. Create or edit the file at:
./config/cron-tasks.js // For Strapi v5
2. Use the following structure inside that file:
./config/cron-tasks.js
module.exports = {
/**
* Simple example.
* Every monday at 1am.
*/
"0 0 1 * * 1": ({ strapi }) => {
// Add your own logic here (e.g. send a queue of email, create a database backup, etc.).
},
};
In this format, the cron rule is used directly as the key instead of naming the job explicitly, resulting in the creation of an anonymous cron job.
Warning:
Using the key format to define cron jobs results in anonymous jobs, which may cause problems when attempting to disable them or when working with certain plugins. For improved control and compatibility, it's always advisable to use the object format instead.
To enable cron jobs in Strapi, set cron.enabled to true in the server configuration file and register your defined tasks as shown below:
// ./config/server.js
const cronTasks = require("./cron-tasks");
module.exports = ({ env }) => ({
host: env("HOST", "0.0.0.0"),
port: env.int("PORT", 1337),
cron: {
enabled: true,
tasks: cronTasks,
},
});
This setup ensures that Strapi will run the scheduled tasks defined in your /config/cron-tasks.js file.
You can dynamically manage cron jobs in Strapi using the built-in strapi.cron methods:
To programmatically add a cron job, use strapi.cron.add() within your plugin or custom code:
// ./src/plugins/my-plugin/strapi-server.js
module.exports = () => ({
bootstrap({ strapi }) {
strapi.cron.add({
myJob: {
// This job runs every second
task: ({ strapi }) => {
console.log("Hello from plugin");
},
options: {
rule: "* * * * * *",
},
},
});
},
});
To remove a cron job by its name, use strapi.cron.remove() and provide the key:
strapi.cron.remove("myJob");
Note: Cron jobs defined using the key format (i.e., where the rule itself is the key) cannot be removed.
To get a list of all active(currently running) cron jobs anywhere in your custom code, use:
strapi.cron.jobs
This will return the current cron jobs registered in the application.
A case where you want to allow your content editors to schedule blog posts to go live at a specific time in the future. Here's how we can achieve that with a cron job in Strapi.
Let's assume your BlogPost content type has these fields:
module.exports = ({ env }) => ({
cron: {
enabled: true,
},
});
module.exports = {
autoPublishBlogPosts: {
task: async ({ strapi }) => {
const now = new Date().toISOString();
const posts = await strapi.entityService.findMany('api::blog-post.blog-post', {
filters: {
publishAt: { $lte: now },
publishedAt: null, // Only unpublished posts
},
});
for (const post of posts) {
await strapi.entityService.update('api::blog-post.blog-post', post.id, {
data: {
publishedAt: new Date().toISOString(),
},
});
strapi.log.info(`Auto-published post: "${post.title}"`);
}
},
options: {
rule: '*/1 * * * *', // Runs every minute
},
},
};Cron jobs are essential in modern development. Paired with CMS platforms like Strapi, they handle content publishing, data syncing, and background tasks without extra tools.
Now AI is making them smarter.
Platforms like Cronly use real-time data like server load and user activity to schedule tasks at the best possible moment. They also detect errors and fix issues before they cause problems.
Strapi is used by over 2,000 companies in more than 50 countries. Teams across e-commerce, media, and tech rely on cron jobs to keep content fresh and systems running smoothly.
AI takes this further. It enables predictive publishing, real-time triggers, and personalised content delivery. According to PostWhale, CMS platforms are moving toward fully automated, AI-powered workflows.
Cron jobs aren’t outdated. They’re evolving into smart automation tools that make digital operations faster, lighter, and more reliable.

Building an APK locally with Expo gives you greater control over your development workflow, allowing you to generate a standalone Android package without depending on Expo’s cloud build services.
This approach is particularly valuable for developers who need faster iteration, offline capabilities, or want to customize their build process.
In this blog, we’ll walk you through the steps to build an APK locally using Expo, highlight the tools you'll need, and cover potential challenges you might face along the way.
Before you begin, ensure you have the following installed:
If you don’t already have an Expo project, create one with the following command:
npx create-expo-app myApp
cd myApp
By default, Expo manages builds in the cloud, but to generate an APK locally, we need to prebuild our project:
npx expo prebuild
This command converts your managed Expo project into a bare React Native project with the necessary Android and iOS folders.
To build an APK, use the following command inside the android directory:
cd android
./gradlew assembleRelease
This process might take a few minutes. Once completed, your APK will be located in:
android/app/build/outputs/apk/release/app-release.apk
For distribution, you should sign your APK. Generate a keystore using:
keytool -genkeypair -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-key-alias
Move the generated keystore to android/app/ and update the android/app/build.gradle file to reference your signing details.


Once the APK is built, install it on an Android device or emulator:
adb install android/app/build/outputs/apk/release/app-release.apk
You can now run and test your Expo-built Android application locally!
Alternative method: building APK locally with EAS build
EAS Build is available to anyone with an Expo account, regardless of whether you pay for EAS or use their Free plan. You can sign up at https://expo.dev/signup.
Expo also provides EAS Build (Expo Application Services), which allows you to build APKs locally without manually handling Gradle commands. Here’s how:
1. Install EAS CLI (if not installed already):
npm install -g eas-cli
2. Configure EAS for local builds:
eas build: configure
3. Run a local build:
Android: eas build --local --platform android
iOS: eas build --local --platform ios
Alternatively, you can use --platform all option to build for Android and iOS at the same time:
eas build --local --platform all
4. This command will build the APK on your local machine.
5. Find the generated APK:
If you are already signed in to an Expo account using Expo CLI, you can skip the steps described in this section. If you are not, run the following command to log in:
eas login
You can check whether you are logged in by running
eas whoamiTo configure an Android or an iOS project for EAS Build, run the following command:
eas build:configureWhile building an APK locally with Expo, you might encounter various challenges. Here are some common issues and how to resolve them:
Problem: The Build fails due to incompatible Gradle versions.
Solution: Update your Gradle version in android/gradle/wrapper/gradle-wrapper.properties.
Find this line:
distributionUrl=https\://services.gradle.org/distributions/gradle-x.x.x-all.zip
Update it to the latest stable version. You can find the latest version here.
Example:
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip

Update Gradle Plugin (If Needed) android/build.gradle
find classpath 'com.android.tools.build:gradle:x.x.x'
Update it to the latest version found in Android Gradle Plugin release notes.
Ensure your Android SDK is current.
Problem: Gradle build fails due to incompatible Java versions.
Solution: Ensure you are using the correct Java version (usually Java 11 or Java 17) by setting it in your environment variables.
Open a terminal or command prompt and run: java -version
If the version is not Java 11 or Java 17, you need to update it.
Install the Correct Java Version
Windows: Download & install Java from Adoptium or Oracle.
Mac/Linux: Use Homebrew (Mac) or SDKMAN (Linux) to install:
brew install openjdk@17
macOS/Linux
Open the Terminal and edit your shell config:
nano ~/.zshrc # (For macOS with zsh)
nano ~/.bashrc # (For Linux or bash)
Add this
export JAVA_HOME=$(/usr/libexec/java_home -v 17)
export PATH=$JAVA_HOME/bin:$PATH
Apply changes:
source ~/.zshrc # or source ~/.bashrc
Verify the version:
java --version
Set Java Version in gradle.properties
Open:android/gradle.properties
Add: org.gradle.java.home=/path/to/java17
(Replace /path/to/java17 with your actual Java installation path.)
Problem: Errors related to conflicting dependencies in package.json.
Solution: Run expo doctor to identify issues and resolve conflicts by upgrading/downgrading dependencies as needed.
Problem: The build process cannot locate the required Android SDK.
Solution: Install and configure the Android SDK in Android Studio and set ANDROID_HOME and ANDROID_SDK_ROOT environment variables.
Problem: The app crashes or doesn't build due to the Hermes engine.
Solution: Try disabling Hermes by modifying android/app/build.gradle or ensure you are using a compatible Hermes version.
Check if Hermes is enabled
Open:
android/app/build.gradle
Find this block:
project.ext.react = [enableHermes: true // Change to false to disable Hermes]
If enableHermes: true, try switching it to false and rebuilding.
Problem: Gradle takes too long to compile the project.
Solution: Enable Gradle caching and daemon mode, and use a more powerful machine for faster builds.
Enable gradle daemon & caching
1. Open:
~/.gradle/gradle.properties
(If it doesn’t exist, create it.)
2. Add or modify these lines:
org.gradle.daemon=true # Keeps Gradle running in the background for faster builds
3. Enable build caching
org.gradle.caching=true # Enables build caching
4. Enable parallel execution
org.gradle.parallel=true # Enables parallel execution
5. Configures projects on demand
org.gradle.configureondemand=true # Configures projects on demand
6. Limits worker processes (adjust based on CPU)
org.gradle.workers.max=4 # Limits worker processes (adjust based on CPU)
Use a faster gradle distribution
1. Open
android/gradle/wrapper/gradle-wrapper.properties
2. Update to the latest Gradle version (check Gradle Releases):
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
Allocate more RAM to gradle
1. Open
android/gradle.properties
2. Add
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -Dkotlin.daemon.jvm.options="-Xmx2g"(Increase -Xmx4g based on available RAM.)
Use incremental compilation
1. Open
android/build.gradle
2. Add this inside the android block:
android {...
compileOptions {
incremental true // Enables incremental Java compilation
}
}
Building APKs locally with Expo gives you complete control over your app’s build process, making it an excellent choice for offline development, thorough debugging, and distributing apps without depending on cloud-based services. This approach can be especially helpful when working in secure environments or when faster iteration cycles are needed.
However, it’s important to be prepared for some common hurdles along the way, such as Gradle version mismatches, dependency conflicts, or issues related to Android SDK configuration.
By identifying and addressing these challenges early, you can create a more reliable and streamlined build workflow.
With the right tools, attention to detail, and a clear understanding of the process, you can confidently build, test, and distribute your Expo-based Android applications entirely on your local machine.
Happy coding!
.avif)
Achieving strong Lighthouse scores starts with basic optimisations, but reaching exceptional performance takes more than default settings. While Next.js offers powerful tools out of the box, real results come from deliberate decisions across rendering, asset management, and Core Web Vitals.
This blog breaks down seven high-impact areas I focused on while improving performance in a real-world project. These targeted optimisations lead to significant Lighthouse improvements, from smart image handling and thoughtful rendering strategies to precise bundle analysis and fine-tuned font delivery.
You’ll get a practical sense of how to reduce layout shifts, cut bundle sizes, and debug Web Vitals effectively. Each technique blends Next.js capabilities with core web performance principles to help you cross the 90+ Lighthouse score threshold with confidence.
NOTE: All approaches highlighted are not router-specific and will work with both App router and page router.
Images often dominate page weight and hurt performance. By leveraging Next.js's <Image>component with modern formats, lazy loading, and responsive sizing, you can slash load times, eliminate layout shifts, and boost Lighthouse scores, without sacrificing visual quality. Here’s how to implement these optimisations effectively.
<Image
src="/example.jpg"
alt="Example"
width={800}
height={400}
sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 800px" //for responsiveness
priority={true} //to go against the default lazy loading image
quality={} //determine what quality to load the image in
/>
Pros:
Cons:
Choosing between Static Site Generation (SSG) and Server-Side Rendering (SSR) has a significant impact on performance, especially regarding Lighthouse scores, page speed, and scalability.
SSG (getStaticProps)
// Static Site Generation (SSG):
export default function Home() {
return <main>Static Content</main>
}
// For dynamic data fetching (SSG with data)
async function getData() {
const res = await fetch('<https://api.example.com/data>', {
cache: 'force-cache' //SSG
})
return res.json()
}
export default async function Page() {
const data = await getData()
return <main>{data.content}</main>
}
Pros:
Cons:
SSR (getServerSideProps)
Pros:
Cons:
// Server-Side Rendering (SSR):
async function getData() {
const res = await fetch('<https://api.example.com/data>', {
cache: 'no-store' // SSR
})
return res.json()
}
export default async function Page() {
const data = await getData()
return <main>{data.content}</main>
}
ISR (Incremental Static Regeneration) for Dynamic Content
// Incremental Static Regeneration (ISR):
async function getData() {
const res = await fetch('<https://api.example.com/data>', {
next: { revalidate: 3600 } // ISR: Revalidate every hour
})
return res.json()
}
export default async function Page() {
const data = await getData()
return <main>{data.content}</main>
}
Pros:
Cons:
Pros:
Cons:
import dynamic from "next/dynamic";
// Import component only when needed (client-side)
const HeavyComponent = dynamic(() => import("../components/HeavyComponent"), {
ssr: false, // Prevents server-side rendering for this component
});
export default function Page() {
return (
<div>
<h1>Code Splitting with Dynamic Import</h1>
<HeavyComponent />
</div>
);
}
Drupal is a mature and flexible content management system (CMS) trusted for building structured content models and managing complex workflows.
On the front end, Next.js is a widely adopted React framework known for its speed, developer experience, and production-grade features. With the release of Next.js 15, the new App Router brings powerful updates, including built-in support for async/streaming, granular caching, and improved routing patterns.
When connecting a decoupled Drupal backend to a Next.js 15 frontend, handling authentication becomes a key step, especially for gated content, personalisation, or editorial workflows.
This blog covers how to implement authentication in a headless Drupal + Next.js 15 setup using the latest App Router features and best practices around API routes, sessions, and token handling.
Before starting, ensure you have Node.js installed. Then, run the following command to create a new Next.js 15 project:
npx create-next-app@latest my-nextjs-app --ts --experimental-app
cd my-nextjs-app
npm install
To start the development server:
npm run dev
Your Next.js app should now be running at http://localhost:3000.
my-nextjs-app/
├── app/ # App Router directory
│ ├── layout.tsx # Root layout (applies to all pages)
│ ├── page.tsx # Home page (/)
│ ├── about/ # Example route: /about
│ │ └── page.tsx
│ └── api/ # API routes
│ └── user/route.ts
│
├── components/ # Reusable components (e.g., Header, Button)
│ └── Header.tsx
│
├── styles/ # Global and module CSS files
│ ├── globals.css
│ └── Home.module.css
│
├── public/ # Static assets (images, favicon, etc.)
│ └── favicon.ico
│
├── next.config.js # Next.js configuration
├── tsconfig.json # TypeScript configuration (if using TS)
└── package.json # Project metadata and dependencies
Install Drupal new setup
To enable API-based communication between Next.js and Drupal, install the JSON: API module:
ddev composer require drupal/jsonapi
ddev drush en jsonapi -y
To allow Next.js to access Drupal’s API, update services.yml:
cors.config:
enabled: true
allowedOrigins: ['*']
allowedHeaders: ['Content-Type', 'Authorisation']
allowedMethods: ['GET', 'POST', 'OPTIONS', 'PATCH', 'DELETE']
Clear the cache:
ddev drush cr
Next.js 15 introduces improved async handling and caching. To fetch content from Drupal, install the required packages:
npm install next-drupal drupal-jsonapi-params
Set the variables
# Required
NEXT_PUBLIC_DRUPAL_BASE_URL=https://my-site.ddev.site/
NEXT_IMAGE_DOMAIN=my-site.ddev.site
NEXT_PUBLIC_BASE_URL=http://localhost:3000
Update the image dome in next.config.ts file
Import type { NextConfig } from “next”;
const nextConfig: NextConfig = {
images: {
domains: ["my-site.ddev.site"], // Allow images from this domain
remotePatterns: [
{
protocol: "https",
hostname: "my-site.ddev.site",
pathname: "/sites/default/files/**",
},
],
},
};
export default nextConfig;Then, create a client to fetch data using the new fetch API with caching:
import { NextDrupal } from "next-drupal"
import { DrupalJsonApiParams } from "drupal-jsonapi-params";
const drupal = new NextDrupal(process.env.NEXT_PUBLIC_DRUPAL_BASE_URL);
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; // To fix the development fetch error
export async function fetchArticles() {
const params = new DrupalJsonApiParams()
.addFields("node--article", ["title", "id", "body", "field_image", "field_tags"])
.addInclude(["field_image", "field_tags"])
.addSort("created", "DESC");
const response = await drupal.getResourceCollection("node--article", {
params: params.getQueryObject(),
cache: "force-cache" // Ensures fast loading
});
return response;
}
Rendering the fetched content in Next.js 15:
import { fetchArticles } from "../lib/drupal";
import Image from "next/image";
export default async function Page() {
const data = await fetchArticles();
console.log(data);
return (
<div className="">
{data.map((article) => (
<div key={article.id || article.title}> {/* ✅ Add key to top-level item */}
<h2 className="article-title">{article.title}</h2>
{article.field_image?.uri?.url && (
<Image
src={`${process.env.NEXT_PUBLIC_DRUPAL_BASE_URL}${article.field_image.uri.url}`}
alt={article.title}
width={400}
height={250}
className="image"
/>
)}
{article.body?.value ? <p>{article.body.value}</p> : <p></p>}
{article.field_tags?.length > 0 && (
<ul>
{article.field_tags.map((tag, index) => (
<li key={tag.id || `${tag.name}-${index}`}>{tag.name}</li>
))}
</ul>
)}
</div>
))}
</div>
);
}
By integrating Drupal’s headless capabilities with Next.js 15 and its modern App Router, teams gain a powerful architecture that combines content flexibility with frontend performance.
Drupal continues to provide a structured, API-first backend for complex content models and editorial workflows, while Next.js 15 brings faster rendering through granular caching, improved async handling, and enhanced developer ergonomics.
This setup creates a future-ready foundation for scaling digital experiences. You can introduce personalisation, real-time updates, static generation for anonymous content, and even edge rendering with Next.js middleware, which also aligns well with the growing demand for composable architectures, enabling easy integration with third-party services like analytics, commerce, or AI-based recommendations.
As headless adoption grows, pairing Drupal with Next.js 15 offers a stable yet forward-looking path, supporting editorial flexibility, high performance, and extensibility for evolving digital products.
Web applications today are expected to respond instantly. Even minor lags between user action and content load can lead to user drop-off, higher bounce rates, and lost engagement.
At QED42, we’ve been experimenting with several ways to address these challenges, and one technology that has stood out is the Speculation API. This powerful tool has allowed us to solve critical performance bottlenecks, delivering faster and more seamless experiences to our users.
This article breaks down the performance bottlenecks we faced, why traditional methods fell short, and how the Speculation API helped us solve them—both in Vanilla JavaScript and in Next.js 15+ environments.
One of the most common performance issues we faced was slow navigation between pages. In a typical web application, when a user clicks a link, the browser must fetch the new page’s resources, such as HTML, CSS, and JavaScript. This process can take time, especially on slower networks or for pages with heavy assets. The delay between clicking a link and seeing the new page load often led to a poor user experience.
To address this, we initially relied on traditional techniques like <link rel="prefetch"> and <link rel="preload">. While these methods helped to some extent, they had significant limitations:
To demonstrate how we implemented the Speculation API, we’ll walk through examples in both Vanilla JavaScript and Next.js 15+. These examples reflect real-world scenarios where we used the API to solve performance challenges.
In one of our projects, we used the Speculation API to preload the next page when a user hovers over a link. Here’s how we did it:
// Check if the browser supports the Speculation API
if ('speculationRules' in document) {
// Define a rule to preload the next page when a link is hovered
const rule = {
source: "list",
urls: ["/next-page.html"],
actions: ["prefetch"],
};
// Apply the rule
document.speculationRules.add(rule);
}
In a Next.js 15+ project, we used the Speculation API to preload critical pages during the initial page load. Here’s how we implemented it:
Enable the Speculation API in next.config.js:
module.exports = {
experimental: {
speculationRules: true,
},
};
Add Speculation rules in a component:
import { useEffect } from 'react';
export default function Home() {
useEffect(() => {
if ('speculationRules' in document) {
const rule = {
source: "list",
urls: ["/about", "/contact"],
actions: ["prefetch"],
};
document.speculationRules.add(rule);
}
}, []);
return (
<div>
<h1>Welcome to the Home Page</h1>
<a href="/about">About Us</a>
<a href="/contact">Contact Us</a>
</div>
);
}
While the Speculation API has been a game-changer for us, it’s important to understand how it compares to other solutions:
.avif)
Software development works best when every feature is clear, tested, and built with purpose. Test-Driven Development (TDD) supports this by starting with tests before any code is written. Each test defines a specific behaviour, guiding the implementation and confirming that it works as intended.
For example, when building a login feature, a developer first writes a test for successful authentication. Only then do they write the code to make it pass. This approach keeps development focused and consistent, with built-in checks at every step.
TDD helps teams write cleaner code, avoid surprises, and move forward with confidence—one test at a time.
Test-Driven Development (TDD) is a development approach where writing tests comes before writing any functional code. The idea is to first define how a piece of software should behave by writing a test that will initially fail—because the code doesn’t exist yet. Then, the developer writes just enough code to pass that test. Once it passes, the code can be improved or extended, guided by additional tests.
This method keeps the development process focused, with each new feature or fix backed by a specific, testable outcome. It also helps catch issues early and makes sure that the codebase grows in a predictable, maintainable way.
The process follows a simple cycle called Red-Green-Refactor:
Here's a visual representation of the TDD cycle:
.avif)
In traditional development, developers write code first and test it afterwards. This often leads to situations where tests are written as an afterthought or skipped altogether, increasing the chances of undetected bugs and technical debt. Test-driven development (TDD) addresses these problems by enforcing a strict test-first approach, leading to better software quality and maintainability.
By writing tests before implementing functionality, developers are forced to think about the expected behaviour of the code. This reduces the likelihood of unexpected bugs and makes debugging easier. If a bug appears later, developers can quickly pinpoint the issue by running tests.
TDD forces developers to define the behaviour of a feature before writing its implementation. This ensures that the requirements are clear, unambiguous, and testable. It helps in avoiding scope creep and ensures that each feature is built with a well-defined purpose.
TDD aligns well with agile development methodologies, where incremental changes are made frequently. Automated tests allow developers to integrate new code confidently, ensuring that existing functionality is not broken when adding new features. This is particularly useful in Continuous Integration/Continuous Deployment (CI/CD) pipelines.
Since tests are written first, failures highlight exactly where the problem lies. This drastically reduces the time spent debugging and searching for errors. Developers no longer need to spend hours troubleshooting unexpected issues because failing tests guide them directly to the problem area.
Writing tests first encourages developers to break down their code into smaller, testable units. This results in:
TDD allows developers to refactor code with confidence. Since test cases already cover expected behaviours , developers can improve code structure without worrying about breaking existing functionality. If something breaks, the tests immediately notify them.
The cost of fixing a bug increases significantly the later it is found in the development cycle.
By catching issues early, TDD helps reduce software development costs and ensures a smoother development lifecycle.
The introduction of TDD fundamentally changes how software is written, tested, and maintained. It brings both technical and business benefits, ensuring long-term success for development teams.
By continuously testing the code, TDD guarantees that the application behaves as expected under different scenarios. The early detection of bugs and logical errors helps prevent software failures and improves reliability.
Although writing tests initially takes extra time, TDD saves time in the long run by reducing debugging efforts and regression issues. Developers gain confidence that their code works as intended, leading to a more productive workflow.
Developers tend to write only the necessary code to pass the test, avoiding unnecessary complexity.
With a well-defined set of tests, new developers can quickly understand the expected behaviour of different system parts. It also facilitates pair programming and code reviews, as tests document how different components interact.
Technical debt arises when developers take shortcuts to meet deadlines, leading to poor code quality and future maintenance nightmares. TDD mitigates this by ensuring:
Modern software development relies heavily on automation. TDD enables seamless integration with CI/CD pipelines, allowing teams to:
When a test fails, it immediately points to the exact part of the code that needs attention. This minimizes the time spent searching for issues and accelerates the debugging process.
Let’s walk through the TDD process by building a String Calculator that sums numbers given in a string format.
Create a new file stringCalculator.test.js and write the first test case:
const add = require('./stringCalculator');
test('returns 0 for an empty string', () => {
expect(add("")).toBe(0);
});
Create a new file stringCalculator.js and implement a minimal solution:
function add(numbers) { return 0; // Minimal implementation to make the test pass } module.exports = add;
Run the test using Jest:
npm test
The test should pass now.
Now, let's add a test for a single number input:
test('returns the number itself when a single number is passed', () => {
expect(add("5")).toBe(5);
});
Modify the add function:
function add(numbers) {
if (!numbers) return 0;
return parseInt(numbers); // Convert string to integer
}
Repeat the process for multiple numbers:
test('returns sum of two comma-separated numbers', () => {
expect(add("1,2")).toBe(3);
});
Modify add to handle multiple numbers:
function add(numbers) {
if (!numbers) return 0;
return numbers.split(',').reduce((sum, num) => sum + parseInt(num), 0);
}
Now, the function correctly adds numbers given as a string!
To implement TDD in a project using Jest:
npm init -y
npm install --save-dev jest
"scripts": {
"test": "jest"
}
npm test
Enhances reliability – Early bug detection reduces production issues.
Improves maintainability – Clear test cases serve as documentation.
Increases development speed in the long run – Less debugging and troubleshooting.
Encourages simplicity – Forces developers to write minimal and effective code.
However, TDD has some trade-offs:
Initial development might take longer due to writing tests first.
Requires discipline to follow the Red-Green-Refactor cycle.
Test-driven development (TDD) is gaining new momentum, supported by industry adoption and the rise of AI in software workflows. Mature agile teams are leading the way—70% of advanced teams use TDD, compared to just 15% of early-stage teams. Research also shows a 40% reduction in defect density when TDD is applied effectively.
AI is now amplifying these benefits. Razer’s Wyvrn platform includes an AI QA Copilot that reduces testing time by 50% and improves bug detection by 25%. In India, Tata Consultancy Services (TCS) reports that generative AI is accelerating engineering timelines by up to 20% in complex sectors like automotive.
This combination of TDD and AI is already improving stability for businesses, reliability for nonprofits, and traceability for legal systems. In my view, AI-assisted TDD won’t replace developers—it will simply raise the baseline of software quality. As adoption grows, this approach will likely become a standard across industries.

Getting your website noticed is no small feat, especially when competing with millions of others online. Search Engine Optimization (SEO) is what makes your site easier to find, helping it rank higher in search results and bring in more visitors.
Imagine you're building an online store with Next.js 14+ and TypeScript. You want customers to find your products when they search for them, and you also want your site to load fast and feel smooth.
This is where Next.js and TypeScript come in. With features like server-side rendering (SSR), dynamic routing, and automatic image optimization, Next.js helps you create websites that search engines love.
In this guide, we'll look at practical ways to boost SEO with Next.js and TypeScript. You’ll learn how to use SSR to deliver content faster, manage metadata to improve your page rankings, and optimize images so they load quickly without sacrificing quality. We'll also cover how dynamic routing can make your URLs more user-friendly, which is great for both search engines and visitors.
Whether you’re building a small personal blog or a feature-packed e-commerce site, these techniques will help you create a website that ranks higher, works better, and keeps visitors coming back.
Let’s dive into how you can combine the latest tools and techniques to make your website a success.
Search Engine Optimization, or SEO, is about making your website easier for search engines like Google to find and understand.
When your site is optimized for search engines, it has a better chance of showing up higher in search results when people look for topics related to your content. This can help your website attract more visitors, grow its audience, and achieve its purpose.
Think of search engines as librarians for the internet. When someone searches for something, the search engine scans its vast library of websites to find the best matches.
SEO helps your website stand out to these search engines by showing them that your site is trustworthy, relevant, and easy to navigate.
Next.js makes it easier to build SEO-friendly websites by providing tools like:
The initial step is to create a Next.js app configured with TypeScript. You can do this by running the following command:
npx create-next-app@latest seo-nextjs — typescript
This will set up a new Next.js project preconfigured for TypeScript. For more details, visit the Next.js documentation.
A well-structured website URL hierarchy can benefit our websites ranking. Clean and descriptive URLs improve both user experience and SEO. Next.js enables the creation of dynamic routes, making it simple to build SEO-friendly URLs.
Next.js uses file-system routing built on the concept of pages. When a file is added to the pages directory, it is automatically available as a route. The files and folders inside the pages directory can be used to define most common patterns.
Let’s take a look at a couple of simple URLs :

Homepage: https://www.example.com → app/page.tsx
Listings: https://www.example.com/product → app/product/page.tsx
Detail: https://www.example.com/product/1 → app/product/[productId]/page.tsx
Image optimization is a fundamental SEO factor. With Next.js, we can use the next/image component to ensure responsive image loading and efficient optimization. Don’t forget to provide descriptive alt attributes for our images to improve accessibility and SEO.
import Image from 'next/image';
export default function ImageComponent() {
return (
<Image
src='/img.png'
width={500}
height={500}
alt='alt-text'
/>
);
}
XML sitemaps provide search engines with a map of our website’s structure, making it easier for them to index our content. The next-sitemap package simplifies the generation of XML sitemaps.
Here’s an example where we generate sitemap entries for a homepage, a product listing page, and individual product pages dynamically fetched from an API:
import type { MetadataRoute } from 'next'
const getProducts = () => {... // Fetch and return Products
}
export default function sitemap(): MetadataRoute.Sitemap {
type SitemapEntry = {
url: string;
lastModified?: string | Date;
changeFrequency?: 'daily' | 'yearly' | 'always' | 'hourly' | 'weekly' | 'monthly' | 'never';
priority?: number;
};
const baseUrl = 'https://www.example.com';
const productUrl = '/product';
const productsRes = getProducts(); // Fetch products from an API
const productsData: SitemapEntry[] = productsRes?.map((productEle) => ({ // Map products to sitemap format
url: `${baseUrl}${productUrl}/${productEle?.slug}`,
lastModified: productEle?.createdDate || new Date(),
changeFrequency: 'daily',
priority: 1,
})) || [];
const sitemap: SitemapEntry[] = [ // Combine static and dynamic entries
{
url: baseUrl, // Homepage
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 1,
},
{
url: `${baseUrl}${productUrl}`, // Product Listing page
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 1,
},
...productsData, // Product Detail Pages
];
return sitemap;
}

In Next.js, a robots.txt file is used to provide instructions to search engine crawlers (e.g., Googlebot, Bingbot) about which pages or sections of your site should be indexed or ignored. It helps manage search engine visibility for your website.
Here’s an example how to configure a robots.txt file in a Next.js app using the MetadataRoute.Robots type provided by Next.js
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
const baseUrl = 'https://www.example.com';
return {
rules: {
userAgent: '*',
allow: ['/', '/product'],
disallow: '/private/',
},
sitemap: `${baseUrl}/sitemap.xml`,
}
}

The Metadata API in Next.js simplifies defining metadata for your application, such as meta tags and link tags, inside the HTML <head> element. This helps improve SEO and makes your pages more shareable on social media platforms.
You can export either:
These exports go in layout.js or page.js files associated with your routes.
Static metadata in Next.js refers to predefined, unchanging metadata values added to your application for SEO and social sharing purposes. These metadata values, such as titles, descriptions, and Open Graph tags, are set at build time and do not change dynamically based on runtime conditions.
Here’s an example of static metadata defined in a layout file in a Next.js application. It provides default metadata for the pages within the layout, ensuring consistency across the section of the app.
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = {
metadataBase: new URL('https://www.example.com'),
title: {
default: 'Example',
template: `%s | Example`,
},
keywords: ['keyword1', 'keyword2', 'keyword3'],
openGraph: {
description: 'This is a meta description...',
images: [''],
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
{children}
</body>
</html>
);
}
Contains metadata used for Open Graph protocol, which enhances link previews on platforms like Facebook and LinkedIn.

Dynamic metadata in Next.js allows you to programmatically generate and customize metadata values based on runtime data, such as page content, user preferences, or external APIs. Unlike static metadata, which is predefined and consistent across pages, dynamic metadata can change depending on the context, providing a more tailored experience for each page.
Dynamic metadata is especially useful for pages that rely on user input or content that changes frequently, such as blog posts, product details, or user profiles.
Here’s an example of dynamic metadata defined in a product file in a Next.js application. It provides dynamic metadata for the product detail pages based on data fetched from API.
import React from 'react';
import { notFound } from 'next/navigation';
export async function generateMetadata({ params }: { params: { productId: string } }){
try{
// read route params
const id = params?.productId;
// fetch product data
const response = await fetch(`https://.../${id}`).then((res) => res.json());
if(response?.length === 0){
return{
title: 'Not Found',
description: 'The page you are looking for does not exist'
}
}
return {
openGraph: {
title: response[0].title,
description: response[0].description,
images: [''],
},
};
} catch(error){
console.error(error);
return{
title: 'Not Found',
description: 'The page you are looking for does not exist'
}
}
}
const page = ({ params }: { params: { productId: string } }) => {
if (parseInt(params?.productId) > 5) {
notFound();
}
return (
<div>HTML code hoes here</div>
);
};
export default page;
Extract Route Parameters: The productId is extracted from params.
Fetch Product Data: Fetches data from API for the product using the productId.
Returns Dynamic Metadata:
If the product is not found (response?.length === 0), a fallback metadata object is returned with a “Not Found” title and description.
If the product is found, metadata is populated using the product data.
Error Handling: If any error occurs during the process, a fallback metadata object is returned.
SEO is essential for making your website visible to the right audience, and with the capabilities of Next.js 14+ and TypeScript, building an SEO-friendly web application has never been more straightforward.
From server-side rendering and static site generation to optimized images, dynamic routes, and detailed metadata management, Next.js provides all the tools you need to create high-performing websites that search engines love.
By combining these features with a thoughtful approach to your website’s structure, content, and accessibility, you can enhance your search engine rankings while delivering a seamless user experience.
Whether you’re crafting a small project or managing a large-scale application, leveraging the power of Next.js and TypeScript ensures your site is ready to meet modern SEO demands effectively.

Strapi is a powerful CMS that offers a variety of features out of the box, along with a robust plugin marketplace. However, there are times when you might need a custom solution to meet specific project requirements. Fortunately, Strapi makes it easy to extend its functionality through custom plugins. Some common use cases for this include creating a custom authentication provider, integrating with third-party APIs, or enhancing the admin panel's search functionalities.
In one of our projects, we needed to implement custom functionality, so we turned to the documentation and available examples. While there were plenty of examples, the documentation felt scattered and didn’t provide a clear, cohesive guide on how to get started with plugin development.
In this tutorial, we will walk through the steps of creating a simple plugin in Strapi. Our goal is to fetch configuration data from the plugin and display it on the admin panel.
Strapi plugins are structured in two parts: the backend (server folder) and the frontend (admin folder). We will create custom routes and expose APIs that can be accessed from Strapi’s frontend to retrieve data and trigger various events.
Before starting, ensure that you have Strapi installed and running. If you don't have it installed yet, you can create a new Strapi project with the following command:
yarn create strapi-app test-project-1 --quickstartPlugins in Strapi are located in the ./plugins directory of your project. To create a new plugin, use the CLI command below:
yarn strapi generate pluginAfter running this command, enter the desired plugin name. A new folder will be generated inside the /src/plugins/<plugin-name> directory.
This folder will consist of two subfolders: admin (for the UI, built with React.js) and server (for backend logic like routes, controllers, and services).
Note: Strapi uses a design system for its UI components, which you can explore here.
To enable the plugin, navigate to the config/plugins.js file and add the following configuration:
module.exports = () => ({
'test-plugin': {
enabled: true,
resolve: './src/plugins/test-plugin',
},
});This will enable the plugin and make it available in the Strapi admin panel. Once this is done, run the following command:
yarn run develop -- --watch-adminThen, open http://localhost:1337/admin/ in your browser, and you should see your plugin listed in the sidebar.

To display configuration data in the Strapi admin panel, we need to modify the config/plugins.js file. Plugins can have a config property where custom configurations can be defined and accessed throughout your plugin.
For example, let's add a configuration object to our my-plugin entry in the config/plugins.js file.
module.exports = () => ({
'test-plugin': {
enabled: true,
resolve: './src/plugins/test-plugin',
config: {
base_url: 'http://connect-to-third-party-api.com',
publicKey: '1234567890',
},
},
});First, let's create a route to fetch the configuration data. Open src/plugins/my-plugin/server/routes/index.js and add the following route definition:
module.exports = [
{
method: 'GET',
path: '/',
handler: 'myController.index',
config: {
policies: [],
},
},
{
method: 'GET',
path: '/get-configs',
handler: 'myController.configs',
config: {
policies: [],
},
},
];
Now, create a controller that handles the route we just defined. Go to src/plugins/my-plugin/server/controllers/my-controller.js and add this function:
'use strict';
module.exports = ({ strapi }) => ({
index(ctx) {
ctx.body = strapi.plugin('test-plugin').service('myService').getWelcomeMessage();
},
configs(ctx) {
console.log('called configs');
ctx.body = strapi.plugin('test-plugin').service('myService').getConfigs();
},
});
In this example, we're using the strapi.plugin() method to access the plugin's services, such as myService.
Side Note - Strapi provides several helper functions within the controller. For example, using strapi.plugin() and passing the plugin's name as a parameter, you can access the plugin’s services, middleware, and other data. To call a specific service, use the strapi.plugin('plugin-name').service('serviceName') method. The service name should match the file name in the services folder, using camelCase.
For instance, if the service file is named user-authentication-aws.js, you would access it with strapi.plugin('test-plugin').service('userAuthenticationAws').
Note: When generating a plugin, Strapi automatically creates key files such as the controller (my-controller.js) and service (my-service.js) files under the server folder
Next, define the service that the controller is using. In src/plugins/my-plugin/server/services/my-service.js, add the following:
'use strict';
module.exports = ({ strapi }) => ({
getWelcomeMessage() {
return 'Welcome to Strapi 🚀';
},
getConfigs() {
return strapi.config.get('plugin.test-plugin');
},
});
Now that the backend is set up, let's display this configuration data on the Strapi admin panel. Open the src/plugins/my-plugin/admin/src/pages/HomePage/index.js file and modify it as follows:
import React, { useEffect, useState } from 'react';
import pluginId from '../../pluginId';
import { useFetchClient } from '@strapi/helper-plugin';
const HomePage = () => {
const [configs, setConfigs] = useState({});
const client = useFetchClient();
useEffect(() => {
client
.get(`/my-plugin/get-configs`)
.then((response) => {
setConfigs(response.data);
})
.catch((error) => {
console.error('Error fetching configs:', error);
});
}, [client]);
return (
<div>
<h1>{pluginId} HomePage</h1>
<p>Configs: {JSON.stringify(configs)}</p>
</div>
);
};
export default HomePage;
This component makes a GET request to the /get-configs route we defined earlier and displays the configuration data.
To make the UI more presentable, we can utilize Strapi's design system components. Update the HomePage component as follows:
import React, { useEffect, useState } from 'react';
import pluginId from '../../pluginId';
import { useFetchClient } from '@strapi/helper-plugin';
import { Box, Tbody, Thead, Typography, Table, Tr, Th, Td } from '@strapi/design-system';
const HomePage = () => {
const [configs, setConfigs] = useState({});
const client = useFetchClient();
useEffect(() => {
client.get(`/test-plugin/get-configs`).then((response) => {
setConfigs(response.data);
});
}, []);
return (
<Box padding={6}>
<Typography variant="alpha">{pluginId} HomePage</Typography>
<Table colCount={2}>
<Thead>
<Tr>
<Th>Key</Th>
<Th>Value</Th>
</Tr>
</Thead>
<Tbody>
{Object.keys(configs).map((key) => (
<Tr key={key}>
<Td>{key}</Td>
<Td>{configs[key]}</Td>
</Tr>
))}
</Tbody>
</Table>
</Box>
);
};
export default HomePage;
Now, the configuration data will be displayed in a table format using Strapi's design system components.

To help visualize the plugin flow, the diagram below outlines how a request from the UI is processed in Strapi:
This flow demonstrates how requests move from the UI to the backend, invoking routes, controllers, and services within a Strapi plugin.

In this blog post, we've seen how to create a basic plugin in Strapi, add a custom configuration, and display that configuration data in the Strapi admin panel. This is just the start—there's much more you can do by extending your plugin with additional functionality and UI elements!
For more information on Strapi plugins, refer to the official documentation.
Happy coding!