• 14 min read

How to inspect React Server Component Activity with Next.js

As web developers, we've become accustomed to the incredible power of browser dev tools. Need to understand why a component isn't rendering correctly? Pop open the inspector! Want to trace a network request? The Network tab is your best friend. Debugging, profiling, and understanding frontend code has never been easier. Until now.

Then came React Server Components (RSCs), and suddenly, a significant chunk of our application logic vanished from the client side. Like magic, components render with data seemingly appearing out of thin air. While this shift brings performance benefits and cleaner code, it also presents a new challenge: How do we inspect and debug what's happening on the server?

Browser tools now offer limited visibility into our components' server-side execution. Network requests originating from the server are hidden, and component lifecycles within RSCs remain a mystery. This lack of transparency can make troubleshooting, performance optimization, and understanding data flow in our applications significantly more difficult.

This guide will examine React Server Components' behavior via OpenTelemetry. This powerful observability framework can illuminate the inner workings of our server-side components. We'll explore using OpenTelemetry to trace the activity using Next.js as a framework.

A Quick Dip into Server Components and the App Router

Before we explore OpenTelemetry, let's recap the fundamentals of React Server Components within the Next.js App Router. This new architecture offers a paradigm shift in how we build React applications, bringing server-side rendering to a new level.

Here's the gist:

  • Server Components (RSCs): These components live and execute solely on the server. They fetch data, perform computations, and render UI elements, sending only the necessary HTML to the client. This minimizes the amount of JavaScript shipped to the browser, resulting in faster initial page loads and improved performance.
  • Client Components: These are your traditional React components that run in the browser. They handle user interactions, manage client-side state, and can access browser APIs.
  • Next.js App Router: This new routing system in Next.js 13+ is designed to leverage the power of RSCs. It enables features like server-side data fetching, streaming, and layouts while seamlessly integrating with client components.

The beauty of this setup lies in its flexibility. You can render entire pages as RSCs or mix and match server and client components based on your needs. This allows you to optimize for performance by keeping heavy lifting on the server while maintaining interactivity on the client.

However, this separation also introduces the challenge of understanding the interplay between server and client. That's where OpenTelemetry comes in, providing the tools to trace the execution of your RSCs and gain valuable insights into their behavior.

Diagram showing how the initial page load can work with React Server Components and Next.js. Please note that this is a highly simplified representation.

Current Approaches to Inspecting RSCs without OpenTelemetry

Before we dive into the possibility of inspecting RSCs with OpenTelemetry, let's revisit how you can currently analyze their behavior without OpenTelemetry.

  • Debugger: You can use Node.js debuggers to debug RSC behavior locally. It is not entirely straightforward, but it can enable you to get down to the nitty-gritty details.
  • Console Logging: Of course, you can sprinkle console.log statements into your code.
  • React Developer Tools: React Developer Tools can still be helpful, but its functionality is limited for RSCs. You can inspect the component tree and view props, but you won't have access to the full range of debugging features available for client components.

The React Developer Tools showing how one of Dash0’s RSCs is represented within it. We are going to come back to this one for a case study later.

OpenTelemetry: Illuminating the Server-Side

OpenTelemetry (often shortened to OTel) is an open-source observability framework that provides a vendor-agnostic standard for instrumenting, generating, collecting, and exporting telemetry data. In simpler terms, it helps you understand how your application is performing by providing insights into its inner workings. So, it's what we are used to from the in-browser developer tools!

For web developers, OpenTelemetry offers a powerful set of tools to:

  • Trace requests: Follow the journey of a user request as it travels through your application, from the initial client-side interaction to server-side processing and back. This allows you to pinpoint bottlenecks and identify performance issues.
  • Collect metrics: Gather quantitative data about your application, such as response times, error rates, and resource usage. This helps you monitor the overall health of your system and identify areas for optimization.
  • Aggregate logs: Correlate logs from different parts of your application to gain a holistic view of events and troubleshoot issues more effectively.

So, how does this help with React Server Components?

