Поиск по сайту:

5 советов по повышению производительности ваших приложений React


Ваше приложение React чувствует себя немного вялым? Вы боитесь включать «вспышку рисования» в Chrome DevTools из-за того, что вы можете увидеть? Попробуйте эти 5 советов по повышению производительности!

Эта статья содержит 5 советов по производительности для разработки React. Вы можете использовать это оглавление для быстрой навигации по этой статье.

Перейти к советам

  • Используйте memo и PureComponent
  • Избегайте анонимных функций
  • Избегайте литералов объектов
  • Используйте React.lazy и React.Suspense
  • Избегайте частого подключения/отключения

Используйте памятку и PureComponent

Рассмотрим это упрощенное приложение React ниже. Считаете ли вы, что будет перерисовываться, если только props.propA изменит значение?

import React from 'react';

const MyApp = (props) => {
  return (
    <div>
      <ComponentA propA={props.propA}/>
      <ComponentB propB={props.propB}/>
    </div>
  );
};

const ComponentA = (props) => {
  return <div>{props.propA}</div>
};

const ComponentB = (props) => {
  return <div>{props.propB}</div>
};

Ответ ДА! Это связано с тем, что MyApp на самом деле переоценивается (или перерисовывается 😏), а находится там. Таким образом, хотя его собственные реквизиты не изменились, его родительский компонент вызывает его повторный рендеринг.

Эта концепция также применяется к методам render для компонентов React на основе классов.

Авторы React поняли, что это не всегда будет желаемым результатом, и можно будет легко повысить производительность, просто сравнив старые и новые реквизиты перед повторным рендерингом… по сути, это то, чем является React.PureComponent. предназначен для того, чтобы делать!

Давайте используем memo с функциональными компонентами (позже мы рассмотрим компоненты на основе классов):

import React, { memo } from 'react';

// 🙅♀️
const ComponentB = (props) => {
  return <div>{props.propB}</div>
};

// 🙆♂️
const ComponentB = memo((props) => {
  return <div>{props.propB}</div>
});

Вот и все! Вам просто нужно обернуть функцией memo(). Теперь он будет перерисовываться только тогда, когда propB действительно изменяет значение, независимо от того, сколько раз его родитель перерисовывался!

Давайте посмотрим на PureComponent. По сути, это эквивалентно memo, но для компонентов на основе классов.

import React, { Component, PureComponent } from 'react';

// 🙅♀️
class ComponentB extends Component {
  render() {
    return <div>{this.props.propB}</div>
  }
}

// 🙆♂️
class ComponentB extends PureComponent {
  render() {
    return <div>{this.props.propB}</div>
  }
}

Этот прирост производительности почти слишком прост! Вы можете задаться вопросом, почему компоненты React автоматически не включают эти внутренние средства защиты от чрезмерного повторного рендеринга.

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

Серебряные пули в мире программирования редки! И memo/PureComponent не являются исключением. Вам обязательно захочется протестировать их взвешенно и вдумчиво. В некоторых случаях они могут удивить вас, насколько они могут сэкономить вычислительные ресурсы.

Для React Hooks используйте useMemo как аналогичный способ предотвращения ненужной вычислительной работы.

Избегайте анонимных функций

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

import React from 'react';

function Foo() {
  return (
    <button onClick={() => console.log('boop')}> // 🙅♀️
      BOOP
    </button>
  );
}

Поскольку анонимным функциям не присваивается идентификатор (через const/let/var), они не сохраняются всякий раз, когда этот функциональный компонент неизбежно отрисовывается снова. Это заставляет JavaScript выделять новую память каждый раз при повторном рендеринге этого компонента вместо выделения одного фрагмента памяти только один раз при использовании «именованных функций»:

import React, { useCallback } from 'react';

// Variation 1: naming and placing handler outside the component 
const handleClick = () => console.log('boop');
function Foo() {
  return (
    <button onClick={handleClick}>  // 🙆♂️
      BOOP
    </button>
  );
}

// Variation 2: "useCallback"
function Foo() {
  const handleClick = useCallback(() => console.log('boop'), []);
  return (
    <button onClick={handleClick}>  // 🙆♂️
      BOOP
    </button>
  );
}

useCallback — еще один способ избежать ловушек анонимных функций, но он имеет те же компромиссы, что и React.memo, которые мы рассмотрели ранее.

