Когда мне приходится рассказывать о React, или когда я даю первую лекцию учебного курса, показывая всякие интересные вещи, кто-нибудь непременно спросит: «Встроенные функции? Слышал, они медленные».



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

Что такое «встроенная функция»?


В контексте React то, что называют встроенной функцией (inline function) — это функция, которая определяется в процессе рендеринга. В React есть два значения понятия «рендеринг», которые часто путают. Первое относится к получению элементов React из компонентов (вызов методов render компонентов) в процессе обновления. Второе — это реальное обновление фрагментов страницы путём модификации DOM. Когда я в этой статье говорю о «рендеринге», то имею в виду именно первый вариант.

Вот несколько примеров встроенных функций:

class App extends Component {
  // ...
  render() {
    return (
      <div>
        
        {/* 1. встроенный обработчик событий "компонента DOM" */}
        <button
          onClick={() => {
            this.setState({ clicked: true })
          }}
        >
          Click!
        </button>
        
        {/* 2. "Кастомное событие" или "действие" */}
        <Sidebar onToggle={(isOpen) => {
          this.setState({ sidebarIsOpen: isOpen })
        }}/>
        
        {/* 3. Коллбэк свойства render */}
        <Route
          path="/topic/:id"
          render={({ match }) => (
            <div>
              <h1>{match.params.id}</h1>}
            </div>
          )
        />
      </div>
    )
  }
}

Преждевременная оптимизация — корень всех зол


Прежде чем продолжать, нам нужно поговорить о том, как оптимизировать программы. Спросите любого профессионала в области производительности, и он скажет вам, что преждевременная оптимизация — это зло. Это относится абсолютно ко всем программам. Любой, кто знает толк в оптимизации, может это подтвердить.

Помню выступление моего друга Ральфа Холзманна, посвящённое gzip, которое по-настоящему упрочило во мне эту идею. Он говорил об эксперименте, который провёл с LABjs, старой библиотекой для загрузки скриптов. Можете посмотреть это выступление. То, о чём я тут говорю, происходит в течение примерно двух с половиной минут, начинаясь с 30-й минуты видео.

В то время в LABjs было сделано кое-что странное, направленное на оптимизацию размера готового кода. Вместо использования обычной объектной нотации (obj.foo) там применялось хранение ключей в строках и использование квадратных скобок для доступа к содержимому объектов (obj[stringForFoo]). Причиной подобного было то, что после минификации и сжатия кода с помощью gzip необычно написанный код должен был бы стать меньше, чем код, который написан привычным способом.

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

Оказалось, что избавление от «оптимизации» позволило сократить размер итогового файла на 5.3%! Очевидно, автор библиотеки писал её сразу в «оптимизированном» виде, не проверяя, даст ли это какие-то преимущества. Без измерений невозможно узнать, улучшает ли что-нибудь некая оптимизация. Кроме того, если оптимизация только ухудшает положение дел, вы об этом тоже не узнаете.

Преждевременная оптимизация не только может значительно увеличить время разработки, ухудшая чистоту кода, она может иметь негативные последствия и привести к проблемам, как это было с LABjs. Если бы автор библиотеки проводил замеры вместо того, чтобы воображать проблемы с производительностью, он сэкономил бы время на разработку, выпустил бы более чистый код, обладающий лучшими характеристиками.

Процитирую тут этот твит: «Меня раздражает, когда люди, развалившись в кресле, рассуждают о том, что некий код будет медленным для решения их задач, не проводя никаких замеров производительности». Я эту точку зрения поддерживаю.

Итак, повторюсь — не занимайтесь преждевременной оптимизацией. А теперь — вернёмся к React.

Почему говорят, что встроенные функции ухудшают производительность?


Встроенные функции считают медленными по двум причинам. Во-первых — это связано с опасениями, касающимися потребления памяти и сборки мусора. Во вторых — из-за shouldComponentUpdate. Разберём эти опасения.

?Потребление памяти и сборка мусора


Для начала, программисты (и конфигурации estlint) обеспокоены потреблением памяти и нагрузкой на систему от сборки мусора при создании встроенных функций. Это — наследие тех дней, когда стрелочные функции в JS ещё не получили широкого распространения. Если в React-коде, во встроенных конструкциях, часто использовалась команда bind, это, исторически, вело к плохой производительности. Например:

