Optimizing Next.js with OpenTelemetry
Tags: webdev, javascript, programming, nextjs
Published: 2024-01-13 13:37:28

Unlock the full potential of your Next.js application with my comprehensive guide on performance optimization using OpenTelemetry and Docker.

## Setting the Stage

Performance is the lifeblood of any successful web application. Users expect seamless experiences, and slow-loading pages can lead to frustration and abandonment.

Let's lay the groundwork by understanding the critical role of instrumentation in achieving optimal performance for your Next.js application.

### Why does Instrumentation Matter?

Instrumentation involves adding code to your application to collect data on various aspects of its behavior. This data is invaluable for identifying bottlenecks, understanding resource consumption, and making informed decisions for optimization. In the context of web development, effective instrumentation provides insights into how your application interacts with its environment, enabling you to pinpoint areas for improvement.

### Introduction to OpenTelemetry

Enter OpenTelemetry, a powerful and flexible observability framework. OpenTelemetry allows you to collect traces, metrics, and logs from your application, providing a comprehensive view of its performance. Traces represent the journey of a request through your application, while metrics offer quantitative measurements of various aspects, and logs provide detailed information for debugging.

### Key Concepts: Tracks and Spans

As we dive into instrumentation, it's crucial to understand two fundamental concepts: tracks and spans. Tracks represent the broader paths that a request takes through your application, while spans break down these tracks into smaller, more manageable units. Think of tracks as chapters in a book and spans as individual sentences within each chapter. Together, they form a narrative that helps you comprehend your application's performance story.

## Getting Your Docker Toolbox Ready

Now, we need to prepare our local envoironment. We will use Jaeger with Prometheus and Zipkin to visualize our traces and metrics.

### Installing Docker

