Почти 15 лет назад Райан Томайко (Ryan Tomayko) написал книгу "The Thing About Git" (Про Git). Это было время, когда SVN (Subversion - система управления версиями) все еще активно использовался. Мало кто понимал, почему Git такой особенный, и тогда я тоже не принадлежал к их числу. Статья Райана уловила суть Git'а и убедила меня перейти на него.

Уже было написано множество статей о том, почему и как следует принимать Fastify, но сейчас 2022 год, а Express - самый традиционный фреймворк веб-сервера для Node.js - по-прежнему имеет примерно в 49 раз больше еженедельных загрузок npm, чем Fastify:

Еженедельные загрузки npm

24,099,092

Express

486,761

Fastify

Это не так уж удивительно, Express неплохо написан и надежен, до такой степени, что прошел через многие годы без существенных обновлений. Он настолько хорош, насколько это возможно.

Но мне кажется, что данная статистика загрузок скорее всего свидетельствует о том, что многие люди не видят полной картины, когда речь заходит о Fastify.

На первый взгляд, практических различий между настройкой веб-серверов в Express и Fastify не так много - они оба позволяют регистрировать обработчики для определенных URL, позволяют связывать промежуточные функции между запросами.

Но Fastify может гораздо больше: такие вещи, как логирование, валидация и сериализация, предлагаются из коробки. Статьи, на которые я ссылался выше, являются хорошей отправной точкой в изучении этого, и они также охватывают некоторые из различных полезных вещей, которые Fastify делает, когда речь заходит о производительности.

Я не буду перечислять их снова. Вместо этого сосредоточу внимание на наиболее существенных для меня вещах, которые действительно имеют значение, а в заключение выскажу некоторые соображения.

Гранулярность хуков

В Express-приложениях, если вы хотите иметь хоть какой-то контроль над тем, что отправляется по проводам, вам нужно переписать методы write() и end() для текущего ответа. Возможно, есть очень хитрый способ заменить их на глобальном уровне, но это создаст другие проблемы с поддержкой. Вот пример, который я смог найти, о том, как записать тело ответа для Express-запроса:

function logResponseBody (req, res, next) {
  const oldWrite = res.write
  const oldEnd = res.end
  const chunks = []
  res.write = (chunk, ...args) => {
    chunks.push(chunk)
    return oldWrite.call(res, chunk, ...args)
  }
  res.end = (chunk, ...args) => {
    if (chunk) {
      chunks.push(chunk)
    }
    const body = Buffer.concat(chunks).toString('utf8')
    console.log(req.path, body)
    return oldEnd.call(res, chunk, ...args)
  }
  next()
}

Существует фундаментальная проблема этого паттерна: перезапись функций обходится дорого - Node.js приходится проделывать много работы под капотом, чтобы внедрить новую функцию и выполнить сборку мусора. Приложения Node.js, как правило, требуют большой загрузки процессора и памяти - как правило, их масштабирование для выполнения значительного объема работы обходится не так уж дешево. Все, что вы можете сделать для более эффективной работы JavaScript, также способно оказать значительный “кумулятивный” эффект на производительность и масштабируемость вашего приложения.

Fastify обладает системой хуков, которая позволяет перехватывать несколько этапов в цикле запрос-ответ. Обратите внимание, что этот тип логирования запросов и ответов уже осуществляется Fastify - он использует Pino на внутреннем уровне и имеет функции сериализации логов, которые можно настроить для захвата запросов и ответов, как вам нравится.

Pino, кстати, чрезвычайно быстр и был создан для снижения накладных расходов на логирование.

См. статью The Cost of Logging (Стоимость логирования), написанную соавтором Fastify Matteo Collina (Маттео Коллина).

Но если бы в нем не было встроенных средств логирования, мы могли бы легко использовать хук onSend для достижения того же самого, то есть для перехвата тела ответа:

fastify.addHook('onSend', (req, reply, payload, next) => {
  console.log(req.url, payload)
  next()
})

Хотите запустить что-то сразу после получения запроса? Или перед тем, как будет выполнен парсинг тела запроса? Может быть, до того, как он будет валидирован? В общем, суть вы поняли.

Безопасное расширение запросов и ответов

Еще одной ключевой особенностью Fastify является возможность безопасного расширения объектов запроса и ответа - под безопасным я подразумеваю способ, который не влияет на производительность.

Если мы не можем использовать что-то вроде хука onSend, то просто создадим отдельный метод logSend(), который надежно прикрепляется к prototype, связанному с объектом ответа. Это означает, что Node.js будет тратить время на создание этой функции только один раз, во время загрузки, а не динамически, при каждом отдельном запросе. Кроме того, изменение формы объекта "на лету" снижает производительность.

Поэтому с помощью API декорирования Fastify мы можем сделать следующее:

fastify.decorateReply('logSend', function (body) {
  console.log(req.path, body)
  this.send(body)
})

И после этого reply.logSend() становится доступным в ваших хендлерах. Опять же, Fastify предлагает логирование из коробки, и вам никогда не понадобится делать ничего подобного, однако это служит иллюстрацией того, как Fastify позволяет расширять основные классы объектов.

Система инкапсуляции плагинов

Недооцененный аспект Fastify - его способность обеспечивать инкапсуляцию. Плагины можно настроить на запуск в глобальном контексте или создать отдельный дочерний контекст. У вас может быть бесконечное дерево предков и потомков плагинов.

