Небольшое практическое исследование было вдохновлено статьей https://itnext.io/using-css-to-speed-up-your-react-apps-a26470829472, а конкретно следующим отрывком (перевод мой):

Hiding instead of unmounting

Представьте, что у вас есть 2 вкладки. При переключении между ними стандартными методами “React” происходит размонтирование “таба из” и монтирование “таба в”. ... пример из Твиттера ...

Ответ кроется в React runtime. Каждый раз при монтировании нового компонента, React должен создать virtual DOM (связанный список всех подкомпонентов внутри смонтированного дерева),выполнить всю логику внутри каждого из компонентов (или deprecated componentWillMount ) и затем добавить компоненты в DOM. В то же время, React должен размонтировать предыдущий компонент,что означает - свернуть virtual DOM, прогнать componentWillUnmount и затем удалить соответствующие элементы из DOM.

Возможно, это окажется сюрпризом, но размонтирование - не дешевая операция. Вы можете подумать, что это просто удаление, но React должен удалить рефы из многих участков virtual DOM и прогнать жизненные циклы всех компонентов, которые будут отмонтированы (unmounted). Это может происходить не быстро. До времен React Fiber (перед React 15.x.x.) размонтирование предыдущего и монтирование следующего компонента было последовательным. Нынче, React производит это параллельно для экономии времени, с учетом того, что эти процессы не зависят друг от друга.

Фокус в том,чтобы не заставлять React монтировать\размонтировать,а использовать CSS для показа\скрытия содержимого табов.

Дальше было про тот же пример из Твиттера и то, что при таком подходе к разработке - DOM дерево может слишком разрастись. Как разработчик приложения, которое потенциально может генерировать очень много компонентов и огромное количество табов (и человек, получивший задачу разобраться - а не будет ли это хорошей оптимизацией))) - я решил написать простой код для проверки этой теории и соотвественно последствий:

import './App.css';
import uniqid from 'uniqid';
import React,{useState} from 'react';

function App() {
  const [tab, setTab] = useState(true)

  const Row = (quantity, bColor) => {
    const result = []
    for (let i =0; i< quantity; i+=1){
    result.push(<div key={uniqid + i} style={{height: '30px', width: '200px', border:'1px solid '+ bColor }}>Row {i} Tab {tab?1:2}</div>)
  }
    return result
};
const handleClick = () => {
  console.log(tab)
  setTab(tabState=>!tabState)
}

  return (
    <div className="App">
      <button type='button' onClick={handleClick}>Вкладка {tab?1:2}</button>
      <div style={{height: '300px', width: '300px', overflow:'auto', border:'1px solid gray', margin: '0 auto' }}>
    {/* { <ul>
      {tab && Row(50000, 'tomato')} */}
    { <ul style={{display:!tab?'none':'block'}}> 
      {Row(50000, 'tomato')}
    </ul>}
     {/* { <ul>
       {!tab && Row(50000, 'green')} */}
    { <ul style={{display:tab?'none':'block'}}>
      {Row(50000, 'green')}
    </ul>}
    </div>
    </div>
  );
}

export default App;

Единственная бибилиотека, которая была установлена после CRA и дальнейшей чистки от лишнего - это uniqueId (для дополнительной нагрузки и дополнительный демонстрации - как работают ключи). Код имитирует переключение между двумя табами по нажатию кнопок - закомментированный участок кода из 2х строк заменяет собой раскомментированный и наоборот. Мы генерируем 2 списка по 50000 строк (в зависимости от мощности вашего компьютера - для наглядности, попробуйте поиграть с этой цифрой, если отрабатывает слишком быстро или слишком медленно) с немного разными стилями.

Как это работает, уверен, каждый в состоянии проверить сам, так что перейду сразу к выводам:

  1. в случае с работой через React-way у меня на ноутбуке переключение между табами составляет от 7 до 10 секунд

  2. в случае с работой через скрытие display:none\display:block - переключение от 1.5 до 2.5 секунд