You can learn more about docker in my [Docker Guide](https://www.zanreal.net/blog/beginners-guide-to-docker) and [Docker Compose Guide](https://www.zanreal.net/blog/beginners-guide-to-docker-compose) to go further without any problems.

### Setting local environment

Now we need to clone a ready-to-use repository made by Vercel.

```bash
git clone https://github.com/vercel/opentelemetry-collector-dev-setup
```

so, just go into that repository

```bash
cd opentelemetry-collector-dev-setup
```

and run the following command to start the docker containers.

```bash
docker-compose up -d
```

That's it! Now we can access Jaeger UI using [0.0.0.0:16686](http://0.0.0.0:16686).

## Next.js Instrumentation

### Creating a new project

If you want to instrument your project at the beggining starting from stratch, you can use the following command to create a new Next.js project using the OpenTelemetry example:

[Repository URL](https://github.com/vercel/next.js/tree/canary/examples/with-opentelemetry)

or just create a new project using the following command:

```bash
pnpm create next-app --example with-opentelemetry your-app-name
```
```bash
npx create-next-app --example with-opentelemetry your-app-name
```
```bash
yarn create next-app --example with-opentelemetry your-app-name
```
```bash
bun create next-app --example with-opentelemetry your-app-name
```

### Instrumenting existing projects

First of all, we need to tell Next.js that we will be using an **EXPERIMENTAL** feature. To do that, we need to add the following lines to our `next.config.js` file.

`next.config.js`

```js
const nextConfig = {
  // ...
  experimental: {
    instrumentationHook: true
  }
  // ...
}
```

Now, we need to install the following packages:

```bash
pnpm add @opentelemetry/api @vercel/otel
```
```bash
npm install @opentelemetry/api @vercel/otel
```
```bash
yarn install @opentelemetry/api @vercel/otel
```
```bash
bun add @opentelemetry/api @vercel/otel
```

And at the final stage of basic instrumentation we need to create an `instrumentation.ts` file in same level as our `pages` of `app` folder.

`instrumentation.ts`

```ts
import { registerOTel } from '@vercel/otel';

export function register() {
  registerOTel('your-service-name');
}
```

Remember that if you have as `src` folder, you need to create this file in it.

Aaaaaaand, we're done with that part!

## Testing instrumentation

We currently want to get as many spans as possible, so we will use the following command to run our project:

```bash
NEXT_OTEL_VERBOSE=1 next dev
```

If you want to have an easier access to it, just add new script to your `package.json` as I've done:

```json
{
  "scripts": {
    "dev:otel": "NEXT_OTEL_VERBOSE=1 next dev"
  }
}
```

To make an test of our instrumentation, we just need... to open our browser and go to [localhost:3000](http://localhost:3000) and we shall see new traces incoming in our Jaeger UI.

## Adding custom spans

### OTel Wrapper for Next.js

I've made a small wrapper for Next.js to make it easier to use OpenTelemetry in your Next.js project. I'm placing it in a `lib` folder on my project's root.

`@/lib/otel.ts`

```ts
'use server';

import { type Span, trace } from '@opentelemetry/api';

export async function otel<T>(
  fnName: string,
  fn: (...args: any[]) => Promise<T>,
  ...props: any[]
): Promise<T> {
  const tracer = trace.getTracer(fnName);
  return tracer.startActiveSpan(fnName, async (span: Span) => {
    try {
      return await fn(...props);
    } finally {
      span.end();
    }
  });
}
```

But how to use it? It's simple! Just import it and use it as a wrapper for your functions, server actions, db calls or just anything.

`actions.ts`

```ts
'use server';

import { Redis } from '@upstash/redis';
import { otel } from '@/lib/otel';

const redis = Redis.fromEnv();

export async function increment(slug: string, prefix: string): Promise<void> {
  return otel('your-component', async () => {
    await redis.incr(['pageviews', prefix, slug].join(':'));
  });
}
```

or pass function with it's props to it ;)

`actions.ts`

```ts
'use server';

import { Redis } from '@upstash/redis';
import { otel } from '@/lib/otel';

const redis = Redis.fromEnv();

async function increment(slug: string, prefix: string): Promise<void> {
  await redis.incr(['pageviews', prefix, slug].join(':'));
}

export async function incrementWithOtel(
  slug: string,
  prefix: string
): Promise<void> {
  return otel('your-component', increment, slug, prefix);
}
```

Just like that! Now you can use it in your project and see how it works.

### Adding custom spans via `@opentelemetry/api`

If you don't want to use my wrapper, you can use the following code to add custom spans to your project.

#### Active Span

`actions.ts`

```ts
'use server';

import { Redis } from '@upstash/redis';
import { trace } from '@opentelemetry/api';

const redis = Redis.fromEnv();

export async function increment(slug: string, prefix: string): Promise<void> {
  return trace
    .getTracer('your-component')
    .startActiveSpan('your-operation', async (span) => {
      try {
        await redis.incr(['pageviews', prefix, slug].join(':'));
      } finally {
        span.end();
      }
    });
}
```

#### Manual Span

`actions.ts`

```ts
'use server';

import { Redis } from '@upstash/redis';
import { trace } from '@opentelemetry/api';

const redis = Redis.fromEnv();

export async function increment(slug: string, prefix: string): Promise<void> {
  const span = trace.getTracer("your-component").startSpan("your-operation");
  await redis.incr(['pageviews', prefix, slug].join(':'));
  span.end();
}
```

## Conclusion

As you wrap up the initial steps in this practical guide, your Dockerized environment is ready to capture and export crucial data through OpenTelemetry. The foundation is set for a deep dive into optimizing Next.js performance, armed with the insights needed to make informed decisions on where and how to enhance your application's speed and efficiency.

Get ready to transform your Next.js application into a high-performance masterpiece. The journey continues as you explore advanced instrumentation techniques and delve into the art of performance optimization with OpenTelemetry.

Now you know how to instrument your Next.js project using OpenTelemetry and Docker. I hope you enjoyed this article and learned something new. If you have any questions, feel free to reach out to me via [contact](https://www.zanreal.net/contact).