С компонентами на основе классов решение довольно простое и не имеет недостатков. Рекомендуемый способ определения обработчиков в React:

import React from 'react';

class Foo extends Component {
  render() {
    return (
      <button onClick={() => console.log('boop')}>  {/* 🙅♀️ */}
        BOOP
      </button>
    );
  }
}

class Foo extends Component {
  render() {
    return (
      <button onClick={this.handleClick}>  {/* 🙆♂️ */}
        BOOP
      </button>
    );
  }
  handleClick = () => {  // this anonymous function is fine used like this
    console.log('boop');
  }
}

Избегайте литералов объектов

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

function ComponentA() {
  return (
    <div>
      HELLO WORLD
      <ComponentB style={{  {/* 🙅♀️ */}
        color: 'blue',     
        background: 'gold'
      }}/>
    </div>
  );
}

function ComponentB(props) {
  return (
    <div style={this.props.style}>
      TOP OF THE MORNING TO YA
    </div>
  )
}

Каждый раз, когда перерисовывается, в памяти должен \создаваться новый литерал объекта. Кроме того, это также означает, что фактически получает другой объект style. Использование memo и PureComponent здесь даже не предотвратит повторный рендеринг 😭

Этот совет относится не только к пропсам style, но обычно литералы объектов непреднамеренно используются в компонентах React.

Это можно легко исправить, назвав объект (конечно, вне тела компонента!):

const myStyle = {  // 🙆♂️
  color: 'blue',     
  background: 'gold'
};
function ComponentA() {
  return (
    <div>
      HELLO WORLD
      <ComponentB style={myStyle}/>
    </div>
  );
}

function ComponentB(props) {
  return (
    <div style={this.props.style}>
      TOP OF THE MORNING TO YA
    </div>
  )
}

Используйте React.lazy и React.Suspense

Частично сделать ваше приложение React быстрым можно с помощью разделения кода. Эта функция была представлена в React v16 с React.lazy и React.Suspense.

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

- bundle.js (10MB!)

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

- bundle-1.js (5MB)
- bundle-2.js (3MB)
- bundle-3.js (2MB)

Первоначальный сетевой запрос должен будет загрузить всего 5 МБ, и он может начать показывать что-то интересное конечному пользователю. Представьте себе веб-сайт блога, которому сначала нужны только верхний и нижний колонтитулы. Как только он загрузится, он начнет запрашивать 2-й пакет, который содержит настоящие сообщения в блоге. Это просто рудиментарный пример, где разделение кода было бы удобно. 👏👏👏

Как это делается в React?

Если вы используете вот так.

Вот простой пример реализации lazy и Suspense:

import React, { lazy, Suspense } from 'react';
import Header from './Header';
import Footer from './Footer';
const BlogPosts = React.lazy(() => import('./BlogPosts'));

function MyBlog() {
  return (
    <div>
      <Header>
      <Suspense fallback={<div>Loading...</div>}>
        <BlogPosts />
      </Suspense>
      <Footer>
    </div>
  );
}

Обратите внимание на реквизит fallback. Это будет показано пользователю сразу же, пока загружается второй фрагмент пакета (например, ).

Прочтите эту замечательную статью о разделении кода с помощью React Suspense 🐊

Избегайте частого подключения/размонтирования

Много раз мы привыкли заставлять компоненты исчезать с помощью тернарного оператора (или чего-то подобного):

import React, { Component } from 'react';
import DropdownItems from './DropdownItems';

class Dropdown extends Component {
  state = {
    isOpen: false
  }
  render() {
    <a onClick={this.toggleDropdown}>
      Our Products
      {
        this.state.isOpen
          ? <DropdownItems>
          : null
      }
    </a>
  }
  toggleDropdown = () => {
    this.setState({isOpen: !this.state.isOpen})
  }
}

Поскольку удален из DOM, это может привести к перерисовке/перекомпоновке в браузере. Это может быть дорого, особенно если это приводит к смещению других элементов HTML.

Чтобы смягчить это, рекомендуется избегать полного размонтирования компонентов. Вместо этого вы можете использовать определенные стратегии, такие как установка CSS opacity на ноль или настройка CSS visibility на \none. он эффективно исчезает без каких-либо затрат на производительность.