ТЕХНОЛОГИЯ EXPEDIA GROUP - ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ

(Маленький) футляр для функциональных компонентов React

Сравнение влияния стиля реализации компонента React на размер пакета

Может ли использование компонентов React на основе функций вместо классов уменьшить ваши пакеты? Давайте разберемся.

В Vrbo ™ (входит в Expedia Group ™) мы поддерживаем набор из около 80 общих компонентов React, которые используются различными внутренними и внешними приложениями. Недавно мы провели аудит этих общих компонентов React, чтобы увидеть, какие изменения мы можем внести для повышения производительности компонентов в приложениях, которые их используют. Одной из областей, которые мы определили, которая может быть небольшой быстрой победой, было преобразование компонентов на основе классов без сохранения состояния в функциональные компоненты. Мы надеялись, что это даст файлы меньшего размера для преобразованных компонентов. Мы знали, что это не будет значительным приростом производительности, но будет относительно безопасным изменением (если только какой-то потребитель не использует ссылку на компонент), которое может совокупно уменьшить размер пакета приложения.

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

Компоненты без сохранения состояния

Компоненты без сохранения состояния - это самые простые компоненты в экосистеме React. Вот сравнение двух способов определения тривиального компонента React, который отображает кнопку:

Функция

import React from 'react';
export default function Simple() {
    return <button>{'Click'}</button>;
}

Примечание. Существует несколько способов определения экспорта (функция стрелки, экспорт переменной и т. д.). Мы использовали указанный выше формат, потому что наша конфигурация транспилятора создавала для него самый маленький выходной файл.

Класс

import React, {Component} from 'react';
export default class Simple extends Component {
    render() {
        return <button>{'Click'}</button>;
    }
}

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

  • Версия на основе классов импортирует и расширяет Component из react.
  • Версия на основе классов использует метод render для возврата JSX.

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

Функция (переведено)

import React from 'react';
export default function Simple() {
    return React.createElement('button', null, 'Click');
}

Класс (транспарентный)

import _inheritsLoose from '@babel/runtime/helpers/inheritsLoose';
import React, { Component } from 'react';
var Simple =
/*#__PURE__*/
function (_Component) {
    _inheritsLoose(Simple, _Component);
    function Simple() {
        return _Component.apply(this, arguments) || this;
    }
    var _proto = Simple.prototype;
    _proto.render = function render() {
        return React.createElement('button', null, 'Click');
    };
    return Simple;
}(Component);
export { Simple as default };

Транспиляция увеличила различия между двумя реализациями. Использование синтаксического сахара class приводит к добавлению дополнительных шаблонов в вывод по сравнению с примером функции. Мы прогнали перенесенный код через терсер, чтобы минимизировать и удалить любые различия из-за форматирования. После минификации чистая разница между двумя реализациями составляет 180 байт. Так что не большая разница, но кое-что :-). Обратите внимание, что версия на основе class вызывает помощник inheritsLoose Babel, что приводит к дополнительным путям кода, которые будут использоваться во время выполнения.

Компоненты с состоянием

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

Функция с состоянием ловушки

import React, {useState} from 'react';
export default function Simple() {
    const [click, setClick] = useState(0);
    function handleClick() {
        setClick(click + 1);
    }
    return (
        <button onClick={handleClick}>
            {`Click ${click}`}
        </button>
    );
}

Состояние класса

import React, {Component} from 'react';
export default class Simple extends Component {
    state = {
        click: 0
    }
    handleClick = () => {
        this.setState({
            click: this.state.click + 1
        });
    }
    render() {
        return (
            <button onClick={this.handleClick}>
                {`Click ${this.state.click}`}
            </button>
        );
    }
}

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

  • Версия функции теперь импортирует и использует useState для создания и управления состоянием для подсчета кликов, в то время как версия на основе class - использует свойство класса для state и использует React setState API для изменения значения.
  • Версия на основе class - требует от разработчика правильного понимания this в JavaScript и понимания того, когда область должна быть привязана (т. Е. Синтаксис инициализатора свойства для handleClick) .

Теперь сравнение переданного вывода с использованием того же процесса сборки, что и раньше:

Функция с состоянием подключения (переданная)

import _slicedToArray from '@babel/runtime/helpers/slicedToArray';
import React, { useState } from ‘react’;
export default function Simple() {
    var _useState = useState(0),
        _useState2 = _slicedToArray(_useState, 2),
        click = _useState2[0],
        setClick = _useState2[1];

    function handleClick() {
        setClick(click + 1);
    }
    return React.createElement('button', {
        onClick: handleClick
    }, 'Click '+ click);
}

Состояние класса (передано)

import _inheritsLoose from '@babel/runtime/helpers/inheritsLoose';
import React, { Component } from 'react';
var Simple =
/*#__PURE__*/
function (_Component) {
    _inheritsLoose(Simple, _Component);
    function Simple() {
        var _this;
        for (var _len = arguments.length, args = new Array(_len), 
             _key = 0; _key < _len; _key++) {
            args[_key] = arguments[_key];
        }
        _this = _Component.call.apply(_Component,   
                    [this].concat(args)) || this;
        _this.state = {
            click: 0
        };
        _this.handleClick = function () {
            _this.setState({click: _this.state.click + 1});
        };
        return _this;
    }
    var _proto = Simple.prototype;
    _proto.render = function render() {
        return React.createElement('button', {
            onClick: this.handleClick
        }, 'Click '+ this.state.click);
    };
    return Simple;
}(Component);
export { Simple as default };

