ChatGPT умирает на длинных разговорах. Не AI-часть — модель отлично держит тысячи сообщений. Умирает фронтенд. Таб зависает, скролл лагает, иногда браузер просто крашится.

Самое обидное — именно длинные разговоры самые ценные. Чем дольше обсуждаешь, тем больше контекста у модели, тем полезнее ответы. А продукт ломается ровно в тот момент, когда начинается максимальная отдача.

Мне это надоело и я полез разбираться.

Что происходит под капотом

Когда вы открываете разговор, ChatGPT делает запрос на /backend-api/conversation/{id}. В ответе приходит JSON с полем mapping — объект, где каждый ключ это ID сообщения, а значение — нода с контентом, parent-ссылкой и списком children.

{
  "mapping": {
    "abc-123": {
      "message": { "content": { "parts": ["..."] }, "author": { "role": "user" } },
      "parent": "abc-122",
      "children": ["abc-124"]
    }
  },
  "current_node": "abc-999"
}

React получает этот объект целиком и рендерит все ноды в DOM. Каждое сообщение — это не один <div>, а дерево: аватар, контент, кнопки, подсветка кода, MathJax-формулы. Одно сообщение ≈ 250 DOM-нод.

Считаем:

  • 500 сообщений → ~125 000 DOM-нод

  • 2 000 сообщений → ~500 000 DOM-нод

Chrome начинает задыхаться на ~100K нод. При полумиллионе таб просто ложится. Виртуализации списка нет.

Решение

Идея тривиальная: перехватить ответ API до того как React его получит, и обрезать mapping до последних N сообщений.

1. Перехват fetch. Content script с "world": "MAIN" позволяет подменить window.fetch:

const originalFetch = window.fetch;

window.fetch = async function(...args) {
  const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || '';
  
  if (!/\/backend-api\/conversation\/[0-9a-f-]{36}$/.test(url)) {
    return originalFetch.apply(this, args);
  }
  
  const response = await originalFetch.apply(this, args);
  const data = await response.clone().json();
  const truncated = truncateMapping(data, 100);
  
  return new Response(JSON.stringify(truncated), {
    status: response.status,
    headers: response.headers,
  });
};

2. Обрезка. Идём от current_node по цепочке parent, собираем последние N сообщений:

function truncateMapping(data, max) {
  const keep = new Set();
  let id = data.current_node;
  let count = 0;

  while (id && count < max) {
    const node = data.mapping[id];
    if (!node) break;
    keep.add(id);
    if (node.message?.content) count++;
    id = node.parent;
  }

  const result = {};
  for (const id of keep) {
    const node = { ...data.mapping[id] };
    node.children = node.children?.filter(c => keep.has(c)) || [];
    if (node.parent && !keep.has(node.parent)) node.parent = null;
    result[id] = node;
  }

  return { ...data, mapping: result };
}

3. Кэш. Полный JSON сохраняется в Map. Кнопка "загрузить ещё" увеличивает окно и пересобирает из кэша — без повторного запроса.

Результат

  • Разговор на 3000+ сообщений грузится мгновенно

  • AI видит полную историю (обрезка только на стороне рендеринга)

  • 30 КБ, ноль зависимостей, ноль внешних запросов

Задача уровня react-window — библиотеки на 6KB, которая существует с 2018 года. Чинится за вечер. Одним человеком. В Chrome extension.

Ссылки

Если у вас есть длинные разговоры в ChatGPT которые вы боитесь открывать — попробуйте.

* * *

А, чуть не забыл:

Как починить фронтенд продукта компании за $800B за вечер?

Да просто надо как следует разозлиться, упереться рогом, и разобраться как оно устроено. Код за вас напишет AI.