<div>
  {stuff.map(function(thing) {
    <div>{thing.whatever}</div>
  }.bind(this)}
</div>

Проблемы с Function.prototype.bind были исправлены здесь, а стрелочные функции, либо применялись как встроенные возможности языка, либо транспилировались с помощью babel в обычные функции. И так и так мы можем считать, что медленными они не являются.

Помните о том, что не надо делать предположений о том, что некий код окажется медленным. Пишите код так, как делаете это всегда и замеряйте производительность. Если удаётся найти какие-то проблемы — исправляйте их. Вам не надо доказывать, что стрелочные функции работают быстро — пусть кто-нибудь другой докажет, что они медленны. Иначе это — преждевременная оптимизация.

Насколько я знаю, никто пока не привёл исследование своего приложения, указывающее на то, что встроенные функции приводят к проблемам с производительностью. До этого момента не стоит даже об этом говорить, однако, я, в любом случае, поделюсь тут ещё одной идеей.
Если нагрузка на систему от создания встроенной функции достаточно высока для того, чтобы создавать специальное правило eslint для предотвращения этого, с чего бы нам стремиться перемещать эти тяжёлые операции в весьма важный с точки зрения воздействия на скорость работы системы блок инициализации?

class Dashboard extends Component {
  state = { handlingThings: false }
  
  constructor(props) {
    super(props)
    
    this.handleThings = () =>
      this.setState({ handlingThings: true })

    this.handleStuff = () => { /* ... */ }

    // ещё больше нагрузки на систему с bind
    this.handleMoreStuff = this.handleMoreStuff.bind(this)
  }

  handleMoreStuff() { /* ... */ }

  render() {
    return (
      <div>
        {this.state.handlingThings ? (
          <div>
            <button onClick={this.handleStuff}/>
            <button onClick={this.handleMoreStuff}/>
          </div>
        ) : (
          <button onClick={this.handleThings}/>
        )}
      </div>
    )
  }
}

Занимаясь предварительной оптимизацией, мы замедлили инициализацию компонента в три раза. Если бы все обработчики событий были встроенными функциями, первоначальному вызову render надо было бы создать лишь одну функцию. Вместо этого мы создаём три. Причём, никаких замеров производительности не проводилось, поэтому у нас нет причины считать это проблемой.

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

?PureComponent и shouldComponentUpdate


Настоящая суть проблемы кроется в PureComponent и shouldComponentUpdate. Для того, чтобы осмысленно заниматься оптимизацией производительности, нужно понимать две вещи: особенности shouldComponentUpdate, и то, как работает сравнение на строгое равенство в JavaScript. Не понимая этих концепций, можно, пытаясь сделать код быстрее, только всё ухудшить.

Когда вызывают setState, React сравнивает старый элемент с новым (это называется согласованием), а затем использует полученную информацию для обновления элементов реального DOM. Иногда эта операция может происходить довольно медленно, если имеется слишком много элементов, которые надо проверять (что-то вроде большого SVG). В таких случаях React предоставляет обходной путь, который называется shouldComponentUpdate.

class Avatar extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    return stuffChanged(this, nextProps, nextState))
  }
  
  render() {
    return //...
  }
}

Если у компонента задан shouldComponentUpdate, прежде чем React сравнит старый и новый элементы, он обратится к shouldComponentUpdate для того, чтобы узнать о том, изменилось ли что-нибудь. Если в ответ вернётся false, React полностью пропустит операцию сравнения элементов, что сэкономит какое-то время. Если компонент достаточно велик, это может привести к заметному влиянию на производительность.

Самый распространённый способ оптимизации компонента — расширение React.PureComponent вместо React.Component. PureComponent будет сравнивать свойства и состояние в shouldComponentUpdate, в результате, вам не придётся делать это самостоятельно.
class Avatar extends React.PureComponent { ... }

Класс Avatar теперь использует строгое сравнение на равенство при работе со свойствами и состоянием прежде чем запрашивать обновления. Можно ожидать, что это ускорит работу программы.

?Сравнение на строгое равенство


