Ускорить таблицу на React в 1 000 раз, изменив лишь одну строку

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

JP Camara, главный инженер Wealthbox, в своём блоге поделился интересным опытом ускорения TanStack Table — новой версии React-библиотеки для создания функциональных таблиц — аж до 10 мс. Делимся с вами переводом его статьи.

JP Camara

Главный инженер Wealthbox — CRM для финансовых консультантов

Несколько месяцев назад я работал над интерфейсом JavaScript для большого набора данных, используя TanStack Table. Ограничения были такие: 

  • максимум 50 тыс. строк контента,

  • группировка до 3 колонок.

Производительность была хорошей при использовании React и виртуализированного рендеринга для отображения 50 000 строк. Когда же я включил функцию группировки таблиц TanStack, производительность упала уже на нескольких тысячах строк и стала чрезвычайно низкой при 50 000 строк.

Я бы мог и не обратить внимания, если бы всё замедлялось на 100 мс или даже на 500 мс. Но в самых тяжёлых случаях рендеринг строк занимал больше 1 секунды без группировки и до 30–40 секунд с группировкой.

Как я выявил проблему

Сначала я использовал Chrome-профайлер JavaScript, но с ним сложно работать на низкой производительности. Профайлер накладывает на код заметную нагрузку. Исполнение кода уже занимало 30–40 секунд, поэтому профайлер не годился. Оценить производительность при анализе кода React — вообще сложно: некоторые части внутреннего кода используются слишком часто, поэтому результаты трудно расшифровать.

У меня был запасной план, и я обратился к старому доброму console.time. Таймер помогает увидеть, сколько времени занимает выполнение участка кода:

console.time('expensive code');
thisIsExpensive();
console.timeEnd('expensive code');
// console.time
//   expensive code: 1000ms

На будущее учёл, что можно воспользоваться: console.profile. Он подходит для профилирования небольших блоков кода, а не для расшифровки всего стека рендеринга React. Но, в данном случае, учитывая медленную скорость исходного кода, метод бы не помог. Подробнее о нём можно почитать здесь.

Мы, программисты, полны идей о том, ЧТО нужно оптимизировать, и не умеем делать это правильно. Мы обоснованно предполагаем, ЧТО важно оптимизировать и ГДЕ зарыта проблема. Но, пока мы это не ИЗМЕРИМ, наши суждения ошибочны. 

Я начал оборачивать как можно больше кода в тестирование, чтобы убедиться, что верно выбрал части кода для оптимизации. Затем сужал бенчмарк до конкретного куска, который нужно улучшить.

Вот общая того схема, как я нашёл проблему с производительностью:

console.time('everything');
elements.forEach(() => {
  console.time('methodCall');
  methodCall(() => {
    console.time('build');
	build();
	console.timeEnd('build');
  });
  console.timeEnd('methodCall');
});
console.timeEnd('everything');
// build      49ms
// methodCall 50ms
// build      51ms
// methodCall 52ms
// everything 102ms

Все мои предположения о потенциальной проблеме с производительностью ДО проведения измерений оказались ошибочными:

  • С моим кодом всё было нормально. Ошибка была в библиотеке, которую я использовал, — для меня это стало неожиданностью. Я проверил каждую строчку кода, и все они работали хорошо. Тормозил всё код библиотеки. Вся логика для таблицы TanStack в React сосредоточена в хуке useReactTable.  Проблема с производительностью возникала именно там:

console.time('everything');
customCode();
console.time('useReactTable');
useReactTable(...);
console.timeEnd('useReactTable');
console.timeEnd('everything');
// useReactTable 31500 ms
// everything    31537 ms
  • Когда пишешь на JavaScript, одним из преимуществ работы с пакетами является то, что в любой момент можно открыть папку node_modules и поиграться со сторонним кодом. Я смог изменить исходный код TanStack Table напрямую, добавив информацию о времени загрузки.

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

Чуть ниже даю сокращённую версию исходного кода сгруппированных строк с первым замером времени загрузки. Я пытался вычислить проблемные части кода, измеряя, сколько времени они выполнялись. Здесь стоит обратить внимание на операторы console.time.

function getGroupedRowModel<TData extends RowData>() {
  console.time('everything');
  //...
	
  console.time('grouping filter')
  const existing = grouping.filter(columnId =>
    table.getColumn(columnId)
  )
  console.timeEnd('grouping filter')
	
  const groupUpRecursively = (
    rows: Row<TData>[],
    depth = 0,
    parentId?: string
  ) => {
    if (depth >= existing.length) {
      return rows.map(row => {
      row.depth = depth
      //...
      if (row.subRows) {
        console.time('subRows')
        row.subRows = groupUpRecursively(
          row.subRows, 
          depth + 1
        )
        console.timeEnd('subRows')
      }
      return row
    });
	
    const columnId: string = existingGrouping[depth]!
    const rowGroupsMap = groupBy(
      rows, 
      columnId
    )
	
    const aggregatedGroupedRows = Array.from(rowGroupsMap.entries()).map(([groupingValue, groupedRows], index) => {
      let id = `${columnId}:${groupingValue}`
      id = parentId ? `${parentId}>${id}` : id
      console.time(
        'aggregatedGroupedRows groupUpRecursively'
      )
      const subRows = groupUpRecursively(
        groupedRows, 
        depth + 1,
        id
      )
      console.timeEnd(
        'aggregatedGroupedRows groupUpRecursively'
      )
      //...
      }
    }
  }
  console.timeEnd('everything');
}

Мне казалось, что виновата функция groupUpRecursively. Логичное предположение, что десятки тысяч рекурсивных вызовов могут снижать производительность (спойлер: как обычно, я ошибся

Источник: https://habr.com/ru/companies/netologyru/articles/750246/


Интересные статьи

Интересные статьи

Компоненты React проявляют себя лучше всего, когда говорят: «Дайте мне данные, и я их нарисую». В этом сценарии мы будем практиковаться в отображении данных, о которых знает компонент. Данные ”отрисов...
Свежие новости и статьи из мира фронтенд-разработки за последнюю неделю 25 апреля–1 мая.— Сделайте так, чтобы поиск по странице работал даже в сворачиваемых элементах, с помощью атрибута hidden, но с ...
Привет, друзья! В этом небольшом туториале я покажу вам, как разработать простой редактор кода на React. Обратите внимание: туториал рассчитан, преимущественно, на начинающих разработчиков, хо...
В июле этого года AliExpress сообщил о новом инструменте, который с помощью машинного обучения автоматизирует и ускоряет загрузку товаров на платформу. Этот же способ подходит интернет-магазинам, чтоб...
React-admin позволяет быстро получить достаточно быструю, красивую и функциональную админку. Нужно только написать dataProvider или подстроить бэк под него.Чтобы этот процесс максимально...