Когда вы создаете отдельный дочерний контекст, все хуки и декораторы, определенные в этом контексте, привязываются только к нему.

fastify.register(function privateContext (fastify, _, done) {
  fastify.addHook('onRequest', authorize)
  fastify.register(privateRoutes)
  done()
})
fastify.register(function publicContext (fastify, _, done) {
  fastify.register(publicRoutes)
  done()
})

Это косвенно повышает безопасность вашего кода - если вы попытаетесь обратиться к методу, который доступен с помощью плагина в другом контексте, то получите сообщение об ошибке, поскольку он окажется недоступным. Когда у вас есть чистое разделение задач, это облегчает отладку и предотвращает ошибки при структурировании кода.

Что насчет воркер-сред?

Одним из самых захватывающих событий в JavaScript за последнее время стало введение воркер-сред на стороне сервера, которые стали популярными благодаря Cloudflare Workers, Deno Deploy и, совсем недавно, Netlify Edge Functions.

Следует отметить, что Fastify — это веб-фреймворк для Node.js — он в значительной степени зависит от API-интерфейсов, ориентированных на Node.js. И Node.js, и серверные воркер-среды основаны на движке V8 - поэтому у них много общего, но Node.js имеет собственные расширения и абстракции для обеспечения наилучшей производительности веб-серверов JavaScript, включая, например, потоки воркеров.

Вместо этого все серверные воркер-платформы основаны на стандарте Service Workers, который изначально регулировал сервис-воркеры, выполняемые в браузере. Идея использования API Service Workers для написания веб-серверов стала для меня довольно неожиданной - я всегда считал, что API в сравнении с фреймворками Node.js несколько неинтуитивен.

addEventListener(`fetch`, (event) => {
  event.respondWith(handleRequest(event.request))
})

Он основан на стандарте Fetch Standard. После того как вы настроите необходимых слушателей и обработчиков, то, по сути, будете работать как с API Service Worker, так и с классами URL, Request и Response, как это определено в Fetch Standard.

Тем не менее, сам факт того, что он основан на стандарте, который тщательно продуман и определен, и повсеместно используется сервис-воркерами браузеров, делает его весьма привлекательным для создания веб-серверов.

Это если вы можете позволить себе зависеть от любой из управляемых платформ, которые его поддерживают. На данный момент, несмотря на то, что базовый код для Deno Deploy уже доступен, самостоятельное размещение воркер-среды для выполнения вашего кода, скорее всего, окажется более сложным и проблематичным, чем вы ожидаете.

Уже начали появляться веб-фреймворки, ориентированные на воркер-среды с более дружественным высокоуровневым API - популярными примерами являются worktop и h3. Не вижу причин, почему мы не можем иметь облегченную версию Fastify, работающую в этих средах. С этой целью я начал эксперимент, предоставив базовые классы Fastify Request и Reply и несколько хуков цикла запрос-ответ:

import FastifyEdge from 'fastify-edge'

const app = FastifyEdge()

app.addHook('onSend', (req, reply, payload) => {
  if (req.url === '/') {
    return `${payload} World!`
  }
})

app.get('/', (_, reply) => {
  reply.send('Hello')
})

Вы можете ознакомиться с ним (и, возможно, внести свой вклад!) здесь: galvez/fastify-edge.

Как начать работу с Fastify

Если вы окончательно убедились, что должны использовать Fastify вместо Express, я рекомендую посмотреть видео Маттео Коллины (Matteo Collina) "Быстрое введение в Fastify".

Серия статей Саймона Плендерлейта (Simon Plenderleith) "Изучение Fastify" и статьи Мануэля Спиголона (Manuel Spigolon) также являются хорошими ресурсами для этого. Обязательно изучите документацию.

Следует отметить, что вам может не понадобиться все из того, что предлагает Fastify. Веб-серверы Node.js на базе Express продолжают использоваться на большинстве серверов Node.js в продакшне, и все в порядке. Несмотря на то, что это звучит как веский аргумент, и в какой-то степени так оно и есть, нет ничего плохого в повышении эффективности ваших веб-серверов Node.js.

Вы тратите меньше ресурсов сервера, обеспечиваете возможность масштабирования приложения различными способами и сохраняете интеграцию с унаследованным кодом Express.

За последние пару лет я провел много консультаций для клиентов, испытывающих проблемы с масштабированием приложений Node.js, и мигрирование на Fastify стало для меня первоочередной задачей. Кроме того, нужно потратить некоторое время на полное понимание цикла событий Node.js - что также абсолютно необходимо, если в дальнейшем вы хотите избежать потери системных ресурсов на неоптимальный код.

В заключение хочу пригласить всех на бесплатный урок, где мы рассмотрим протокол GRPC, технические характеристики и области применения. Мы научимся создавать простейшие GRPC клиенты и серверы на Node.js, а также микросервисы с NestJS. Это занятие будет полезно тем, кто хочет познакомиться со стандартом GRPC, сравнить этот подход с традиционным REST API и использовать его в среде Node.js.

- Зарегистрироваться на бесплатный урок

Комментарии (2)


  1. doctorw
    15.12.2022 20:32
    +4

    Никак не могу понять, какое отношение имеет первый абзац ко всему что идёт после него?


    1. fedorro
      17.12.2022 18:12

      Типа эта статья должна подтолкнуть перейти с Express на Fastify как та книга с SVN на Git.