В JavaScript существует шесть примитивных типов: string, number, boolean, null, undefined, и symbol. Когда выполняют строгое сравнение двух переменных примитивных типов, которые хранят оно и то же значение, получается true. Например:

const one = 1
const uno = 1
one === uno // true

Когда PureComponent сравнивает свойства, он использует строгое сравнение. Это отлично работает для встроенных примитивных значений вроде <Toggler isOpen={true}/>.

Проблема при сравнении свойств возникает для других типов, то есть, извините — единственного типа. Всё остальное в JS — это Object. А как же функции и массивы? На самом деле всё это — объекты. Позволю себе процитировать выдержку из документации MDN: «Функции — это обычные объекты, имеющие дополнительную возможность быть вызванными для исполнения».

Ну что тут скажешь — JS это JS. В любом случае, строгое сравнение разных объектов, если даже они содержат одни и те же значения, вернёт false.

const one = { n: 1 }
const uno = { n: 1 }
one === uno // false
one === one // true

Итак, если вы встраиваете объект в JSX-код, адекватное сравнение свойств в PureComponent окажется невозможным, в результате чего будет произведено более трудоёмкое сравнение элементов React. Это сравнение выяснит лишь то, что компонент не изменился, как результат — потеря времени на двух сравнениях.

// первый рендер
<Avatar user={{ id: ‘ryan’ }}/>

// следующий рендер
<Avatar user={{ id: ‘ryan’ }}/>

// сравнение свойств полагает, что что-то изменилось, так как {} !== {}
// сравнение элементов (согласование) выясняет, что ничего не изменилось

Так как функции — это объекты, и PureComponent выполняет строгую проверку на равенство свойств, сравнение встроенных функций при анализе свойств всегда оканчивается сообщением о том, что они разные, после чего будет осуществлён переход к сравнению элементов в ходе процедуры согласования.

Вы можете заметить, что относится это не только ко встроенным функциям. То же самое можно сказать и об обычных объектах, и о массивах.

Для того, чтобы shouldComponentUpdate делал при сравнении одинаковых функций то, чего мы от него ожидаем, нужно сохранять ссылочную идентичность функций. Для опытных JS-разработчиков это — не такая уж и плохая новость. Но, если учесть то, что Майкл и я узнали после обучения примерно 3500 человек, имеющих различный уровень подготовки, можно отметить, что эта задача оказалась для наших учеников не такой уж и простой. Надо отметить, что и классы ES тут не помогают, поэтому в данной ситуации приходится пользоваться другими возможностями JS:

class Dashboard extends Component {
  constructor(props) {
    super(props)
    
    // Используем bind? Это замедляет инициализацию и, если такое повторяется раз 20,
    // ужасно смотрится.
    // Кроме того, это увеличивает размер пакета.
    this.handleStuff = this.handleStuff.bind(this)

    // _this - это дурной тон.
    var _this = this
    this.handleStuff = function() {
      _this.setState({})
    }
    
    // Если вам доступны классы ES, то, возможно, вы можете использовать и 
    // стрелочные функции (то есть, работаете с babel или с современным браузером).
    // Это не так уж и плохо, но перемещение всех обработчиков в конструктор - это уже
    // не так уж и хорошо.
    this.handleStuff = () => {
      this.setState({})
    }
  }
  
  // так куда лучше, но это пока за пределами JavaScript,
  // поэтому тут можно задаться вопросом о том, как работает комитет TC39 и
  // как он оценивает предложения по языку.
  handleStuff = () => {}
}

Тут надо отметить, что изучение приёмов сохранения ссылочной идентичности функций ведёт к удивительно длинным беседам. У меня нет причин призывать к этому программистов, разве что им захочется выполнить требования их конфигурации eslint. Главное, что мне хотелось показать — это то, что встроенные функции не мешают оптимизации. А теперь поделюсь собственной историей оптимизации производительности.

Как я работал с PureComponent


Когда я впервые узнал о PureRenderMixin (это — конструкция из ранних версий React, которая позже превратилась в PureComponent), я использовал множество измерений и оценил производительность моего приложения. Затем я добавил PureRenderMixin ко всем компонентам. Когда я предпринял измерение производительности оптимизированной версии, то надеялся, что в результате всё будет так замечательно, что я смогу с гордостью всем об этом рассказывать.

Однако, к моему великому удивлению, приложение стало работать медленнее.

Почему? Подумаем об этом. Если у вас есть некий Component, сколько операций сравнения приходится выполнять при работе с ним? А если речь идёт о PureComponent? Ответы, соответственно, заключаются в следующем: «только одно», и «как минимум одно, а иногда — два». Если обычно компонент при обновлении меняется, то PureComponent будет выполнять две операции сравнения вместо одной (свойства и состояние в shouldComponentUpdate, а затем — обычное сравнение элементов). Это означает, что обычно PureComponent будет медленнее, но иногда — быстрее. Очевидно, большинство моих компонентов постоянно менялись, поэтому, в целом, приложение стало работать медленнее. Печально.

Универсального ответа на вопрос: «Как повысить производительность?» нет. Ответ можно найти только в замерах производительности конкретного приложения.

О трёх сценариях использования встроенных функций


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

?Обработчик событий компонента DOM


<button
  onClick={() => this.setState(…)}
>click</button>

Обычно внутри обработчиков событий для кнопок, полей ввода и других компонентов DOM, не делается ничего кроме вызова setState. Это обычно делает встроенные функции наиболее чистым подходом. Вместо того, чтобы прыгать по файлу в поисках обработчиков событий, их можно найти в коде описания элемента. Сообщество React обычно приветствует подобное.

Компонент button (и любой другой компонент DOM) даже не может быть PureComponent, поэтому тут не нужно беспокоиться о shouldComponentUpdate и о ссылочной идентичности.

В результате, это можно считать медленным только в том случае, если согласиться с тем, что простое определение функции — это достаточно большая нагрузка на систему, о которой стоит беспокоиться. Нет подтверждений того, что это так. Неоправданное избавление от встроенных обработчиков событий — это знакомая нам преждевременная оптимизация.

?«Кастомное событие» или «действие»


<Sidebar onToggle={(isOpen) => {
  this.setState({ sidebarIsOpen: isOpen })
}}/>

Если Sidebar — это PureComponent, мы не пройдём сравнение свойств. Опять же, так как обработчик прост, его встраивание может оказаться наилучшим выходом.

Теперь поговорим о событиях вроде вроде onToggle, и о том, почему Sidebar занимается их сравнением. Есть лишь две причины поиска различий в свойствах в shouldComponentUpdate:

  1. Свойство используют для рендеринга.
  2. Свойство используют ради достижения побочного эффекта в componentWillReceiveProps, в componentDidUpdate, или в componentWillUpdate.

Большинство свойств вида on<whatever> не соответствуют этим требованиям. Таким образом, большинство вариантов использования PureComponent ведут к выполнению ненужных сравнений, что принуждает разработчиков поддерживать, без необходимости, ссылочную идентичность обработчиков.

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

Для большинства компонентов я бы посоветовал создать класс PureComponentMinusHandlers и наследоваться от него, вместо того, чтобы наследоваться от PureComponent. Это поможет просто пропустить все проверки функций. Как раз то, что нужно. Ну — почти то, что нужно.

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

// 1. Приложение передаст свойство форме.
// 2. Форма собирается передать функцию кнопке,
// которая перекрывает свойство, полученное от приложения.
// 3. Приложение собирается выполнить setState после монтирования и передать
// *новое* свойство форме.
// 4. Форма передаёт новую функцию кнопке, перекрывая
// новое свойство.
// 5. Кнопка проигнорирует новую функцию и не сможет 
// обновить обработчик нажатия, её передача будет осуществлена 
// с устаревшими данными.
class App extends React.Component {
  state = { val: "one" }

  componentDidMount() {
    this.setState({ val: "two" })
  }

  render() {
    return <Form value={this.state.val} />
  }
}

const Form = props => (
  <Button
    onClick={() => {
      submit(props.value)
    }}
  />
)

class Button extends React.Component {
  shouldComponentUpdate() {
    //Давайте представим, будто мы сравнили всё, кроме функции.
    return false
  }

  handleClick = () => this.props.onClick()

  render() {
    return (
      <div>
        <button onClick={this.props.onClick}>This one is stale</button>
        <button onClick={() => this.props.onClick()}>This one works</button>
        <button onClick={this.handleClick}>This one works too</button>
      </div>
    )
  }
}

