Инъекция React JS в приложение на Angular JS или борьба за производительность

    Дорогие Хабролюбители, всем привет! Не откроем Америку если скажем, что существуют сотни плагинов и библиотек, которые облегчают специализированные задачи, связанные с построением современных web интерфейсов. Angular один из них, про его производительность писалось много и в большинстве случаев даются рекомендации чего не нужно делать, чтобы все было хорошо.

    Основной аргумент сторонников — медленно работают неправильные приложения, а вот правильные не должны содержать более 2000-3000 элементов. Если содержит больше значит что-то не так. См. например http://iantonov.me/page/angularjs-osnovy.

    Аргумент в общем вполне здравый, но всегда есть ситуации, когда нужно написать «неправильное» web приложение потому, что такие требования. В этой статье мы решили рассказать как раз про такую задачу, и как мы ее решили. На наш взгляд статья будет полезна в большей степени профессиональным веб разработчикам. Итак, наша задача была сделать календарь для системы бронирования для одного спортивного клуба. Календарь отображает семь 12–часовых блоков, каждый день из которых разделен на 15 минутные интервалы. В блоке может быть от 2-х до 10 DOM элементов. Вроде ничего не предвещало беды, верхняя граница ~3000.




    Ну что-же начинаем писать:
    <div ng-repeat=”day in days”>	
    	….
    	<div ng-repeat=”hour in day.hours”>
    		<div ng-repeat=”block in hour.blocks”>
    		…
    			<div ng-repeat=”block in hour.blocks”>
    			…
    				<div ng-repeat=”session in block.sessions”>
    				…
    				</div>
    			</div>
    		</div>
    	</div>
    </div>
    


    Готово! Но! Data-binding всего этого занимал примерно 2-3 секунды. Кстати, тут может встать вопрос, как вообще измерить, сколько он занимает? Попытка сделать это с помощью профайлера напрямую не дает ответа, в профайлере трудно понять, что относится именно к data-binding листа, а что добавляет routing и прочее.

    Поэтому мы сделали довольно просто:
    $timeout(function() {
    	$scope.days = days;
        var now =  new Date();
        $timeout(function() { 
        console.log(new Date - now);
        $scope.apply()
      }, 0);
    }, 100);
    


    Первый timeout нужен, чтобы отработали все другие скрипты, чтобы не нарушать точность измерения, собственно число 100(ms) подбирается экспериментально. Второй можно смело делать 0, т.к. как только браузер получит первую передышку, он сразу выполнит внутренний handler $timeout-a, что собственно и будет обозначать, что data-binding завершен.

    С точки зрения отзывчивости пользовательского интерфейса 2 – 3 секунды довольно большие значения, как же решать эту проблему?

    Первым делом, конечно, еще и еще раз перечитать tech.small-improvements.com/2013/09/10/angularjs-performance-with-large-lists и, конечно же, Optimizing AngularJS: 1200ms to 35ms. В последней статье результат, кстати, выглядит особенно впечатляющее. Но результат достигается виртуализацией (т.е. тем что отрисовывается только маленькая отображаемая часть) и кэшированием. Виртуализация – хорошее решение, но только когда виртуализуемый контент достаточно несложен. В противном случае проявляются очень неприятные тормоза при скроллинге страницы, которые нивелируют весь эффект виртуализации.

    Кстати, говоря о виртуализации, для отображения табличных данных есть замечательный элемент ngGrid, который может отображать миллионы (проверьте :)) элементов. Но разгадка здесь, конечно, опять – виртуализация (а иначе почему все строки в ngGrid одинаковой высоты).

    Итак, перепробовав множество вариантов, в конце концов приходим к ReactJS. По заявлениям разработчиков, это наиболее быстрый framework для data-binding (и теперь я склонен согласиться что это так). Хотя, например, на сайте Vue утверждается иное, мой собственный тест показал превосходство React.

    Итак react.

    На Хабре уже довольно много статей посвящённых этому framework-у. Для начинающих я бы посоветовал:



    а также замечательную статью


    На Хабре есть ее перевод на русский язык.

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

    Код для отображения представления на ReactJS выглядит следующим образом:
    var Hello = React.createClass({
        render: function() {
            return React.DOM.div({}, 'Hello ' + this.props.name);
        }
    }); 
    React.renderComponent(Hello({name: 'World'}), document.body);
    


    Здесь, первая часть определяет компонент, а строчка “React.renderComponent(Hello({name: 'World'}), document.body)”, отрисовывает его.
    API React.DOM достаточно удобен, однако после шаблонов Angualr и Knockout, генерация DOM с помощью Javascript выглядит немного архаично и может показаться слишком трудоемким занятием. Для решения этого Facebook предлагает “In-browser JSX Transform”.

    Встречая в браузере
    <script type="text/jsx">


    трансформер превратит вот такой код
    var HelloMessage = React.createClass({
      render: function() {
        return <div>{'Hello ' + this.props.name}</div>;
      }
    });
    


    в такой:
    var Hello = React.createClass({
        render: function() {
            return React.DOM.div({}, 'Hello ' + this.props.name);
        }
    }); 
    


    Обратите внимание на синтаксис: return [Разметка]; без кавычек (хотя по опыту с ReactJS.Net лишние скобки или переносы строк не помешают). Для того, чтобы отобразить повторяющиеся элементы, по какому-нибудь массиву нужно сделать вот так
    var HelloMessage = React.createClass({
      render: function() {
        … 
        var nodes = array.map(function(element) {
          return <div>…</div>;
        });
        
        return <div>{nodes}</div>;
      }
    });
    


    Хотя, больше вероятность, что Вы сделаете вот так:
    var SubMessage = React.createClass({
      render: function() {
        return <div>…</div>;
      }
    });
    
    var HelloMessage = React.createClass({
      render: function() {
        … 
        var nodes = array.map(function(element) {
          return <SubMessage >…</ SubMessage >;
        });
        
        return <div>{nodes}</div>;
      }
    });
    


    Почему? Ответ заключается в том, чтобы компоненты имели свое состояние, про которое написано ниже.

    Кстати, работу по преобразованию JSX, вполне можно получить и серверу, для этого есть богатый набор интеграций https://github.com/facebook/react/wiki/Complementary-Tools#jsx-integrations, что собственно мы и сделали, в нашем случае это был ReactJS.Net.

    А что же, two-way binding – краеугольный камень MV-* framework-ов? Собственно, как метко замечено в http://maketea.co.uk/2014/03/05/building-robust-web-apps-with-react-part-1.html, ReactJS таковым не является, однако кое-какие средства, для решения подобных вопросов есть. В ReactJS различают свойства и состояние компонента. Свойства — это то что мы передаем компоненту вызывая React.renderComponent(Hello({[СВОЙСТВА]}), …) и получаем уже внутри компонента через this.props. В большинстве случаев, свойства – это некоторое дерево объектов, по которому нужно сгенерировать разметку. Идеологически, свойства предполагают довольно статичное поведение. Для того, чтобы разметка изменилась при обновлении свойств, придется еще раз вызывать React.renderComponent (хотя от себя замечу, что этот renderComponent зачастую работает быстрее чем аналогичные конструкции MV-* фрэймворков, всё-таки алгоритм различий очень неплох).

    Состояние (state) — это то, что может меняться. Состояние определяется парой методов:
    • getInitialState – определяет начальное состояние компонента (которое будет возвращаться как раз на основе свойств).
    • setState – устанавливает состояние.


    При вызове setState перерисовка случится только для того компонента, для которого он был вызван.

    Как же передать данные в обратную сторону? Для этого нужен объект valueLink, который представляет собой начальное значение и обработчик его изменения. Этот обработчик, должен вести себя по ситуации. Facebook не рекомендует записывать новоe значение непосредственно в props поэтому, придется записать новое значение в state, чтобы разметка обновилась адекватно данным. То есть в наиболее полном виде подобие two-way binding в ReactJS будет выглядеть так:

    Где в Angular было
    <input ng-model=value />. 
    


    В ReactJS будет:
    var HelloMessage = React.createClass({
      getInitialState() {
        return { value: this.props.value };
      },	
      render: function() {
        var valueLink = {
        value: this.state.value,
          requestChange: function(newValue) {
            this.setState({value: newValue});
          }.bind() // Обратите внимание, на вызов .bind()
        };
        return <input value={valueLink}/>;
      }
    });
    


    Многословно? Конечно!!! Это цена за пятикратно выросшую производительность.

    Итак, возвращаясь к наше задаче, что мы имеем:
    1. Приложение на AngularJS в котором некий кусочек работает медленно.
    2. Мы хотим увеличить производительность (собственно в 5 раз нам будет приемлемо).
    3. Мы хотим добиться этого минимальными трудозатратами
    4. В идеале мы хотим изменить только лишь View часть приложения, оставить и Контроллер и Модель без изменения.


    Идея описана в http://habrahabr.ru/post/215607/.

    Собственно, наш:
    <div ng-repeat=”day in days”>	
    	….
    </div>
    


    превращается теперь в:
     <div calendar=”days” />
    

    Но это еще не React, это еще Angular, а calendar – это директива, которая отслеживает изменения в Days и вызывает renderComponent.

    Хорошая новость в том, что переписывание кода View на JSX – процесс почти механический. Ng-class, превращается в className={className}, где className –только что вычисленные имена классов (обратите, кстати, внимание что именно className а не class, и если вы забудете это, React услужливо подскажет вам в консоли). Ng-show в style={style}. Ng-Model в см. выше. Он настолько механический, что есть даже начинание ngReact, которое делает это автоматически.

    Однако, как написано здесь, можно получить и минус к производительности. Во избежание мы пошли проверенным “ручным” путем.

    В итоге:
    То, что раньше занимало 2-3 секунды, стало занимать 300-500 ms. Это сделало user experience вполне приятным. По времени переделка заняла около 3-х дней изначальная реализация ~15 дней. Удалось обойтись изменением только view, хотя, возможно, это специфика именно этой задачи.

    В качестве заключения хочется сказать, что использование React-инъекций (с вашего позволения назовем их именно так) в приложениях, написанные на Angular дают возможность увеличить производительность приблизительно в 5 раз. А самое главное — вносимые изменения хоть и носят ручной и механический характер, но применяются локально.

    С нашей точки зрения, в похожих ситуациях React может стать волшебной палочкой, которая ускорит “медленные” места приложения.
    • +32
    • 17,8k
    • 3
    True Engineering
    59,49
    Лаборатория технологических инноваций
    Поделиться публикацией

    Комментарии 3

      +2
      Дизайн календарей меняется не так уж и часто. Примерно раз в несколько тысяч лет. Почему нельзя было просто захардкодить дни и выводить всё в одном ngRepeat'e из 15-минутных отрезков. Данные для него можно было заранее фильтровать, например, в контроллере. Кажется, слишком тяжелое решение для вывода простенького календаря.
        0
        Последний кусок кода можно сократить в два раза, если использовать аддон LinkedStateMixin
          0
          Абсолютно верно, мы не затронули в статье mixins, но это очень полезная вещь.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое