callbackify, class adapter и async iterator

Контекст можно организовать через класс (alternative to closure). callbackify — обратная операция к promisify. async iterator над пользовательскими источниками. Создание Promise дорого — иногда thenable достаточно. Литеральные объекты in-place — критичный паттерн для V8-оптимизации.

Контекст не только через замыкания

Паттерны не только про функции. Люди привыкли, что паттерны это с классами. Контекст можно организовать при помощи класса.

Класс vs замыкание — два способа упаковать state + методы:

// Closure-style
const counter = (start = 0) => {
  let count = start;
  return {
    inc:  => ++count,
    get:  => count
  };
};

// Class-style
class Counter {
  #count;
  constructor(start = 0) { this.#count = start; }
  inc { return ++this.#count; }
  get { return this.#count; }
}

Классы перестали быть сахаром

Очень долгое время классы в JS были синтаксическим сахаром над прототипами. Только не так давно классы в спецификации были выделены и реализованы в V8 как отдельная сущность.

В современном V8 классы оптимизируются лучше прототипного стиля.

Создание Promise дорого

Создание промисов в V8 — очень затратная по издержкам операция. Если есть возможность каким-либо образом обойти создание промисов — лучше это обойти.

За функционирование промиса во многом отвечает host-среда, а не сам V8. Это работает намного медленнее.

Thenable вместо Promise

function fastResolved(value) {
  return {
    then(resolve) {
      resolve(value);
      return this;
    }
  };
}

// Вместо Promise.resolve(value) — заметно быстрее на горячем пути
await fastResolved(42);

Если вам не нужна полная функциональность Promise, и можете обойтись методом then — это будет гораздо быстрее.

Thenable принимается везде, где принимаются промисы — async/await, Promise.all и т.д. Спецификация трактует их одинаково.

callbackify

Обратное к promisify:

const util = require('node:util');

const asyncFn = async (x) => x * 2;
const callbackFn = util.callbackify(asyncFn);

callbackFn(5, (err, result) => console.log(result)); // 10

Нужно когда: интегрируешь async-функцию в legacy API, ожидающий callback-стиль.

Async iterator над custom-источником

class Counter {
  constructor(max) { this.max = max; this.current = 0; }

  [Symbol.asyncIterator] {
    return {
      next: async  => {
        if (this.current < this.max) {
          await new Promise(r => setTimeout(r, 100));
          return { value: this.current++, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
}

for await (const n of new Counter(5)) console.log(n);

Symbol.asyncIterator + объект с async-методом next = async iterable.

Адаптер класса к callback

class FastResolver {
  constructor(value) {
    this.value = value;
  }
  then(resolve) {
    queueMicrotask( => resolve(this.value));
    return this;
  }
}

await new FastResolver(42); // работает с await

Класс с методом then реализует thenable-контракт.

Литеральный объект in-place — оптимизация V8

// Хорошо: V8 видит, что объект локальный
this.resolve({ value, done: false });

// Хуже: объект через идентификатор
const item = { value, done: false };
this.resolve(item);

В this.resolve внутрь передаётся объект в литеральной форме. Это для V8 чрезвычайно важно — он сразу относится позитивно. Не нужно хранить объект или поддерживать дальше.

V8 может вообще не выделять объект на куче (scalar replacement), если видит литерал прямо в аргументе.

Bun на JavaScriptCore

Bun использовал в качестве основы JavaScriptCore, а не V8. Это сразу сделало его очень ограниченным с точки зрения оптимизаций. Изначальная идея — оптимальная реализация API Node.

Бенчмарки Bun хороши, но миграции с Node не происходит — слишком много несовместимостей. Узкое решение для маленьких приложений.

Node остаётся мейнстримом бэкенда на JS.

Связанные темы

Ресурсы