> Здесь с этим кодом можно поэкспериментировать.

Итак, если вам нравится идея наследоваться от PureRenderWithoutHandlers, не передавайте ваши обработчики, не участвующие в сравнении, напрямую другим компонентам — их надо каким-то способом обернуть.

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

Должен честно сказать, что это приложение-пример — добавка к материалу, которую я сделал после публикации с подачи Эндрю Кларка. Так может показаться, что я точно знаю, когда надо поддерживать ссылочную целостность, а когда — нет.

?Свойство render


<Route
  path="/topic/:id"
  render={({ match }) => (
    <div>
      <h1>{match.params.id}</h1>}
    </div>
  )
/>

Свойства render — это шаблон, используемый для создания компонента, который существует для создания и поддержания разделяемого состояния (тут об этом можно почтить подробнее). Содержимое свойства render неизвестно компоненту. Например:

const App = (props) => (
  <div>
    <h1>Welcome, {props.name}</h1>
    <Route path="/" render={() => (
      <div>
        {/*
          props.name находится за пределами Route и оно не передаётся
          как свойство, поэтому Route не соответствует
          идеологии PureComponent, у него
          нет сведений о том, что здесь появится после рендеринга.
        */}
        <h1>Hey, {props.name}, let’s get started!</h1>
      </div>
)}/>
  </div>
)

Это означает, что встроенная функция свойства render не приведёт к проблемам с shouldComponentUpdate. Компонент недостаточно информирован для того, чтобы его можно было бы преобразовать в PureComponent.

Итак, опять же, доказательств медленности свойств render у нас нет. Всё остальное — мысленные эксперименты, не имеющие отношения к реальности.

Итоги


  1. Пишите код так, как привыкли, реализуя в нём ваши идеи.
  2. Проводите замеры производительности для того, чтобы находить узкие места. Здесь можно узнать о том, как это сделать.
  3. Используйте PureComponent и shouldComponentUpdate только при необходимости, пропуская функции, являющиеся свойствами компонента (только если они не используются в перехватчиках событий жизненного цикла для достижения побочных эффектов).

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

