Оптимізація продуктивності в React

React. Просунуті керівництва. Частина П'ята
Продовження серії перекладів розділу "Просунуті керівництва" (Advanced Guides) офіційній документації бібліотеки React.js.
Оптимізація продуктивності в React
Внутрішньо, React використовує кілька просунутих технік, які зводять до мінімуму кількість дорогих операцій DOM, необхідних для оновлення інтерфейсу. Для більшості додатків, що використовують React, швидкодія одержуваного інтерфейсу досить без додаткових дій для оптимізації продуктивності. Тим не менш, є кілька способів, за допомогою яких ви можете прискорити ваш додаток React.

Використання остаточної (production) складання
Якщо ви тестуєте продуктивність чи відчуваєте проблеми з продуктивністю у вашому додатку React, переконайтеся, що ви тестуєте минифицированную остаточну (production) складання:
  • Для створення збірки додатку React, вам необхідно запустити
    npm run build
    і слідувати подальшим інструкціям.
  • Для однофайловых збірок ми пропонуємо підготовлені для остаточної зборки версії React, що мають розширення
    .min.js
    .
  • Для складальника Browserify, вам необхідно запустити збірку з параметром
    NODE_ENV=production
    .
  • Для складальника Webpack, вам необхідно додати наступні плагіни в конфігураційний файл остаточної зборки (production config):
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: JSON.stringify('production')
}
}),
new webpack.optimize.UglifyJsPlugin()

Збірка для розробки (development) включає в себе додаткові попередження, які допомагають розробляти програму, але вони уповільнюють роботу додатка проводячи додаткові дії.
Профілювання компонентів з допомогою Timeline в Chrome
Використовуючи інструменти продуктивності в підтримуваних браузерах, development режимі, ви можете визулизировать як компоненти монтуються, оновлюються і размонтируются. Наприклад:
Компоненти React в Timeline Chrome
Виконайте наступні дії Chrome:
  1. Завантажте вашу програму з параметром
    ?react_perf
    в рядку запиту (наприклад:
    http://localhost:3000/?react_perf
    ).
  2. Відкрийте вкладку Timeline Інструменти розробника Chrome і натисніть Record (запис).
  3. Виконайте дії, які ви хочете профілювати. Не записуйте дії більше ніж 20 секунд — інакше Chrome може зависнути.
  4. Зупиніть запис.
  5. Події React будуть згруповані під ярликом User Timing.
Зверніть увагу, що ці цифри є відносними, оскільки компоненти будуть відображатися швидше в режимі production. Зрештою, це повинно допомогти вам уявити в якому місці користувальницький інтерфейс отримує оновлення помилково, і наскільки глибоко і часто відбувається оновлення вашого користувацького інтерфейсу.
В даний час цю можливість підтримують тільки Chrome, Edge і IE, але, оскільки ми використовуємо стандарт User Timing API, ми очікуємо, що і інші браузери додадуть підтримку цієї можливості.
Уникайте зайвої перемальовування
React створює та підтримує в актуальному стані своє внутрішнє представлення відображуваного користувальницького інтерфейсу. Воно включає в себе елементи React повертаються вашими компонентами. Це дозволяє React уникати створення вже існуючих вузлів DOM і доступу до них понад необхідності, що може бути повільніше операції з JavaScript об'єктами. Інколи цю модель називають "віртуальним DOM", хоча Нативний React (React Native) працює також.
Коли властивості компонента (props) або стан змінюються, React порівнює нову повертається версію елемента з попередньою відображеної в DOM, і в разі якщо вони не еквівалентні — оновлює DOM.
У деяких випадках, ваш компонент можна прискорити шляхом перевизначення функції життєвого циклу
shouldComponentUpdate
, яка викликається перед початком процесу переотображения (ререндеринга). Визначення функції за замовчуванням повертає
true
, дозволяючи React здійснювати оновлення:
shouldComponentUpdate(nextProps, nextState) {
return true;
}