Казалось бы вопросы сняты - можно пользоваться, но давайте посмотрим на выделение памяти для chrome.exe(для win10 - это найти через поиск "монитор ресурсов"):

  1. потребление памяти находится в рамках 600-1200мб в случае с рендером методами React

  2. от 1800 до 3200мб в случае со скрытием табов через стили

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

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


  1. harios
    04.10.2022 12:46
    +3

    1. потребление памяти находится в рамках 600-1200мб в случае с рендером методами React

    2. от 1800 до 3200мб в случае со скрытием табов через стили

    Я тебе какое зло сделал


    1. DarthVictor
      07.10.2022 00:39
      +1

      Ну так

      Мы генерируем 2 списка по 50000 строк

      Вообще, можно ли нагрузочный тест считать корректным, если пробки не выбило?


  1. devlev
    04.10.2022 13:35
    +1

    Из личного опыта могу сказать что такое решение подойдет не во всех случаях. Дочерние компоненты в табе могу создавать различные вебворкеры, постоянные соединения с сервером, или делать циклически какие то действие, а так же как то реагировать на размеры окна и тем самым подстраиваться под видимую область. В случае с использованием подхода с display:none нужно учитывать это в каждом компоненты которые будет в скрытой вкладке и как то реагировать на это. Это приводит к еще больше накладным расходам. Все это негативно влияет как на расходуемую память так и на стабильность работы.

    Ну и пара признать что не стоит рендерить 50к строк и надеяться что все будет работать быстро. Виртуальный скролл решает данную проблему.


    1. Zakhar_82 Автор
      04.10.2022 22:49

      Да, конечно, всегда необходимо взвешивать всё в сумме. У нас многие компоненты используют react-window (ну как много ... два). Постарался предложить максимально изолированный и простой пример. Мне кажется, что скрывать табы разумно было бы в случае, если у тебя есть условная лента и несколько вспомогательных табов, которые просты и предсказуемы, чтобы не терять не перезагружать и не перерендеревать ленту при переключении.


  1. MentalBlood
    04.10.2022 14:07

    Очень не хватает графиков зависимостей потребления памяти от кол-ва строк


  1. nin-jin
    04.10.2022 21:17
    -3

    Тут проходил мимо $mol_man, набросал по быстрому вёрстку:

    $my_tabs $mol_view
    	count 50000
    	sub /
    		<= Deck $mol_deck items /
    			<= Left $mol_list
    				title \Left
    				rows <= left /
    					<= Left_row*0 $mol_paragraph
    						sub / <= row_text* \Row 0
    			<= Right $mol_list
    				title \Right
    				rows <= right /
    					<= Right_row*0 $mol_paragraph
    						sub / <= row_text* \Row 0

    Раскрасил её стилями:

    namespace $.$$ {
    	
    	const { px } = $mol_style_unit
    	
    	$mol_style_define( $my_tabs, {
    		
    		Left_row: {
    			border: {
    				width: px(1),
    				style: 'solid',
    				color: 'green',
    			},
    			padding: $mol_gap.text,
    		},
    		
    		Right_row: {
    			border: {
    				width: px(1),
    				style: 'solid',
    				color: 'tomato',
    			},
    			padding: $mol_gap.text,
    		},
    		
    	} )
    	
    }

    Оживил логикой:

    namespace $.$$ {
    	export class $my_tabs extends $.$my_tabs {
    		
    		@ $mol_mem
    		left() {
    			return Array.from( { length: this.count() }, ( _, i )=> this.Left_row( i )  )
    		}
    		
    		@ $mol_mem
    		right() {
    			return Array.from( { length: this.count() }, ( _, i )=> this.Right_row( i )  )
    		}
    		
    		row_text( index: number ) {
    			return `Row ${ index } Tab ${ this.Deck().current() }`
    		}
    		
    	}
    }
    

    И получил чуть больше секунды на переключение табов и 140мб потребления памяти:

    Слишком быстро, подумал он, и пошёл учить react-way.


    1. Zakhar_82 Автор
      04.10.2022 22:40

      Спасибо большое за комментарий, впервые от Вас услышал про $mol, я так понимаю - точка входа https://mol.hyoo.ru/, как раз скоро отпуск - буду расширять кругозор)))


    1. Gary_Ihar
      05.10.2022 13:25
      +2

      Синтаксис - боль


      1. nin-jin
        05.10.2022 14:55
        -1

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