Статьи

Оптимизация производительности React с помощью компонентов без сохранения состояния

Логотип React из костей, выставленный в музее рядом с окаменелостями динозавров. Посмотрите на эволюцию компонентов без состояния.

Эта история о компонентах без сохранения состояния . Это означает, что компоненты не имеют this.state = { ... } . Они имеют дело только с входящими «подпорками» и подкомпонентами.

Во-первых, супер основы

 import React, { Component } from 'react' class User extends Component { render() { const { name, highlighted, userSelected } = this.props console.log('Hey User is being rendered for', [name, highlighted]) return <div> <h3 style={{fontStyle: highlighted ? 'italic' : 'normal'}} onClick={event => { userSelected() }} >{name}</h3> </div> } } 

Примечание редактора: мы опробуем CodeSandbox для демонстраций в этой статье.
Сообщите нам свое мнение!

Ура! Оно работает. Это действительно просто, но подает пример.

Что следует отметить:

  • Это без гражданства. Нет this.state = { ... } .
  • Здесь есть console.log чтобы вы могли понять, как он используется. В частности, когда вы выполняете оптимизацию производительности, вы хотите избежать ненужных повторных рендеров, когда реквизиты фактически не изменились.
  • Обработчик событий там «встроенный». Это удобный синтаксис, потому что код для него близок к элементу, который он обрабатывает, плюс этот синтаксис означает, что вам не нужно делать никаких .bind(this) приседаний.
  • С такими встроенными функциями, это небольшое снижение производительности, так как функция должна создаваться при каждом рендеринге. Подробнее об этом позже.

Это презентационный компонент

Теперь мы понимаем, что вышеприведенный компонент не только не имеет гражданства, но и является тем, что Дан Абрамов называет презентационным компонентом . Это просто имя, но в основном оно легкое, дает немного HTML / DOM и не возится с данными о состоянии.

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

Итак, давайте перепишем это:

 const User = ({ name, highlighted, userSelected }) => { console.log('Hey User is being rendered for', [name, highlighted]) return <div> <h3 style={{fontStyle: highlighted ? 'italic' : 'normal'}} onClick={event => { userSelected() }}>{name}</h3> </div> } 

Разве это не прекрасно? Это похоже на чистый JavaScript и что-то, что вы можете написать, не думая об используемой вами платформе.

Они повторяют рендеринг, говорят они 🙁

Предположим, наш маленький User используется в компоненте, состояние которого меняется со временем. Но государство не влияет на нашу составляющую. Например, что-то вроде этого:

 import React, { Component } from 'react' class Users extends Component { constructor(props) { super(props) this.state = { otherData: null, users: [{name: 'John Doe', highlighted: false}] } } async componentDidMount() { try { let response = await fetch('https://api.github.com') let data = await response.json() this.setState({otherData: data}) } catch(err) { throw err } } toggleUserHighlight(user) { this.setState(prevState => { users: prevState.users.map(u => { if (u.name === user.name) { u.highlighted = !u.highlighted } return u }) }) } render() { return <div> <h1>Users</h1> { this.state.users.map(user => { return <User name={user.name} highlighted={user.highlighted} userSelected={() => { this.toggleUserHighlight(user) }}/> }) } </div> } } 

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

Если бы вы отлаживали это приложение сейчас с помощью react-addons-perf я уверен, что вы нашли бы, что время потрачено впустую, отображая Users->User . о нет! Что делать?!

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