Транспортировка функционального компонента с включенной обработкой данных вводит использование вспомогательного метода Babel, slicedToArray,, а затем использует его для деструктуризации массива. В противном случае код остается нетронутым. Для компонента на основе классов вводится код для управления областью this. Аргументы также копируются неглубоко. В целом, для включения поддерживаемой конфигурации браузера существует больше модификаций кода на основе классов, но размер файла существенно не увеличился. Разница между двумя минимизированными транспиляциями немного увеличилась до 259 байт (180 ранее).

Компоненты, оптимизированные для рендеринга

Ненужные отрисовки - обычная проблема производительности React. Очень легко создавать компоненты, которые визуализируются без изменений. Это может серьезно снизить производительность приложения. К счастью, команда React всегда ищет способы оптимизировать производительность приложений. Они предоставили несколько API-интерфейсов для снижения риска повторного рендеринга. Такие API, как memo, shouldComponentUpdate и PureComponent, были добавлены, чтобы помочь разработчику ограничить время отрисовки компонента.

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

Функция со статусом обработчика и памяткой

import React, {useState} from 'react';
function Simple({disabled = false, onClick}) {
    const [click, setClick] = useState(0);

    function handleClick() {
        const newClick = click + 1;
        setClick(newClick);
        onClick(newClick);
    }
    return (
        <button disabled={disabled} onClick={handleClick}>
            {`Click ${click}`}
        </button>
    );
}
export default React.memo(Simple);

Состояние класса с PureComponent

import React, {PureComponent} from 'react';
export default class Simple extends PureComponent {
    state = {
        click: 0
    }
    handleClick = () => {
        const newClick = this.state.click + 1;

        this.setState({
            click: newClick
        });
        this.props.onClick(newClick);
    }
    render() {
        return (
            <button disabled={this.props.disabled}
                    onClick {this.handleClick}>
                {`Click ${this.state.click}`}
            </button>
        );
    }
}

В приведенных выше примерах мы добавили несколько новых свойств (disabled, onClick), чтобы смоделировать вариант использования, когда компонент будет без необходимости повторно визуализироваться, если свойства не проверены, чтобы определить, действительно ли они изменились. Для реализации сравнения свойств был добавлен React.memo для обертывания компонента на основе функций, а компонент на основе класса был изменен для расширения PureComponent.

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

Функция со статусом перехвата и памяткой (передано)

import _slicedToArray from '@babel/runtime/helpers/slicedToArray';
import React, { useState } from 'react';
function Simple(_ref) {
    var onClick = _ref.onClick,
        _ref$disabled = _ref.disabled,
        disabled = _ref$disabled === void 0 ? false : _ref$disabled;
    var _useState = useState(0),
        _useState2 = _slicedToArray(_useState, 2),
        click = _useState2[0],
        setClick = _useState2[1];
    function handleClick() {
        var newClick = click + 1;
        setClick(newClick);
        onClick(newClick);
    }
    return React.createElement('button', {
        onClick: handleClick,
        disabled: disabled
    }, 'Click '+ click);
}
export default React.memo(Simple);

Состояние класса с PureComponent (передано)

import _inheritsLoose from '@babel/runtime/helpers/inheritsLoose';
import React, { PureComponent } from 'react';
var Simple =
/*#__PURE__*/
function (_PureComponent) {
    _inheritsLoose(Simple, _PureComponent);
    function Simple() {
        var _this;
        for (var _len = arguments.length, args = new Array(_len), 
                 _key = 0; _key < _len; _key++) {
            args[_key] = arguments[_key];
        }
        _this = _PureComponent.call.apply(_PureComponent,
                    [this].concat(args)) || this;
        _this.state = {
            click: 0
        };
        _this.handleClick = function () {
            var newClick = _this.state.click + 1;
            _this.setState({
                click: newClick
            });
            _this.props.onClick(newClick);
        };
        return _this;
    }
    var _proto = Simple.prototype;
    _proto.render = function render() {
        return React.createElement('button', {
            disabled: this.props.disabled,
            onClick: this.handleClick
        }, 'Click '+ this.state.click);
    };
    return Simple;
}(PureComponent);
export { Simple as default };

Влияние транспиляции на React.memo и PureComponent добавок действительно минимально, как показано красным выше. В случае с функцией произошла некоторая транспиляция для обработки деструктурирования свойства. В примере на основе class он просто заменил использование Component на PureComponent. В целом, эти изменения немного сократили разницу между двумя параметрами, поскольку минимизированная разница теперь составляет 245 байт (259 байтов ранее).

Я чему-нибудь научился?

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

В этом элементарном упражнении переключение компонента на основе class - без сохранения состояния на функциональный компонент сэкономило нам как минимум 180 байтов минимизированных для каждого компонента в зависимости от поддержки нашего браузера. Конечно, ваш опыт может варьироваться в зависимости от конфигурации вашей транспиляции / минимизации и сложности компонентов, но шаблон для транспиляции классов - это то, что есть. Хотя 180 байт - это небольшой объем, когда мы заменили наши общие компоненты без сохранения состояния на функциональные компоненты, мы достигли общего уменьшения на ~ 5% размера минифицированного файла. Мы можем еще больше уменьшить размер файла, если дополнительно изменим компоненты на основе class с состоянием, чтобы использовать новую функцию перехватчиков. Однако мы еще не готовы к принудительному применению React 16.8.0 для всех наших потребителей.

Переключение сэкономило нам как минимум 180 байт на компонент

Помимо преимущества в размере файла, с введением хуков React рекомендует писать новые компоненты в виде функциональных компонентов, а не компонентов на основе классов. Хотя они заявили, что не планируют отказываться от поддержки классов, поскольку они рекомендуют новым компонентам использовать перехватчики, почему бы не начать переключаться сейчас и получить в процессе несколько байтов?