Якщо вам відомо, що в деяких випадках ваш компонент не потребує оновлення, ви можете повернути
false
з функції
shouldComponentUpdate
для пропуску процесу переотображения (ререндеринга), включаючи виклик методу
render()
в поточному компоненті і нижче по ієрархії.
shouldComponentUpdate в дії
На малюнку представлено дерево компонентів. Мітка
SCU
повертає результат повертається функцією
shouldComponentUpdate
, а мітка
vDOMEq
— еквівалентний елемент React попереднім поданням. Колір кіл відображає необхідність перемальовування компонента.
Дерево компонентів
В компоненті C2 функція
shouldComponentUpdate
повернула
false
, в наслідок чого все піддерево, починаючи від C2 і нижче, React не буде перемальовувати (викликати функцію render), і для цього навіть не довелося викликати функцію
shouldComponentUpdate
в компонентах C4 і C5.
Для C1 і C3 —
shouldComponentUpdate
повернула
true
, тому React довелося спуститися нижче і перевірити нащадків. Для C6
shouldComponentUpdate
повернула
true
, і оскільки зазначені елементи не экивалентны віртуального DOM — React довелося оновити DOM.
Ну і останній цікавий варіант — це C8. React довелося отрендерить компонент, але оскільки нове внутрішнє подання React елемента дорівнює попередньому, DOM оновлювати не було потрібно.
Зверніть увагу, що React довелося зробити зміни в DOM тільки для C6, які були неминучі. У випадку з C8 — нас виручило порівняння відрендерених елементів React, для дерева C2 і компонента C7 не довелося навіть порівнювати елементи —
shouldComponentUpdate
повернула
false
і метод
render
не викликався.
Приклади
Уявімо варіант, що наш компонент змінюється тільки при зміні властивості
props.color
або стану
state.count
. Ми можемо реалізувати перевірку цих випадків у функції
shouldComponentUpdate
:
class CounterButton extends React.Component {
constructor(props) {
super(props);
this.state = {count: 1};
}

shouldComponentUpdate(nextProps, nextState) {
if (this.props.color !== nextProps.color) {
return true;
}
if (this.state.count !== nextState.count) {
return true;
}
return false;
}

render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}

В цьому коді, функція
shouldComponentUpdate
перевіряє будь-які зміни у властивості
props.color
або стан
state.count
. Якщо їх значення не змінилися, компонент не оновлюється. Якщо ваш компонент більш складний, ви можете використовувати подібний шаблон, але виробляє "поверхневе порівняння" всіх властивостей (полів)
props
і станів
state
для визначення необхідності оновлення компонента. Цей шаблон використовується досить часто, тому в React є хелпер для реалізації даної логіки — просто наслідуйте свій компонент від
React.PureComponent
. Наступний код досягає тієї ж мети, що і попередній, але він дійсно простіше:
class CounterButton extends React.PureComponent {
constructor(props) {
super(props);
this.state = {count: 1};
}

render() {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
);
}
}

У більшості випадків ви можете використовувати
React.PureComponent
замість того, щоб писати власну функцію
shouldComponentUpdate
. Однак,
React.PureComponent
реалізує тільки поверхневе порівняння, і бувають випадки коли властивості (props) або стану можуть бути змінені таким чином, що поверхневого порівняння не досить — така ситуація може відбуватися з більш складними структурами даних. Наприклад, припустимо, що вам необхідно реалізувати компонент
ListOfWords
для відображення списку слів розділених комами, з батьківським компонентом
WordAdder
, який по натисненню на кнопку додає слово в списку. Наступний код не буде працювати коректно:
class ListOfWords extends React.PureComponent {
render() {
return <div>{this.props.words.join(',')}</div>;
}
}

class WordAdder extends React.Component {
constructor(props) {
super(props);
this.state = {
words: ['marklar']
};
this.handleClick = this.handleClick.bind(this);
}

handleClick() {
// Цей розділ написаний не вірно і призведе до некоректної роботи
const words = this.state.words;
words.push('marklar');
this.setState({words: words});
}

render() {
return (
<div>
<button onClick={this.handleClick} />
<ListOfWords words={this.state.words} />
</div>
);
}
}

