Асинхронная и синхронная обработка

Библиотеки Remark и Rehype являются мощными инструментами для работы с Markdown и HTML в экосистеме JavaScript. Ключевой особенностью их архитектуры является возможность как синхронной, так и асинхронной обработки деревьев синтаксиса (AST), что позволяет гибко интегрировать плагины и оптимизировать производительность.


Синхронная обработка

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

Пример синхронной цепочки обработки с Remark:

import { remark } from 'remark';
import remarkParse from 'remark-parse';
import remarkStringify from 'remark-stringify';

const processor = remark()
  .use(remarkParse)
  .use(remarkStringify);

const markdown = '# Заголовок\n\nТекст параграфа';
const result = processor.processSync(markdown);

console.log(result.toString());

В этом примере processSync выполняет все шаги сразу: парсинг Markdown, применение плагинов и генерацию конечного Markdown-кода. Синхронный подход гарантирует предсказуемый порядок выполнения, но ограничен отсутствием поддержки асинхронных операций внутри плагинов.

Плюсы синхронной обработки:

  • Простота кода и отсутствие необходимости использовать async/await.
  • Предсказуемый порядок выполнения плагинов.
  • Более высокая производительность при обработке больших объемов текста без асинхронных операций.

Минусы:

  • Невозможность интеграции плагинов, выполняющих асинхронные действия (например, сетевые запросы, чтение файлов или подключение к базам данных).
  • Ограниченная масштабируемость в сложных цепочках трансформаций.

Асинхронная обработка

Асинхронная обработка позволяет включать в цепочку плагины, которые возвращают промисы или используют async функции. В этом случае Remark и Rehype автоматически обрабатывают эти промисы, ожидая их завершения перед переходом к следующему шагу.

Пример асинхронной обработки с Remark:

import { remark } from 'remark';
import remarkParse from 'remark-parse';
import remarkStringify from 'remark-stringify';

async function processMarkdown() {
  const processor = remark()
    .use(remarkParse)
    .use(async function customPlugin() {
      return async (tree) => {
        // Асинхронная операция внутри плагина
        await new Promise(resolve => setTimeout(resolve, 100));
        tree.children.push({
          type: 'paragraph',
          children: [{ type: 'text', value: 'Асинхронный параграф' }]
        });
      };
    })
    .use(remarkStringify);

  const markdown = '# Заголовок\n\nТекст параграфа';
  const result = await processor.process(markdown);

  console.log(result.toString());
}

processMarkdown();

В этом примере плагин выполняет асинхронную задержку с помощью setTimeout. Метод process автоматически ожидает завершения всех асинхронных операций. Это позволяет:

  • Подключать плагины, выполняющие HTTP-запросы или доступ к файловой системе.
  • Последовательно применять асинхронные трансформации AST.
  • Использовать внешние источники данных для динамической генерации контента.

Особенности асинхронной обработки:

  • Все плагины должны корректно поддерживать промисы или async функции.
  • Порядок выполнения сохраняется: следующий плагин запускается только после завершения предыдущего.
  • Нужен await для получения результата; синхронные методы (processSync) не могут обрабатывать асинхронные плагины.

Выбор между синхронной и асинхронной обработкой

Выбор подхода зависит от характера задач и используемых плагинов:

Характеристика Синхронная обработка Асинхронная обработка
Использование асинхронных операций Нет Да
Простота кода Высокая Средняя
Производительность на больших текстах без IO Выше Ниже из-за ожидания промисов
Поддержка плагинов с внешними запросами Нет Да
Применение в серверных и сборочных процессах Часто Часто

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


Совместимость Remark и Rehype

Remark работает с Markdown AST (MDAST), а Rehype — с HTML AST (HAST). При необходимости преобразования Markdown в HTML обычно используется комбинация remark → rehype → html, при этом обе библиотеки поддерживают синхронные и асинхронные цепочки:

import { remark } from 'remark';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
import rehypeStringify from 'rehype-stringify';

async function markdownToHtml(markdown) {
  const result = await remark()
    .use(remarkParse)
    .use(remarkRehype)
    .use(rehypeStringify)
    .process(markdown);

  return result.toString();
}

В этом процессе возможны асинхронные плагины на любом этапе: парсинг, трансформация MDAST → HAST, генерация HTML.


Рекомендации по проектированию цепочек обработки

  1. Всегда определять, какой тип операций выполняет плагин: синхронные или асинхронные.
  2. Синхронные плагины лучше группировать в начале цепочки для оптимизации скорости.
  3. Асинхронные плагины лучше выносить в отдельные этапы после синхронных, чтобы минимизировать задержки.
  4. Использовать processSync только если все плагины синхронные; при наличии асинхронных операций использовать process с await.
  5. Проверять корректность возвращаемых значений плагинов: для асинхронных — промис, для синхронных — результат напрямую.

Практическая заметка по отладке

При работе с асинхронными плагинами часто возникает ситуация, когда изменения AST не применяются из-за некорректного возвращения функции. Рекомендуется всегда возвращать либо tree напрямую, либо оборачивать функцию в async и использовать await для всех внутренних промисов. Это предотвращает неожиданные пустые результаты и ошибки при генерации Markdown/HTML.


Асинхронная и синхронная обработка в Remark и Rehype позволяет строить гибкие цепочки трансформации контента, сочетая высокую производительность и возможность интеграции внешних данных. Разделение на синхронные и асинхронные этапы обеспечивает баланс между скоростью и функциональностью.