Вернуться к компоненту класса

 import React, { Component } from 'react' class User extends Component { shouldComponentUpdate(nextProps) { // Because we KNOW that only these props would change the output // of this component. return nextProps.name !== this.props.name || nextProps.highlighted !== this.props.highlighted } render() { const { name, highlighted, userSelected } = this.props console.log('Hey User is being rendered for', [name, highlighted]) return <div> <h3 style={{fontStyle: highlighted ? 'italic' : 'normal'}} onClick={event => { userSelected() }} >{name}</h3> </div> } } 

Обратите внимание на новое добавление метода shouldComponentUpdate . Это довольно некрасиво. Мы не только больше не можем использовать функцию, мы также должны вручную перечислить реквизиты, которые могут измениться. Это предполагает смелое предположение, что функция userSelected prop не меняется. Это маловероятно, но на что-то стоит обратить внимание.

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

А как насчет React.PureComponent?

В React 15.3 появился новый базовый класс для компонентов. Он называется PureComponent и у него есть встроенный метод shouldComponentUpdate который выполняет сравнение с «равным количеством элементов» для каждой пропы. Большой! Если мы используем это, мы можем выбросить наш собственный метод shouldComponentUpdate который должен был перечислить определенные реквизиты.

 import React, { PureComponent } from 'react' class User extends PureComponent { render() { const { name, highlighted, userSelected } = this.props console.log('Hey User is being rendered for', [name, highlighted]) return <div> <h3 style={{fontStyle: highlighted ? 'italic' : 'normal'}} onClick={event => { userSelected() }} >{name}</h3> </div> } } 

Попробуйте, и вы будете разочарованы. Он перерисовывается каждый раз. Почему?! Ответ в том, что функция userSelected воссоздается каждый раз в методе render . Это означает, что когда PureComponent основе PureComponent вызывает собственный shouldComponentUpdate() он возвращает значение true, поскольку функция всегда отличается, так как она создается каждый раз.

Как правило, решение этой проблемы заключается в связывании функции в конструкторе содержащего компонента. Прежде всего, если бы мы это сделали, это означает, что нам пришлось бы вводить имя метода 5 раз (тогда как раньше это было 1 раз):

  • this.userSelected = this.userSelected.bind(this) (в конструкторе)
  • userSelected() { (как само определение метода)
  • <User userSelected={this.userSelected} ... (при определении места рендеринга компонента User )

Другая проблема состоит в том, что, как вы можете видеть, при выполнении этого метода userSelected он опирается на замыкание. В частности, это зависит от user переменной области из итератора this.state.users.map() .

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

recompose на помощь!

Сначала, чтобы повторить, что мы хотим:

  1. Написание функциональных компонентов кажется приятнее, потому что они функции. Это сразу говорит читателю кода, что он не содержит никакого состояния. Их легко рассуждать с точки зрения модульного тестирования. И они чувствуют себя менее многословным и более чистым JavaScript (конечно, с JSX).
  2. Нам лень связывать все методы, которые передаются в дочерние компоненты. Конечно, если методы сложны, было бы неплохо реорганизовать их, а не создавать их на лету. Создание методов на лету означает, что мы можем написать их код прямо там, где они используются, и нам не нужно давать им имя и упоминать их 5 раз в 3 разных местах.
  3. Дочерние компоненты никогда не должны перерисовываться, если не изменены реквизиты для них. Это может иметь значение не только для крошечных быстрых приложений, но и для реальных приложений, когда у вас есть много-много всего этого, все это лишний рендеринг сжигает процессор, когда его можно избежать.

(На самом деле, в идеале мы хотим, чтобы компоненты отображались только один раз. Почему React не может решить эту проблему за нас? Тогда было бы на 90% меньше постов в блоге о том, «Как быстро реагировать».)

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

Наша первая попытка переписать его обратно в функциональный компонент, но с использованием recompose.pure выглядит следующим образом:

 import React from 'react' import { pure } from 'recompose' const User = pure(({ name, highlighted, userSelected }) => { console.log('Hey User is being rendered for', [name, highlighted]) return <div> <h3 style={{fontStyle: highlighted ? 'italic' : 'normal'}} onClick={event => { userSelected() }}>{name}</h3> </div> }) export default User 

Как вы могли заметить, если вы запустите это, компонент User прежнему перерисовываться, даже если реквизиты ( name и highlighted клавиши) не меняются.

Давайте возьмем это на одну ступеньку. Вместо использования recompose.pure мы будем использовать recompose.onlyUpdateForKeys который является версией recompose.pure , но вы указываете ключи prop, чтобы сосредоточиться на них явно:

 import React from 'react' import { onlyUpdateForKeys } from 'recompose' const User = onlyUpdateForKeys(['name', 'highlighted'])(({ name, highlighted, userSelected }) => { console.log('Hey User is being rendered for', [name, highlighted]) return <div> <h3 style={{fontStyle: highlighted ? 'italic' : 'normal'}} onClick={event => { userSelected() }}>{name}</h3> </div> }) export default User 

Когда вы запустите этот файл, вы заметите, что он обновляется только при изменении name реквизита или highlighted элемента. Если родительский компонент перерисовывается, компонент User — нет.

Ура! Мы нашли золото!

обсуждение

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

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

 const MyComp = (arg1, arg2) => { ... } 

… до …

 const MyComp = pure((arg1, arg2) => { ... }) 

В идеале, вместо того, чтобы показывать способы взлома, лучшим решением для всего этого был бы новый патч для React, который является огромным улучшением shallowEqual который способен «автоматически» расшифровать то, что передается и сравнивается функция и только потому, что она не равна, не означает, что она на самом деле отличается.

Прием! Существует альтернатива среднего уровня необходимости возиться с методами привязки в конструкторах и встроенными функциями, которые создаются заново каждый раз. И это Публичные Классовые Поля . Это функция stage-2 в Babel, поэтому вполне вероятно, что ваша установка поддерживает это. Например, здесь используется вилка, которая не только короче, но и означает, что нам не нужно вручную перечислять все нефункциональные реквизиты. Это решение должно отказаться от закрытия. Тем не менее, все же хорошо понимать и знать о recompose.onlyUpdateForKeys при необходимости.

Чтобы узнать больше о React, ознакомьтесь с нашим курсом React The ES6 Way .

Эта статья была рецензирована Джеком Франклином . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!