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:

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);

Running two HTTP requests (e.g., with curl http://127.0.0.1:8080) prints:

0:  start
0:  finish
1:  start
1:  finish

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.

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

@Context()
class Foo {
  @Inject()
  private readonly tracer: Tracer;
}

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:

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

Maintain Stateful Context

If a class holds state tied to the request context, it must remain @ContextProto to avoid sharing state across requests.

@ContextProto()
class Foo {
  state: State;

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

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

Unit Test Refactor

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

  it('should work', () => {
    assert(foo.hello());
  });
});
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Node.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

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.