Backend Development 8 min read

Boosting Node.js Performance: AsyncLocalStorage and tegg v3 Deep Dive

This article explains how AsyncLocalStorage works in Node.js, demonstrates its usage with eggjs, presents benchmark results comparing tegg v3 to earlier versions, analyzes performance bottlenecks, and shows code transformations that reduce memory and CPU overhead for large-scale applications.

Alipay Experience Technology
Alipay Experience Technology
Alipay Experience Technology
Boosting Node.js Performance: AsyncLocalStorage and tegg v3 Deep Dive

AsyncLocalStorage and eggjs

AsyncLocalStorage[1] can safely retrieve variables stored in a context across an async function and its related asynchronous operations. The following code demonstrates its behavior:

<code>const http = require('http');
const { AsyncLocalStorage } = require('async_hooks');

const asyncLocalStorage = new AsyncLocalStorage();

function logWithId(msg) {
  const id = asyncLocalStorage.getStore();
  console.log(`${id}: `, msg);
}

let idSeq = 0;

http.createServer((req, res) => {
  asyncLocalStorage.run(idSeq++, () => {
    logWithId('start');
    setImmediate(() => {
      logWithId('finish');
      res.end();
    });
  });
}).listen(8080);
</code>

Running two HTTP requests (e.g., with

curl http://127.0.0.1:8080

) prints:

<code>0:  start
0:  finish
1:  start
1:  finish
</code>

Node.js uses V8’s Promise lifecycle hooks[2][3] to track Promise creation, resolution, rejection, and chaining. AsyncLocalStorage propagates the async ID via these hooks, allowing a storage object to retrieve the current context safely. Besides Promise, modules such as timer, fs, and net also propagate context through the

AsyncResource

abstraction[4].

Both Koa[5] and Egg[6] have added support: Koa enables it via the

asyncLocalStorage

option, while Egg exposes the current context through

app.currentContext

.

Benchmark

Test repository: https://github.com/eggjs/tegg_benchmark/actions/runs/4025979558

Project Scale Test

Simulates large projects by creating many controllers and services, testing 1, 10, 100, 1,000, and 10,000 components.

Business Complexity Test

Simulates increasing business complexity by having controllers call services, testing the same component counts.

Conclusion

tegg v3 does not suffer performance degradation as project size grows.

Performance loss caused by business complexity growth is slower in tegg v3 than in egg/tegg v1.

Profile – CPU

Hotspots concentrate on Egg’s

defineProperty

and

ClassLoader

due to the large number of controllers/services.

For tegg, the bottleneck shifts to Node’s own GC and async hooks.

Profile – Memory

When the number of controllers/services is high, each controller instantiation consumes significant memory, creating a memory allocation bottleneck.

Current memory pressure resides mainly in

async_hook

.

How to Leap

tegg Injection Principle

Example:

HelloWorldController

injects

Foo

, which injects

Tracer

.

<code>@HTTPController()
class HelloWorldController {
  @Inject()
  private readonly foo: Foo;
}

@Context()
class Foo {
  @Inject()
  private readonly tracer: Tracer;
}
</code>

When a request enters the framework, the entry class (e.g.,

HelloWorldController

) is located and all dependent objects are instantiated.

tegg v1 Performance Bottleneck

Each request creates 10,001 objects, causing heavy memory allocation and high GC pressure.

tegg v3 Optimization Principle

Reduce object instantiation: only

Tracer

needs per‑request context;

HelloWorldController

and

Foo

can be singletons, improving CPU and memory usage.

AsyncLocalStorage proxies objects so that singleton controllers retrieve the correct per‑request

Tracer

via a context storage.

Optimization Results

Object count drops from 10,001 to 0, dramatically lowering memory pressure; tegg v1 heap fluctuated between 200 MB and 1 GB, while tegg v3 stabilizes around 200 MB.

tegg v3 Code Refactor

Change Annotation

Replace

@ContextProto()

with

@SingletonProto

:

<code>~~@ContextProto()~~
@SingletonProto()
class Foo {
  @Inject()
  private readonly tracer: Tracer;
}
</code>

Maintain Stateful Context

If a class holds state tied to the request context, it must remain

@ContextProto

to avoid sharing state across requests.

<code>@ContextProto()
class Foo {
  state: State;

  foo() {
    this.state = 'foo';
  }

  bar() {
    this.state = 'bar';
  }
}
</code>

Unit Test Refactor

<code>describe('test/index.test.ts', () => {
  let foo: Foo;
  beforeEach(async () => {
    foo = await app.getEggObject(Foo);
  });

  it('should work', () => {
    assert(foo.hello());
  });
});
</code>
Performancenode.jsAsyncLocalStorageEggJStegg
Alipay Experience Technology
Written by

Alipay Experience Technology

Exploring ultimate user experience and best engineering practices

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.