OpenTelemetry, specifically with Next.js, allows you to trace the execution of your RSCs, providing a detailed view of what's happening on the server. This includes:

  • Lifecycle events: Track when components are rendered, and the timeline between page, layout and middleware.
  • Data fetching operations: Monitor data requests made by your RSCs, including database queries, API calls, and interactions with external services. This can help you identify slow queries or inefficient data fetching patterns.
  • Rendering performance: Measure the time it takes for RSCs to render and identify potential bottlenecks in your server-side logic.
  • Capturing logging output: See the logging output in a centralized space.

Think of OpenTelemetry as the evolution of the Network tab but with a focus on server-side activity. While the Network tab provides insights into client-side network requests, OpenTelemetry gives you a deeper understanding of the entire request lifecycle, including server-side processing, data fetching, and rendering.

What you get, based on a real-life case study

Before we discuss the setup instructions, let's consider a small real-life usage example based on a feature we recently released that required optimization (yeah, not an unrealistic demo app 🎉). We were about ready to ship our integrations hub, but we noticed that the landing page's performance (an RSC) left much to be desired.

The great thing about working with RSCs is that you can async/await to your heart's content. The bad thing is that you can async/await to your heart's content 🙃.

In our case, we are loading many files through the GitHub API, as that is where the content of the integrations hub lives. During development, the integrations hub had very little content, so these individual file retrievals didn't have a notable impact. However, before this capability was shipped, a lot of content was produced, increasing the page's latency. As a result, the implementation was starting to show how naive it was.

Now, enough words: What results can you expect from integrating OpenTelemetry with RSCs and Next.js?

A trace of a React Server Component served via Next.js App Router in Dash0. Showing a variety of middleware, routing and GitHub fetch activities.

The image above is a screenshot showing a distributed trace of the routing activity in Next.js visualized in Dash0. A lot is happening here, from Next.js middleware execution and Clerk authentication checks to component loading and Dash0/integrations hub-specific logic. For all of these rows (each row is represented by a span – OpenTelemetry's data model for such activity), you can access much more information that is visible through the sidebar. It is not quite an in-browser network tab and also not React Developer Tools. It is something new. And it is powerful.

It can be easy to get lost in the details of such a trace. However, we can safely ignore them for now. Let's focus on the bigger picture: What is taking up time within GET /hub/integrations? There are a few spans like resolve page components and all these GitHub API requests — a boatload of GitHub API requests!

Traces can often be represented in a variety of ways. In this alternative one, you can clearly see the cascade of requests.

The trace and its different visualizations revealed that we are loading too many files without pre-aggregating the necessary data or functional caching. Once we are aware of these issues, we can address them relatively quickly. Becoming aware is often the challenge hence why Dash0 focuses on observability.


On the note of caching btw., we can also see that we added caching (powered by Upstash Redis) to the application. It's generally a good idea, but the caching is too fine-grained as it is on a file level, not a use case level. Resulting in a lot of interactions with the cache and little real benefit.

Span details reveal many fine details about your application and the libraries/frameworks you use, like the fact that Upstash is being used and the latency that cache misses are adding.

Auto-Instrument Your Next.js App with OpenTelemetry

To get these insights yourself, you need two things:

  1. Your application needs to be instrumented to generate OpenTelemetry data.
  2. A place to send the OpenTelemetry to.

Both of these steps are surprisingly straightforward. Let's get to it!

Install the necessary packages

Generating OpenTelemetry in your Next.js application is easy, thanks to the official @vercel/otel package. The package simplifies the setup process and provides sensible defaults to trace your Next.js applications (and more when running on Vercel!). We also install other official OpenTelemetry packages for the core OpenTelemetry APIs.

sh
01234
$ npm install --save\
@vercel/otel \
@opentelemetry/sdk-logs \
@opentelemetry/api-logs \
@opentelemetry/instrumentation

Need to know even more about Next.js instrumentation? Check out the official docs!

Create an instrumentation.js file

Create a new file named instrumentation.js (or instrumentation.ts if you're using TypeScript) in the root or src/ directory of your project. This file will be responsible for registering your Next.js application with OpenTelemetry.

Add the following code to the file:

JavaScript
instrumentation.js
01234
import { registerOTel } from '@vercel/otel'
export function register() {
registerOTel({ serviceName: 'my-next-app' })
}

Make sure to replace the service name with the name of your application. Service names are used to label the generated data, and you can use these names within observability solutions to search/filter/group the data. It is always a good idea to use the same name that you also use in conversations with your colleagues to make the data easily recognizable.

Note: In Next.js versions <15 you still need to explicitly enable the instrumentation hooks. Starting with version 15 this is no longer required.

JavaScript
next.config.js
012345
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
instrumentationHook: true,
},
};

