Optimizing Next.js with OpenTelemetry
Z4NR34L
Posted on January 13, 2024
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 and Docker Compose Guide to go further without any problems.
Setting local environment
Now we need to clone a ready-to-use repository made by Vercel.
git clone https://github.com/vercel/opentelemetry-collector-dev-setup
so, just go into that repository
cd opentelemetry-collector-dev-setup
and run the following command to start the docker containers.
docker-compose up -d
That's it! Now we can access Jaeger UI using 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:
or just create a new project using the following command:
pnpm create next-app --example with-opentelemetry your-app-name
npx create-next-app --example with-opentelemetry your-app-name
yarn create next-app --example with-opentelemetry your-app-name
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
const nextConfig = {
// ...
experimental: {
instrumentationHook: true
}
// ...
}
Now, we need to install the following packages:
pnpm add @opentelemetry/api @vercel/otel
npm install @opentelemetry/api @vercel/otel
yarn install @opentelemetry/api @vercel/otel
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
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:
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:
{
"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 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
'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
'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
'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
'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
'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.
Posted on January 13, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.