Node.js Internals

Performance Hooks: Measuring Performance

When your API suddenly starts responding 200ms slower, intuition says: "The database is lagging!" You optimize indexes, rewrite queries, migrate to SSDs. But the problem was in `JSON.parse()` of a huge response - 150ms of synchronous event loop blocking, which can't be seen without a profiler. **Performance Hooks** are the X-ray of performance: you see the exact time of each operation and instantly find bottlenecks.

  • **E-commerce platform:** Measure the processing time of each checkout stage (validation → product reservation → payment → email sending). Discovered that 80% of the time was spent on synchronous PDF invoice creation - moved it to a queue, response time dropped from 1200ms to 300ms.
  • **GraphQL API:** PerformanceObserver on each resolver. Discovered that the "user + their posts + comments" query makes 1000+ requests to the database (N+1 problem). Added DataLoader, the number of requests dropped to 3.
  • **Real-time service:** Monitoring of Event Loop Delay showed p99 = 250ms every 5 minutes. It turned out that a cron job was synchronously parsing a 100MB JSON file. Replaced with streaming parsing - p99 is stable at 10ms.

Why measure performance

Imagine that your API suddenly started responding 200ms slower. Where is the problem? In the database? In JSON serialization? In a third-party library? Without precise measurements, you will be guessing. **Performance Hooks** is a built-in Node.js profiler that shows how long each operation takes with microsecond precision.

**Key difference from Date.now():** Performance Hooks use monotonic clocks, which do not depend on system time. If the user changes the time on the computer, `Date.now()` lies, but `performance.now()` does not.

What happens to the measurements of Date.now() if the user sets the clock back by an hour while the program is running?

Basics of the perf_hooks API

The `perf_hooks` module provides three main ways to measure performance: **point measurement** via `performance.now()`, **marks and measurements** via `mark()`/`measure()`, and **automatic observation** via `PerformanceObserver`. Each method serves its purpose: from the simple "how long did the function take" to "build a timeline of all operations in the application".

**When to use what:** - `performance.now()` - for a one-time measurement of a single operation - `mark()`/`measure()` - when you need to measure several stages of a process - `PerformanceObserver` - for automatic metric collection in production

What is the main advantage of PerformanceObserver over the regular performance.measure()?

`performance.now()` and measurement accuracy

`performance.now()` returns the time in milliseconds with **microsecond precision** (tenths of a millisecond). Inside Node.js, it uses the system call `clock_gettime(CLOCK_MONOTONIC)` on Linux or `QueryPerformanceCounter()` on Windows. This means that even operations faster than 1ms can be measured accurately.

**Time resolution:** `performance.now()` typically has a resolution of ~1 microsecond (0.001ms). For comparison: `Date.now()` - 1 millisecond, `process.hrtime.bigint()` - 1 nanosecond (but more difficult to use).

Why is performance.now() better suited for microbenchmarks (measuring very fast operations) than Date.now()?

Markers and Measurements: performance.mark() / measure()

**Markers** are named timestamps, **measures** are intervals between markers. Imagine placing flags on a timeline: "loading started", "loading finished", "processing started", "processing finished". Then you call `measure()` and get ready-made intervals: "loading took 150ms", "processing took 50ms".

**Advantage over manual subtraction:** Markers can be placed in different parts of the code (even in different modules), and then all measurements can be collected in one place. There is no need to pass `startTime` variables throughout the code.

Which statement about performance.mark() and measure() is true?

PerformanceObserver: automatic metrics collection

`PerformanceObserver` is a subscription to performance events. Instead of calling `getEntriesByName()` after each `measure()`, you create an observer once, and it automatically receives **all** new measurements. This is the "observer" pattern: you register a callback that triggers on any new entry.

**Types of entries:** `measure`, `mark`, `function`, `gc`, `http`, `dns`. With the observer, you can not only monitor your own measurements but also receive automatic Node.js metrics: GC runtime, DNS requests, HTTP requests.

What is the main advantage of using PerformanceObserver instead of manually calling performance.getEntriesByType('measure') after each measurement?

Monitoring Event Loop: monitorEventLoopDelay

**Event Loop Delay** is the difference between when a timer was supposed to trigger and when it actually triggered. If the event loop is busy (e.g., heavy synchronous computations), callbacks are delayed. `monitorEventLoopDelay()` creates a histogram of delays - a distribution: how many times the delay was 0-1ms, 1-2ms, 2-5ms, etc. This is the **main health indicator** of a Node.js application.

**Healthy values:** p50 < 10ms, p99 < 50ms. If p99 > 100ms - the event loop is blocked, the application is slowing down. Common causes: synchronous crypto operations, large JSON.parse(), regular expressions on huge strings.

If the application responds quickly to requests, it means the event loop is fine.

Even with normal response time, the event loop can be periodically blocked, which is only visible through a latency histogram.

Response time is the average or p50 value. Event Loop Delay shows **distribution tails** (p99, p999) - rare but critical cases of blocking. One synchronous operation lasting 500ms once a minute may be unnoticed in the average response time but will completely freeze the application for all users for those 500ms. The Event Loop histogram is the only way to see such issues.

What does it mean if in an Event Loop Delay histogram the value p99 = 150ms?

Key Ideas

  • **performance.now()** - monotonic clock with microsecond precision. Unlike Date.now(), it does not depend on system time and can measure operations faster than 1ms.
  • **mark() and measure()** - named markers on the timeline. You can place markers in different parts of the code, then gather all intervals in one place. Convenient for profiling multi-stage processes.
  • **PerformanceObserver** - Pub/Sub pattern for metrics. You set up the collection once (for example, sending to Prometheus), and all `measure()` calls automatically go there. Supports types: measure, mark, function, gc, http, dns.
  • **monitorEventLoopDelay()** - a histogram of event loop delays. The main indicator of application health: if p99 > 100ms - there are synchronous locks somewhere (crypto, JSON.parse, regex, fs.readFileSync).
  • **Practical workflow:** 1) Add mark()/measure() to critical points 2) PerformanceObserver for auto-collection 3) Event Loop monitoring in production 4) Histogram analysis (p50, p95, p99) 5) Anomaly detection and optimization.

Related topics

Performance Hooks are closely related to other Node.js optimization techniques:

  • Worker Threads — If the Event Loop Delay shows constant blocking due to CPU-intensive operations, you can offload them to separate threads using Worker Threads, without blocking the main event loop.
  • Async Hooks — Async Hooks allow tracking the lifecycle of asynchronous operations. Combining with Performance Hooks provides a complete picture: which async operations take a long time to execute and why.
  • Child Processes — If a heavy operation cannot be optimized (for example, running an external tool), Performance Hooks will show this, and it can be moved to a child process to avoid blocking the main application.

Вопросы для размышления

  • Which parts of your application are currently running slowly? Try adding mark()/measure() and find the bottlenecks. What turned out to be the slowest - database queries, serialization, or something unexpected?
  • If you added monitorEventLoopDelay() to production right now, what p99 value would you expect to see? Are there any synchronous operations in your code that could block the event loop?
  • How would you organize centralized collection of performance metrics for a microservices architecture? Where to deploy PerformanceObserver - in each service or in a shared library?

Связанные уроки

  • arch-09-cache
Performance Hooks: Measuring Performance

0

1

Sign In