Оптимизация парсинга больших документов

Для работы с библиотекой Marked в JavaScript необходимо установить пакет через npm:

npm install marked

Импортировать библиотеку можно стандартным образом:

import { marked } from 'marked';

Marked предоставляет возможность конвертации текста в формате Markdown в HTML. Основной метод для этого — marked.parse(). Для больших документов использование этого метода напрямую может стать узким местом, поэтому важно понимать, как настроить парсер для оптимальной производительности.


Потоковая обработка Markdown

Большие документы лучше обрабатывать потоково, а не целиком в памяти. Marked поддерживает использование лексеров и парсеров отдельно:

const lexer = new marked.Lexer();
const tokens = lexer.lex(largeMarkdownString); // разбиение на токены

Затем можно передавать токены в парсер:

const parser = new marked.Parser();
const html = parser.parse(tokens);

Такой подход позволяет:

  • Разделять процесс токенизации и парсинга.
  • Использовать частичную обработку, если документ слишком большой.
  • Легче внедрять оптимизации, например, кэширование токенов.

Настройки, влияющие на производительность

Marked поддерживает объект конфигурации с множеством опций. Ключевые параметры для оптимизации:

  • gfm (GitHub Flavored Markdown) — если документ не использует расширенный синтаксис, его отключение уменьшает нагрузку.
  • breaks — контролирует разрыв строк. В больших документах отключение может ускорить обработку.
  • sanitize — проверка HTML может быть затратной; при необходимости лучше использовать внешние библиотеки для безопасного рендеринга.
  • mangle и headerIds — автоматическая генерация ID заголовков и кодировка email-адресов. Для больших документов можно отключить, если это не критично.

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

marked.setOptions({
  gfm: true,
  breaks: false,
  headerIds: false,
  mangle: false
});

Кастомные рендереры

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

const renderer = {
  image(href, title, text) {
    return `<img src="${href}" alt="${text}" loading="lazy">`; // отложенная загрузка
  },
  link(href, title, text) {
    return `<a href="${href}" rel="noopener noreferrer">${text}</a>`; // безопасные ссылки
  }
};

marked.use({ renderer });

Это уменьшает время рендеринга и снижает потенциальные проблемы с безопасностью при больших объёмах контента.


Асинхронный рендеринг и Web Workers

Для очень больших документов стоит вынести процесс парсинга в отдельный поток с помощью Web Workers:

// worker.js
importScripts('https://cdn.jsdelivr.net/npm/marked/marked.min.js');

onmess age = (event) => {
  const html = marked.parse(event.data);
  postMessage(html);
};

Главный поток:

const worker = new Worker('worker.js');
worker.onmess age = (e) => {
  document.getElementById('output').innerHTML = e.data;
};
worker.postMessage(largeMarkdownString);

Такой подход:

  • Не блокирует UI при рендеринге.
  • Позволяет обрабатывать документы размером в десятки мегабайт без подвисаний.
  • Совместим с современными браузерами и Node.js (через Worker Threads).

Кэширование токенов и промежуточного HTML

Для документов, которые часто обновляются частично, имеет смысл кэшировать:

  1. Токены отдельных секций:

    • Разделить Markdown на главы или статьи.
    • Лексер применять к каждой секции отдельно.
    • При изменении рендерить только изменившиеся секции.
  2. Промежуточный HTML:

    • Если документ большой, часто встречается один и тот же блок, можно хранить его HTML и вставлять без повторного парсинга.

Пример кэширования:

const tokenCache = new Map();

function renderSection(markdown, id) {
  if (tokenCache.has(id)) {
    return marked.Parser.parse(tokenCache.get(id));
  }
  const tokens = marked.Lexer.lex(markdown);
  tokenCache.set(id, tokens);
  return marked.Parser.parse(tokens);
}

Оптимизация памяти и GC

Большие Markdown-файлы могут занимать сотни мегабайт в памяти. Рекомендации:

  • Избегать хранения полного HTML в памяти, если достаточно вставлять сразу в DOM.
  • Использовать итеративную обработку по частям.
  • Удалять ссылки на промежуточные объекты (tokens, renderer) после рендеринга.

Использование walkTokens для фильтрации и трансформаций

Метод walkTokens позволяет обрабатывать токены на лету, что часто быстрее, чем модификация HTML после генерации:

marked.use({
  walkTokens(token) {
    if (token.type === 'heading' && token.depth === 1) {
      token.text = token.text.toUpperCase(); // кастомизация заголовков
    }
  }
});

Это эффективно для:

  • Преобразования заголовков.
  • Фильтрации нежелательных элементов.
  • Добавления атрибутов к элементам без полного парсинга HTML.

Итоговый подход к большим документам

Для больших Markdown-документов эффективна комбинация:

  1. Разделение на логические блоки.
  2. Отдельная токенизация и кэширование.
  3. Асинхронный рендеринг через Web Workers.
  4. Использование кастомного рендерера и walkTokens.
  5. Настройка параметров Marked для отключения ненужных функций.

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