Проблема полягає в тому, що
PureComponent
робить просте поверхневе порівняння між старими і новими значеннями масиву
this.props.words
. Просте порівняння повертає
true
якщо обидва порівнюваних значення посилаються на один і той же об'єкт (масив). Оскільки зазначений метод код
handleClick
компонента
WordAdder
змінює безпосередньо масив
words
, порівняння старого і нового значення
this.props.words
повертає еквівалентність, незважаючи на те, що самі слова в масиві змінилися. Компонент
ListOfWords
не буде оновлений, маючи при цьому нові слова, які потрібно відобразити.
Сила незмінних даних
найпростіший спосіб вирішення цієї проблеми — не зраджувати (не мутувати) значення, які ви використовуєте у властивостях (props) або стані. Наприклад, метод
handleClick
може бути переписаний з використанням методу
concat
повертає поверхневу копію масиву з приєднанням нових значень:
handleClick() {
this.setState(prevState => ({
words: prevState.words.concat(['marklar'])
}));
}

ES6 підтримує розвертає (spread) синтаксис для масивів, який може зробити код ще простіше. Якщо ви використовуєте Create React App для створення свого власного додатка, цей синтаксис доступний за замовчуванням.
handleClick() {
this.setState(prevState => ({
words: [...prevState.words, 'marklar'],
}));
};

Таким же чином ви можете переписати код для уникнення зміни (мутації) об'єктів. Наприклад, уявімо, що у нас є об'єкт
colormap
і ми хочемо написати функцію, яка змінює значення
colormap.right
і встановлює його рівним
'blue'
. Ми могли б помилково написати:
function updateColorMap(colormap) {
colormap.right = 'blue';
}

Для реалізації цього без мутації початкового об'єкта, ми можемо використовувати метод Object.assign:
function updateColorMap(colormap) {
return Object.assign({}, colormap, {right: 'blue'});
}

updateColorMap
тепер повертає новий об'єкт, а не змінює старий.
Object.assign
входить в ES6 і вимагає включення babel-polyfill.
У ES6 є можливість розгортання (spread) властивостей об'єкта і тому ми можемо реалізувати оновлення об'єкта без мутації ще простіше:
function updateColorMap(colormap) {
return {...colormap, right: 'blue'};
}

Якщо ви використовуєте Create React App для створення свого додатку,
Object.assign
і розвертає синтаксис для об'єктів доступні за замовчуванням.
Використання незмінних структур даних
Immutable.js — це ще один шлях для вирішення цієї проблеми. Ця бібліотека надає незмінні, постійні колекції, які працюють через структурний обмін:
  • Незмінна: одного разу створена, колекція не може бути змінена в інший момент часу.
  • Постійна: нова колекція може бути створена з попередньої колекції і зміненого набору даних. Первісна колекція залишається чинною після створення нової колекції.
  • Структурний обмін: нова колекція створюється з використанням максимально, наскільки це можливо, такої ж структури, як і в первісній колекції, знижуючи копіювання до мінімуму для поліпшення продуктивності.
Незмінюваність дозволяє зробити відстеження змін легким. Зміни завжди призводять до створення нового об'єкта і нам залишається тільки перевірити чи змінилася посилання на об'єкт. Наприклад, в нативному JavaScript коді:
const x = { foo: "bar" };
const y = x;
y.foo = "baz";
x === y; // true

Не дивлячись на те, що
y
було змінено,
y
посилається на той самий об'єкт, що і
x
— їх порівняння повертає
true
. Ми можемо переписати цей код з використанням immutable.js:
const SomeRecord = Immutable.Record({ foo: null });
const x = new SomeRecord({ foo: 'bar' });
const y = x.set('foo', 'baz');
x === y; // false

В даному випадку, оскільки при зміні
x
була повернута посилання на новий об'єкт, ми можемо сміливо припускати що
x
змінилася.
Ще дві бібліотеки, які можуть допомогти використовувати незмінні дані — це seamless-immutable і immutability-helper.
Незмінні структури даних надають вам найпростіший спосіб відстеження змін в об'єктах — а це те, що нам необхідно для використання
shouldComponentUpdate
, яка в більшості випадків дасть вам хороший приріст продуктивності.
Попередні частини:
Джерело: React — Advanced Guides — Optimizing Performance
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.