Уважаемые читатели! Как вы оптимизируете React-приложения?

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


  1. jakobz
    13.10.2017 14:22

    Мы React используем с первых версий, и уже несколько десятков приложений склепали. И я в упор не понимаю весь этот хайп про оптимизацию реакта.

    У меня опыт такой:
    — все что влезает на экран, даже весьма сложные UI — тормозить не будет, даже если перерисовывать вообще все на каждое действие.
    — если все влезает в 2-3 экрана — это оптимизируется одним-двумя shouldComponentUpdate-ами, где-то в серединке — чтобы отсечь обновления
    — если у нас больше 3 экранов — всяке гриды, ленты, длинные списки — тормозит уже браузер, и это лечится всякими виртуальными скроллингами. Оптимизировать реакт в таких случая особо смысла нет.

    Частая причина тормозов — отстутствие у JS-разработчиков базовых знаний про алгоритмы. Например, не привыты навыки заменять линейный поиск на lookup по хешу. Может потому что оно неудобно в JS. Могут написать алгоритм на O(N^3) на доставании данных, и потом «что-то реакт тормозит».


    1. faiwer
      13.10.2017 17:43

      То, что влезает на 1 экран может содержать как 10 элементов, так и 100, так и 1000. Элементарнейший пример — excel-табличка против простого todo-списка. Да, в таких вещах используется вирт. скроллинг (иначе оно даже не загрузилось на мало-мальски среднем файле). Но даже одного экрана таблицы хватает, чтобы, при кривой реализации, выжрать всю батарейку вашего ноутбука или мобильника в краткие сроки.


      Касательно асимптотики. Ну вот возьмём типичное redux+react приложение. Пусть у нас в react-древе будет всего пару сотен элементов. Мы совершили какое-то действие и store обновился. На самом верхнем уровне за счёт connect-subscribe вызывается render корневого или около-корневого react-компонента. Мы забили на здравый смысл и не используем PureComputed. Собственные shouldComponentUpdate function-ы мы не пишем тоже. Зачем? Что в итоге? render по цепочке вызывается для всех 200 элементом нашего react-древа. На любой чих. Всегда. Всегда сформировывается новое древо. И всегда сверяется со старым. В итоге в DOM улетает одно обновление, скажем, setAttribute.


      А теперь у нас лимитированное кол-во connect-ов (используем с мозгами), мы используем мемоизацию для тяжёлых вычислений, не пробрасываем callback-prop-ы свежесгенерированными анонимками. Обновление store-а затрагивает несколько mapStateToProps и производится несколько shallow-сравнений. render вызывается только у одного компонента и на выходе мы получаем тот же самый setAttribute.


      Разница огромная. На сложных приложениях она заметна не то, что невооружённым взглядом, а на километровом расстоянии.


      Кривая реализация на авось может загубить даже простейшие приложения. Простой пример — у меня дичайше тормозило приложение при открытых devTools-ах. Просто адово. Причина? Redux-расширение. Оно с, настройками по умолчанию, сериализовывало ВЕСЬ store на любой чих туда и обратно. Соответственно оно не имело ни малейшего представления о ссылочной целостности. Съедало столько памяти сколько могло пережевать и начинало медленно помирать. Решилось всё одной единственной настройкой optoins: false, которая will handle also circular reference. С тех пор — порхает как бабочка.


      1. MikailBag
        13.10.2017 18:33

        Кривая реализация на авось может загубить даже простейшие приложения. Простой пример — у меня дичайше тормозило приложение при открытых devTools-ах.

        И вы решили проблему с помощью асимптотической оптимизации, если я правильно понял.
        (Патч вместо всего объекта).


        1. faiwer
          13.10.2017 18:46

          Я просто поменял конфиг для redux-dev-tools-а. Там у него свой middleware подключается. И если указать options: false, то он умеет circular references. И соответственно перестаёт делать 99.999% лишней работы.


          1. MikailBag
            13.10.2017 19:43

            Кто сказал, что оптимизация (в т.ч. асимптотическая) всегда требует больших усилий и ухудшения читабельности кода?


            А ваш случай я понял так.
            Middleware на каждое изменение стора копировало его.
            Вы поменяли опции, и оно начало копировать и сохранять только диффы старого и нового состояния стора.
            Если предположить, что вы пишете абстрактного коня в жидком сферическом вакууме, а вы только добавляете данные к стору примерно равными порциями, но не удаляете, то это ускорение с N^2 до N


            1. faiwer
              13.10.2017 20:10

              Я затрудняюсь с точным расчётом асимптотики этой проблемы, но суть верна, да ;)


      1. Miraage
        15.10.2017 23:30

        Месье, сначала бы посмотрели, как работает модуль react-redux. Открою секрет. Каждый connect() по умолчанию порождает PureComponent, поэтому не будет там лишних обновлений.


        1. faiwer
          16.10.2017 07:03

          miraage, количество лишних vdom-обновлений будет зависеть от устройства конечных mapStateToProps. Но да, вы верно подметили, connect проставит часть pureComponent-ов за нас и часть лишней работы срежет.


  1. kashey
    13.10.2017 14:37

    «Хороший» способ оптимизации — ограничивать область распространения изменений. Не надо вешать на каждый чих shouldUpdate, достаточно в узловых местах добавить connect, которые будет ташить данные из стора, а не родителя, создавать как бы «автономную» область.
    Ну и не стоит забывать что connect создает PureComponent, который только на изменение стора и реагирует.
    Но вообще, по моему опыту, большая часть зла исходит от самого реакта и его экосистемы, чем он кривых рук конечного програмиста.


    1. justboris
      13.10.2017 19:36

      Про экосистему согласен. Есть пример, когда оптимизация через shouldComponentUpdate делает код медленнее чем без него:
      https://github.com/erikras/redux-form/issues/3461


      1. faiwer
        13.10.2017 20:08

        Ну если в shouldComponentUpdate пихать таких слонов как _.isEqual то можно достичь просто фантастических тормозов :)


        shouldComponentUpdate(nextProps) {
          if (JSON.stringify(nextProps) !== JSON.stringify(this.props) {
            ....
          }
        }

        этот пример оттуда просто шикарен :)


        1. justboris
          13.10.2017 20:11

          О том и речь. Не все библиотеки одинаково хороши. В некоторых такие ужасы написаны, которые себе в продакшен тащить не хочется.


  1. mayorovp
    13.10.2017 15:17

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


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


  1. parakhod
    13.10.2017 16:52

    Необходимость в shouldComponentUpdate возникала только с реакт-нативовскими нативными компонентами, да и то когда в нативном коде при обновлении свойств я забывал написать сравнение с предыдущим состоянием и в результате код делал кучу бессмысленной работы. И ещё с компонентом Image в старых версиях на андроиде. Всё.


  1. faiwer
    13.10.2017 17:30
    -2

    У меня "пригорело" ещё от оригинала этой статьи, а тут ещё и перевод подоспел. Честно говоря я не совсем понимаю, что это за приложение у автора было такое,


    Apparently, most of my components changed most of the time, so on the whole, my app got slower. Oops.

    что его компоненты настолько часто изменялись, что shallow-проверки в shouldComponentUpdate оказались overhead-ом, который перекрывает пользу от отсутствия построения лишнего virtualDOM-а.


    Мой ещё не богатый опыт был строго противоположным. Проверки в shouldComponentUpdate ускоряли приложение на порядок (раз в 10), что было хорошо видно на fire-графиках.


    Типичный набор props состоит из 3-7 полей. В итоге типичная shallow-проверка это 3-7 ===. Как это может быть настолько медленным, чтобы не оправдать избавление от лишнего render-а и последующего сравнения двух vdom-деревьев? render — это множественные аллокации и присвоения. Сравнение двух деревьев это куда большее количество ===, чем в shallow-проверке.


    Действительно, единственный вариант, который мне приходит в голову, это когда props-ы и правда меняются практически всегда. Но что это за приложение то такое?


    Касательно преждевременной оптимизации. Где он её тут увидел? Разница только в нотации method(){} против method = (){}. 3 символа?
    Ну а аргумент про то, что callback вынесен далеко от render-метода, и заставляет бегать по файлу… Ну тут на вкус и цвет товарищей нет. Лично я предпочитаю держать render-методы как можно более мелкими, простыми и очевидными. А это значит я не нагромождаю их вычислениями, callback-ми, сложными условиями и пр… Мне кажется, в идеале, render метод должен быть куском html-like кода.


    1. andy128k
      13.10.2017 19:19

      У меня тоже «пригорело». Какие-то «вредные советы» сплошные. Единоразовое создание двух замыканий в конструкторе это у него почему-то overhead, а создание этих же замыканий при каждом рендере (многократно) это нормально.


  1. MikailBag
    13.10.2017 18:35

    1. faiwer
      13.10.2017 18:49

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

      Хоть в рамочку вешай.


  1. dagen
    13.10.2017 22:20

    Статья с намеренно неправильной аналогией в самом начале — это грязный демагогический приём.


    Вдобавок к вышесказанному товарищами faiwer и andy128k: уж если автор советует пихать лямбды в jsx (в то время как все автоматически избегают создания лишних замыканий где угодно, не говоря уж про циклы и часто вызываемые методы, такие как render), то боюсь представить, какова будет его реакция на плагин react-constant-elements, который идёт ещё дальше, и хоистит jsx-элементы.


    P.S. А точнее он начал статью с фотографии своей новой спальни. Спальня красивая, мне понравилась. Но это не спасло статью.


  1. mkuzmin
    14.10.2017 11:15

    Есть подход с мемоизацией.
    https://github.com/timkendrick/memoize-weak
    https://github.com/timkendrick/memoize-bind


    Вторая как раз про реакт. А т.к. объект функции будет тем же самым, то souldComponentUpdate корректно отработает.
    Плюс там используется weak-map, по этому не будет проблем с мусором.


    1. mayorovp
      14.10.2017 13:02

      Слишком сложно как-то написано… На утечки памяти это чудо вообще проверялось?


      1. mkuzmin
        14.10.2017 14:11

        В es6 есть WeakMap. Ключем является объект. Эта связь слабая, если сборщик мусора удалит объект, то map потеряет этот ключ. За счет этого память и не течет.


        https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/WeakMap


        1. mayorovp
          14.10.2017 14:16

          Я знаю про этот класс, но я его не сразу нашел…