Define where the data should go

Finally, the data will be sent to an observability solution. Of course, we recommend sending your data to Dash0 (free for 14 days). Once registered, you can get the necessary environment variables (this is fully configurable using environment variables!) from the onboarding screens. They will look something like this:

012
OTEL_EXPORTER_OTLP_ENDPOINT="https://ingress.abcde.aws.dash0.com"
OTEL_EXPORTER_OTLP_COMPRESSION="gzip"
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer auth_abcde12345,Dash0-Dataset=default"

You can define them locally in the .env file of your project to get started, or configure them within your Vercel project for production insights!

Going Deeper: Creating Custom Spans for Fine-Grained Insights

While @vercel/otel automatically instruments your Next.js application and captures traces for common operations, you might want to gain even more granular insights into specific parts of your code. This is where creating custom spans comes in handy.

Custom spans allow you to track specific operations or code blocks within your Server Components, giving you a more detailed view of their execution. For example, you could create custom spans to:

  • Track expensive calculations: Measure the time it takes to perform complex computations or data transformations within your RSCs.
  • Monitor interactions with external services: Capture detailed information about API calls, database queries, or interactions with caching layers.
  • Analyze specific rendering logic: Isolate and track the performance of individual rendering steps within your components.
  • Annotate your data: What customer was this? What items were in the shopping cart?

Here's how you can create custom spans using the OpenTelemetry API, based on our actual usage in the Next.js middleware (also visible in the screenshot above).

JavaScript
0123456789101112131415161718192021222324252627282930
import { SpanStatusCode, trace } from "@opentelemetry/api";
export function afterAuth(auth, req) {
// Record a span, and mark it as active. Active spans are used to establish the
// span hierarchy.
return trace.getTracer("middleware")
.startActiveSpan("middleware.afterAuth", async (span) => {
// We want to know what path has been authorized and for whom. Capturing
// such metadata in the form of attributes is easy and means flexibility
// when querying/troubleshooting.
span.setAttribute("url.path", req.nextUrl.pathname);
span.setAttribute("enduser.id", auth.userId);
try {
return await doMoreWork(auth, req);
} catch (e) {
// Something went wrong here. Mark the span as erroneous and record
// additional data so that we can troubleshoot the problem.
span.setStatus({
code: SpanStatusCode.ERROR,
message: "afterAuth failed"
});
span.recordException(e);
return getErrorResponse();
} finally {
// Work has been completed. Mark the span as completed, which finalizes
// the span and schedules it for export to your observability backend.
span.end();
}
});
}

In this example, we use the trace.getTracer() function to obtain a tracer instance. Then, we use tracer.startActiveSpan() to create a new span named middleware.afterAuth. We enclose the code we want to track within the span, and finally, we call span.end() to mark the end of the span.

You can also add attributes to your spans using span.setAttribute(), which allows you to attach additional information to the trace. This can be useful for filtering and analyzing traces later on. We are specifically interested in the path the user is on, which user it is, and what organization/tenant they belong to.

By creating custom spans, you can gain a deeper understanding of the performance and behavior of your Server Components, allowing you to optimize your code and identify potential issues more effectively.

Conclusion

React Server Components mark a significant evolution in how we build web applications. They offer performance benefits and improved developer experience. However, this new paradigm also introduces challenges in understanding and debugging server-side logic.

OpenTelemetry is a crucial tool in this landscape, providing a powerful and standardized way to gain deep insights into your RSCs' behavior. By instrumenting your Next.js application with OpenTelemetry, you can reap benefits for local development and production environments.

Combined with Dash0, this is truly magical; try it!