- PVSM.RU - https://www.pvsm.ru -

React, Drag&Drop и performance

React, Drag&Drop и performance - 1

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

В одном из последних проектов нам предстояло реализовать систему для управления учебным процессом образовательного учреждения. То, что у нас получилось в итоге, можно посмотреть здесь — https://habrahabr.ru/company/macte/blog/341726/ [1]

К интерфейсу редактирования расписания были предъявлены следующие требования:

  1. возможность создания, редактирования и удаление занятий;
  2. в рамках одной пары занятие может проводиться сразу у двух групп;
  3. возможность переноса занятия в сетке расписания.

С первыми двумя пунктами никаких проблем не возникло, а вот с третьим пришлось повозиться. На нем и остановимся поподробнее.

React и Drag&Drop

Для начала нам необходимо выбрать Drag&Drop библиотеку. На просторе интернета их великое множество: DraggableJS [2], dragula [3], interactjs.io [4] и пр.  А библиотек, заточенных для использования вместе с React, всего две: React-DnD [5] и react-beautiful-dnd [6].

Библиотека react-beautiful-dnd [6] отлично выглядит на демках, но, к сожалению, вышла уже после реализации проекта. Поэтому мы использовали React-DnD [5].

Про react-beautiful-dnd Alex Reardon написал статью —  «Rethinking drag and drop», которую можно почитать в переводе на хабре — https://habrahabr.ru/company/edison/blog/339086/ [7]

React DnD

Данная библиотека предоставляет нам набор из компонентов высшего порядка (HOC). Если говорить простым языком то:

  • DragSource — делает компонент перетаскиваемым;
  • DropTarget — добавляет компоненту возможность взаимодействовать с перетаскиваемыми компонентами;
  • DragLayer — позволяет реализовать собственное превью для перетаскиваемого элемента;
  • DragDropContext — предназначен для инициализации библиотеки.

Еще одна важная составляющая без которой React DnD не заработает — это drag&drop backend. Библиотека для обеспечения кроссбраузерности, абстракция над стандартным браузерным API.

Авторы React DnD советуют использовать HTML5-Backend [8], хотя совсем и не обязательно. Можно выбрать любой другой или написать свой.

Реализация

Для начала разобьем верстку сетки расписания на четыре основных компонента:

  1. Сетка расписания — ScheduleGrid (в этом компоненте мы будем инициализировать библиотеку React-DnD)
  2. Блок с расписанием на день — ScheduleColumn
  3. Секция с парами — SubjectSeciton (в нашем случае это будет DropTarget)
  4. Отдельное занятие — SubjectItem (в нашем случае это будет DragSource)

React, Drag&Drop и performance - 2

визуально разметили наши компоненты

Компонент App

Реализуем базовый компонент-контейнер App, который будет хранить информацию о недельном расписании и рендерить описанные выше компоненты.

Листинг компонента App

import React, { Component } from 'react';
import ScrollArea from 'react-scrollbar';

import ScheduleGrid from './ScheduleGrid';

import subjectsArray from './schedule-data';

class App extends Component {

  state = {
    subjectsArray: []
  }

  moveSubject = (movedSubjectId, newPosition) => {
    this.setState({
      subjectsArray: this.state.subjectsArray.map(subject => {
        if (subject.ID == movedSubjectId) {
          return {
            ...subject,
            DAY_OF_WEEK: newPosition.day,
            PERIOD: newPosition.period,
          }
        }
        
        return subject;
      }),
    });
  }

  render() {
    return (
      <div className="app-container">
        <div className="app-container-header">
          <h3>Редактирование расписания</h3>
          <h4>Версия React: {React.version}</h4>
        </div>
        <div className="posit-unit--middle">
          <ScrollArea
            speed={0.8}
            className="area"
            smoothScrolling={true}
            contentClassName="schedule-grid"
            horizontal={true}
            vertical={false}
          >
            <ScheduleGrid
              subjectsArray={this.state.subjectsArray}
              columns={6}
              itemsInColumn={6}
              moveSubject={this.moveSubject}
            />
          </ScrollArea>
        </div>
      </div>
    );
  }
}

export default App;

Формат ответа сервера

[
  {
    "ID": "2833",
    "NAME": "КР-101 (1 пара)",
    "DAY_OF_WEEK": 5,
    "GROUP": "834",
    "NOTICE": null,
    "SHEDULE_TYPE_ID": "1956",
    "SHEDULE_TYPE_NAME": "Практика",
    "SHEDULE_TYPE_CODE": "practice",
    "SUBJECT_ID": "868",
    "SUBJECT_NAME": "информатика",
    "CLASSROOM_ID": "883",
    "CLASSROOM_NAME": "55а-ПМК",
    "EDUCATION": "1",
    "PERIOD": "1",
    "TEACHER_ID": "1732",
    "TEACHER_FIRST_NAME": "Диана",
    "TEACHER_MIDDLE_NAME": "Юрьевна",
    "TEACHER_LAST_NAME": "Матвеева",
    "TEACHER_SHORT_NAME": "Д. Ю. Матвеева"
  },
  {
    "ID": "2832",
    "NAME": "КР-101 (1 пара)",
    "DAY_OF_WEEK": 5,
    "GROUP": "834",
    "NOTICE": null,
    "SHEDULE_TYPE_ID": "1957",
    "SHEDULE_TYPE_NAME": "Занятие",
    "SHEDULE_TYPE_CODE": "lesson",
    "SUBJECT_ID": "1491",
    "SUBJECT_NAME": "информационные сервисы",
    "CLASSROOM_ID": "883",
    "CLASSROOM_NAME": "55а-ПМК",
    "EDUCATION": "1",
    "PERIOD": "1",
    "TEACHER_ID": "1732",
    "TEACHER_FIRST_NAME": "Диана",
    "TEACHER_MIDDLE_NAME": "Юрьевна",
    "TEACHER_LAST_NAME": "Матвеева",
    "TEACHER_SHORT_NAME": "Д. Ю. Матвеева"
  }
]

Для построения сетки занятий нас интересуют поля:

  • PERIOD — номер занятий
  • DAY_OF_WEEK — день недели
  • TEACHER_SHORT_NAME — ФИО преподавателя
  • CLASSROOM_NAME — номер аудитории
  • SUBJECT_NAME — название предмета

ScheduleGrid

Далее приступим к реализации сетки занятий, генерируем колонки  ScheduleColumn.
В этом компоненте инициализируем React DnD. Для этого оборачиваем наш компонент в DragDropContext и передаем ему HTML5-backend.

Листинг ScheduleGrid

import React, { Component } from 'react';
import propTypes from 'prop-types';

import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';

import ScheduleColumn from './Grid/ScheduleColumn';
import ScrollButton from './Grid/ScrollButton';

import throttle from './utils/throttle.js';

class ScheduleGrid extends Component {

  static contextTypes = {
    scrollArea: propTypes.object,
  };

  constructor(props, context) {
    super(props);
    this.weekDays = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб'];
    this.scrollLeft = throttle(context.scrollArea.scrollLeft, 1500);
    this.scrollRight = throttle(context.scrollArea.scrollRight, 1500);
  } 
  
  printColumns = () => {
    return Array.from({ length: this.props.columns }, (el, index) => (
      <ScheduleColumn 
        itemsInColumn={this.props.itemsInColumn}
        moveSubject={this.props.moveSubject}
        subjectsArray={this.props.subjectsArray}
        xPos={index + 1}
        key={index}
        weekDay={this.weekDays[index]} />
    ));
  }

  render() {
    return (
      <div>
        <div className="table-schedule table-schedule--days clearfix">
          {this.printColumns()}          
        </div>
        <div className="navigation-table-day">
          <ScrollButton type={'prev'} handleClick={this.scrollLeft} />
          <ScrollButton type={'next'} handleClick={this.scrollRight} />
        </div>         
      </div>
    );
  }
}

export default DragDropContext(HTML5Backend)(ScheduleGrid);

ScheduleColumn

Каждая из колонок состоит из нескольких секций. Секции имеет координаты xPos и yPos, которые соответствуют полям PERIOD и DAY_OF_WEEK из API.

Листинг ScheduleColumn

import React, { Component } from 'react';

import SubjectSection from './SubjectSection';

class ScheduleColumn extends Component {

  getSectionData = (x, y) => {
    return this.props.subjectsArray.filter(subject => {
      return subject.DAY_OF_WEEK == x && subject.PERIOD == y;
    });
  }

  generateSection = yPos => {    
    const { xPos, emptyColumnItemClick, onColumnItemClick } = this.props;
    const sectionData = this.getSectionData(xPos, yPos).slice(0, 2);

    return (
      <SubjectSection        
        key={`${yPos}_${xPos}`}
        yPos={yPos}
        xPos={xPos}
        sectionData={sectionData}
        moveSubject={this.props.moveSubject}
      />
    );
  }

  render() {
    const { weekDay, itemsInColumn } = this.props;

    return (
      <div className="table-schedule--column">
        <div className="day">{weekDay}</div>
        <div className="technical-data">
          <div className="items">     
            <div className="item subject">Предмет:</div>
            <div className="item teacher">Преподаватель:</div>
            <div className="item lecture">Аудитория:</div>
          </div>
        </div>
        <div className="couples-description">
          <div className="sections">
            {Array.from({ length: this.props.itemsInColumn }, (el, index) => this.generateSection(index + 1))}
          </div>
        </div>
      </div>
    ); 
  }  
}

export default ScheduleColumn;

SubjectSection

В терминологии React DnD, данный компонент является DropTarget, т.е. предназначен для взаимодействия с другими перетаскиваемыми компонентами.

Для его описания используем объект SubjectSectionTarget, а также описываем функцию collect, в которой указаны свойства которые мы хотим получать при перетаскивании.

Необходимо также задать ему тип. В нашем случае это — subjectItem. Теперь в него можно перетаскивать компоненты DragSource с аналогичным типом.

Листинг SubjectSection

import React, { Component } from 'react';
import { DropTarget } from 'react-dnd';
import className from 'classnames';

import SubjectItem from './SubjectItem';

const SubjectSectionTarget = {
  drop(props) {
    return props;
  },
  canDrop(props) {
    return props.sectionData.length <= 1;
  },
};

const collect = (connect, monitor) => {
  return {
    connectDropTarget: connect.dropTarget(),
    isOver: monitor.isOver(),
  };
}

class SubjectSection extends Component {

  itemTemplate(xPos, yPos, styles, isOver) {
    const itemClass = className({
      'section': true,
      'section--2-elements': this.props.sectionData.length > 1 || isOver, 
      'section--drag-here': this.props.sectionData.length <= 1 && isOver,
    });

    return (
      <div
        key={`${xPos}_${yPos}`}
        style={styles}
        className={itemClass}       
      >
        {this.props.sectionData.map((data, index, sectionData) => {
          return (
            <SubjectItem
              key={data.ID}
              moveSubject={this.props.moveSubject}
              data={data}
              index={index}
              sectionData={sectionData}
            />
          );
        })}
      </div>
    );
  }

  emptyItemTemplate(xPos, yPos, styles) {
    return (
      <div
        key={`${xPos}_${yPos}`}
        style={styles}
        className="section section--is-dragging"
      >
        <div className="technical-data" />
      </div>
    );
  }

  render() {
    const { connectDropTarget, isOver, sectionData, xPos, yPos } = this.props;
    
    const styles = isOver ? { opacity: 0.7 } : null;
    const sectionIsEmpty = sectionData.length === 0;

    const subjectSection =
      sectionIsEmpty
        ? this.emptyItemTemplate(xPos, yPos, styles)
        : this.itemTemplate(xPos, yPos, styles, isOver);

    return connectDropTarget(subjectSection);
  }
}

export default DropTarget('subjectItem', SubjectSectionTarget, collect)(SubjectSection);

Почти закончили, осталось только реализовать компонент для отображения информации о занятии SubjectItem

SubjectItem

Данный компонент является DragSource, его можно перетаскивать в DropTarget.
Для удобства я разделил его на два, выделил отдельно контейнер и презентационную часть.

Листинг SubjectItem

import React, { Component } from 'react';
import { DragSource } from 'react-dnd';
import classNames from 'classnames';
import equals from 'shallow-equals';

import SubjectContent from './SubjectContent';

const subjectSource = {
  beginDrag(props, monitor, component) {
    return props;
  },
  endDrag(props, monitor, component) {
    
    if (!monitor.didDrop()) {
      return;
    }
    const item = monitor.getItem();
    const dropResult = monitor.getDropResult();

    props.moveSubject(item.data.ID, {
      day: dropResult.xPos,
      period: dropResult.yPos,
    });
  },
};

function collect(connect, monitor) {
  return {
    connectDragSource: connect.dragSource(),
    isDragging: monitor.isDragging(),
    connectDragPreview: connect.dragPreview(),
  };
}

class SubjectItem extends Component {
 
  onTooltipClick(event) {
    event.preventDefault();
    return false;
  }    

  render() {
    const { connectDragSource, isDragging, index, sectionData, data } = this.props;

    const itemClass = classNames({
      'technical-data': true,
      'l-separation': index === 0 && sectionData.length > 1,
    });

    return connectDragSource(
      <a
        href="#"
        style={{ opacity: isDragging ? 0 : 1 }}
        onClick={this.onTooltipClick}
        className={itemClass}
      >
        <div style={{ height: '100%' }}>
          <SubjectContent 
            data={data}
            isDragging={isDragging}
          />          
        </div>
      </a>
    );
  }
}

export default DragSource('subjectItem', subjectSource, collect)(SubjectItem);

Листинг SubjectContent

import React from 'react';
import ReactTooltip from 'react-tooltip';

const printSubjectType = ({ SHEDULE_TYPE_CODE, ID, SHEDULE_TYPE_NAME, NOTICE }) => (
  <span className="types-classes types-classes--vertical">
    <ReactTooltip
      delayShow={250}
      id={`tolltip${ID}`}
      place="bottom"
      class="customeTheme"
      effect="solid"
    />
    <span className="types-classes__items">
      {
        SHEDULE_TYPE_CODE != 'lesson' ? (
          <span
            className="types-classes__item"
            data-tip={SHEDULE_TYPE_NAME}
            data-for={`tolltip${ID}`}
          >
            <span
              className={
                'type-occupation type-occupation--' +
                SHEDULE_TYPE_CODE +
                ' icon'
              }
            />
          </span>
      ) : null }
      {NOTICE ? (
        <span
          className="types-classes__item"
          data-tip={NOTICE}
          data-for={`tolltip${ID}`}
        >
          <span className="type-occupation type-occupation--note icon" />
        </span>
      ) : null}
    </span>
  </span>
);


const SubjectContent = props => {
  const { isDragging } = props;
  const { SUBJECT_NAME = '(нет)', TEACHER_SHORT_NAME = '(нет)', CLASSROOM_NAME = '(нет)' } = props.data;

  return (
    <span className="items">
      <span className="item subject">
        <span className="imit-table">
          <span className="imit-table--column">
            {SUBJECT_NAME}
          </span>
        </span>
      </span>
      <span className="item teacher">
        <span className="imit-table">
          <span className="imit-table--column">
            {TEACHER_SHORT_NAME}
          </span>
        </span>
      </span>
      <span className="item lecture">
        <span className="imit-table">
          <span className="imit-table--column">
            {CLASSROOM_NAME}
          </span>
          {printSubjectType(props.data)}
        </span>
      </span>
    </span> 
  )
};

export default SubjectContent;

Ну что, вроде бы все готово. Можно приступать к тестам.

Тестируем

Запускаем наш чудо-интерфейс и пробуем переместить предмет.

React, Drag&Drop и performance - 3

«Вот, блин!» — сказал мне Google Chrome 62, а в Firefox 57 все отработало нормально.

Позже выяснилось, что React-DnD конфликтует с некоторыми библиотеками, например ReactTooltip. Есть даже открытый issue на github [9].

Ну что, tooltip нам нужен. Придется как-то фиксить. Попробуем задать свое превью изображение, для этого добавим буквально пару строк.

Фиксим вылет браузера

function collect(connect, monitor) {
  return {
    connectDragSource: connect.dragSource(),
    isDragging: monitor.isDragging(),
    connectDragPreview: connect.dragPreview(),
  };
}

class SubjectItem extends Component {

  componentDidMount() {
    const img = new Image();
    img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KTMInWQAAOD1JREFUeAHtnQm8nHdZ75939pkzZ805OVtyktMkTZOmTZrupaWUlkJBBUSKKIJ6URBRP16vF71yBVHB+5ErcvWjotWiVJZ6QfbSUmhLpemSpkmaNHvOvi9zZt9n7vf3nx7oLXCygJBq3mTOzLz7++zP73n+/zE7v5ynwHkKnKfAeQqcp8B5CpynwHkKnKfAeQqcp8B5CpynwHkKnKfAeQqcp8DKFHj60Xsu/dB73/bJB+75+PUr7/nvuzXw73v6c//s9Xrdd89dH/zwZ/72Pe9Mzz1ul2zffh13PfCjunPfj+rC58J1YYb/83f84Zf3fvUj72xvarKBgSttTXfn2nr9bv+P6v7+0zIEZgTu+/hffHXk8U+/fE3/ZvPVK1b3RS0UbrZ04iXX/iAZcuzYsfDjD9xzF9f0TnXe/xQMkVlanDwxoHcRhPfmA9/87K4jD/7FS9raV9X9VqkH/FYPRVrqpVy2Xsmlvr64mP+BmK1UarJz1+fuGDp+eP/PcOnzDIH4wdGDj96Xmp0cmTj61NFiYuofF0b3px596N4r6r64V65WvXwp6/n8dc/vq3iJ+VGvlJsNVrJjIxOjJ36hXj81Eb+X1BdTkxc9es/nJ/c/+tXe/sGNw57n1b7Xvsvr/0M59RMn9l5YL5RvbWuOjUWj8fFAJLpxZuTI/z5xYHd/U9hv4UhsQz03s+Hw4YM2N/KEtQbqlsskzPN75nnoSalkc5MnzB8MWqEatOauC/7BsyMJs82fXSbY6b4npg7d+MVP/eOD0yf2W1N7t7V3dn30dI79D8OQ0eNP/fzi9NidqcU5Gynmrauzy2CKjY+N2/EDj5lXyVks1myx5riNTgzjM6pWKmZ54TukBDClgvyOnTxkmVTCal7E+iueNcXCfw8hz4gho8cff8M//80HPxkoJLmHZlvMVsSUe/7TMCQ1N7F58uhTdy6MnbBosGqzYyft8BOT1tzSYflc1qYnjlspNWstsZgFm1osV65auZDCc+TNZ2EYUrdKpWqFYtEq5RErplMWi3fYfKTN2ttbO0pzI5cHOwf2YHLqKxEV8+gdffKh3/nI+37v/W3etG2/+lZ7+NEnLOP1ijEHVzp2edsLXkMggv/EgSceSM1O2MTxw9YUqVo8jKGZOmpjRxYtBFGT2aQ186SRkM9S6SVbyubN7ytZIBy0YDCMyfKZVy5azV81X60KQ5IWgkKV3JLV0aKZ6ZHd1USyOHTi6Edj8db3dHd3zywTcPmd+wjsf/T+u/7+g+97Q6c3a5dcfhXHZ+3g3m/Ypa9//96+vr7c8r4rvb/go6xDux9+/cLwkd756aM2NX7IjhzaZ7NzM1atByybL1g2m4K4VYs2NVsiW7RECrp4QQuF2qxcj1ihUrc8ZqtUrlswFOEVMB9aVrOcVatlm50ctYNP7bLZoWfCpaWZtxUSs9O5hdnbn0vUej2x/qF7Pnnyz3/n9W9oCc3b4KZNFoqFbWp+ycb3m12ycfA9z91/pc8vaA1BKn37vvaZv5s88bQtzB20dHrGctmCLS5MQcw6zpm8IhS01ni75Qolm5xJWhizFcbwlEoVzFTZwkG/lUtFQuGqdbS1WN2rGmoCQ8s2PXUSBs5b/9oNFiQAWErMWntXn5WLqU8NHz305ni8+VO1UvKNd9/xt7d9+sPvss3bNtva3h6LxwNW8QXt6NCorb9+u23cuvVrKzHhudte0AxJp6c6PH+haXZhuL4wOW0FNKCYy1gAExQisWhqiuLIYzCjYtMzc/iIEkwi4SgXYIRn0bAMUx3mFC0WDVsgoDQBo1GDYzXPqqWE5esFGx0qWfrgXiuQsXT3rLXm1lZbv2H7KztWrX7lgw9+wz7/P/+s/qKfvcZ6VsUtyrVb4pjJXN6O7X3EXvXLH/j49u3bs88l+kqfX7AMGR8af/vCyMm/nhw7atnMlOXzOSvm0xaCIE2xKL7BbyHC10qlYml8Rh2JDRL61iF2HYMUwncE2beEQoRCIQKAJsLikBU4jxy83x/EtNUcg+dnpi2TK1qoqd1mKwWrFrttuGJ2/7GTdvSRe+zGn7va2uNhC7F/PNaGdoXs8LEjFlrVbde99CXvXokBz9/2gmSIopm54aE/Hxk6Zs/se8wS83NEUwVyCZgRj1vARwhbxexUCzyvB3H9FgmHrYjjrhLbxmL4imAAM1Uj7C2Z56vDoIDVcOhiYCAAM9CGcqFmZb57NRw/JtDwNR6vhYkp2/vNz1u9ZLZtx3briKJtQR/HhTgsZCNTCXv86w/Zm9/zF/dsueza4ecTfaXvL0ynXs5dHvTnwl4tbwUkd3FuEak0a27rQAuiuHCSPCS/KqLyXsxn0QazMJoQjoR5RSB0ne08vudZM7lJFWZkMgQAmLFAIAxDcO4+CMw5FOxK80I+orFqyXLJBetoX2+DGy4moqtj/nQeNA+tWsyW7aEH/8Ve/LO/Yre89rU/txLxv9u2F5yGlHLJq+dmRh4dObLPnsGu13HOfb2DMKL2rGRXrIZZsRrZdi6No8eRBwJsj1kEAudLZVtIkIOUCtbV1YGparFIBAaSOAbkX9CaGi8PZpWrFRhVhrlFNAqG4HJ8XhGT6DNfIEaIHeYzERvaZWjGzFLGDjz9Bdt8ze12+1v+y++3tKxZ+G5EX2ndKcGulQ7+YW6r18eiTz1w4EOHjx5829TEUQhXt9bmNov4A1YtZCFgwdn+UDhCNt7sJH52ccGmCVsDmRl8QBqH7rMiJmpsdJyoKWKDg+uIwPwQuWY+MvVaTWYOdUArksmsYx4K1AgSILofUxjE/EWjMnlRNC5iUfwVVsxGJxds3xN77KqX/YS9/pd/yy7a+eIWEsn0mdLoBcGQ3d+879YH777jy8n9d/v9PdusfcOV1je41aKEs/Wqz+qVElqRQQvi1trRTTIYR7qBRrD/yfkJy5zYZSMjhy2dWcCpF/E3SLw/il+JWCRYsEhUpgkfgKMvY+uW0KBsgUCAfMXn8zsmGDmJYdZaqJt0rupAM+JWQDtnl4imTpwkqkrZy1/9Vrv5llut7+Jr7m1e1feKM2WG9j/nTdZ9n/3Yu/7yt279k9V9l1nbhT9hHX1rbM3GrUAgMIPIyYetx1vwvdOa2nphyiq0AzNSLFg9tWjxEEIabyEEjpP8FXD4QYviQ/LY+gr+oOZHOwAWqzj/ItBJoVh2zp1QDE2pOr8jqc2zLer8D2YKpnj4i4nxBZuYmrKedVvt1Te81LZtv8I6e/q4TuVODjmr5ZxmyP5d995215/8yge2bLoaEL3ZAu09tm7zZRZrX21FOVsvhNDWMSEhi69qM0/Zt0XJM6qWWpqyiUNP2iKaUUicsBTaIT8RDFRJHjOYKL/FYEyVaKxcrqAJhMhIfBUNUbQUJjBQpl6HaQHC55aWZrSJd5gbwCelCKUrtYoNbtxi19z0Klvdf4FV8CuZMv6lWCFMO7vlnGWIqmyP/OvffK6rrcOLRqOWD7TZ+m1XAYmvtSqMCPAP5ImIKYAdJ/v2N8OcoNXKeUvMjtjIgV22cOJxq2RmLZurkF+gDXLQEL1cLlsM4qICaBNOnNxEjrzy7Gf5J0Vk0JttFZw2ZgsmRIjgfMpt0B7hX60tMbtg63br7RuwcOtqC7WtUthbLJbLZ+zMl9l3zjIk7svcXJ4fCjYDmWdqftllW712IymdEr4mF9XITyhMjURBcTEhFZx7ZvKkjR181BaGn7JqZhyEsGj+AP6CTJyMBMesGlEcAvtJJjMwEmIHQzIzLmkMhcKEv+BYZOpieRhnHiYvCQDBBGFSiZqJUssiJjEaa7KODkynF7CW9k6LtLRd1NLVf2SZuGfzfs4yZGF64k0hoAs/Nt4/sNNWb7oEwK4ZAkWJejA1ECyIRNeJ/+GXZasZS84N28mDu2zy6B4SuDQJXcg5acB2iOkD7Y0BqQhYFMrrIw+pWD4JpFIg+xZcQkilt6LCJl/F2mIya8pLlJMoJJYmVQm160RhCQtFzWanZyzUss7a+srUYPpOnA0TnnvMOcuQQmr+JZLaQNt6W7P5cqKnPhfZ+LHvFajm8fLjP+CJ5ZHo9MKwjR56yqZPHjB/OYu0l52jrpPkxYDVw2HyDaIw/DJ0BwYhXFbYXGptIQI7jjPPA1+J4DAPqrhr4xOiHKNMX+Ew3oGsHs2BQQZSvDg/Y/G2Ibvgoos5nyNrK3/P2lzpDOdkpi5opFzM9tZa1ljvthdZV98FOO5WkrGoZYmCijCjDpGUHVdJ8JYmJ2z6madt7vgh81F4CkJwEd1DuyJNYVDcDotHMS01cgacboWAQO+JBRJIcKeBgU1Id7e14ouU7LXgG5qbmxwDUEGiMDRECQkBgA8tkXMPwiz5omJxzsaG9wJAgqklErc6tnwff85RDcn11uN91n/pTda/cZv5Yh3m4StKIgjmyoOgfrSnChCYmhm2qeFneO2zYnbeol6Z7dQ2ME1BTJQPjUqSd5QqeaB3EslIq/nJ6muEy4vZmo1PzFhvF5oTX82+HAPyax5Ze6Dk/EsI0Q/yqoCB1bh+AJQ4BEjZ0hqx6QVM11KK5HOcezxssVVrP4QwffJUlcWV+HXOMYQH8uVyuX8JtQ/YqrZ15mvuwBEHnQnB8FsY+CObSFtyYcYKqTGbHjtgixSnqvkp0FaYUQMsRJqVJJYg+sRMwipEZU3xICYMJiQxZWwHGLHk7DTaFLB5ah4b+pvJWeLgXjnK65glfIYP0BHLhs+R40ezSDQVmSlaC0e1LmhZ6u/+fr81872QnuueGB7ezk57VyL6StvOKYbIVKUXE391ZGTy2rI/XhdY6PcFrJxt+IQaWXZqKWeZ2XlbmhqBoIcttzRCaDttdRoKlMD5AAB9RGE1suyh0Wkr++IWx08oUsungVjY1t7RZZ3d3dZPTvPIPV+ycnoBZHfJLhpoJdQlRZRZIqLC7WPmCBzI0OVHfDgXMaUqxmDKIjj8Yr5uc1OjJKmPW9UfttbWjjt5jp1nqyXnjA+RZkyOjd8xNDLxNp8/6LW2tHkh3mvlklevlL1KNuUlx054k0d2e7Mnd3uZmYNeaWnEq+YWAH0zuIsK4HvdK5ZrHhrhTS+kvcVs3Yu3tnsd/Ru83sFN3ujUhEcO48VaurxUpuBdceXl3i/9+ju9SKjm5XLsvzDrAd17QPMg+QHeI9A14GGpeHnuHZTYK+karKBm4tX0PV/wsuk5b3HmmFdKz+xIz09euJIWrLTtnGDI0NBQ5ODBA/dPTM3+YijcZG3NLdTByaAzS1YCEk8mEjY5MmpzOM7U1DHLLRxBqk+ar0xPFb7BhxSHQFsDSKgcfyZftZlEznzkFPFVa+yCCy+xw0eP2X33kyxS525v67QU4OEDD9xvF27eYJdd+yJb39dsnR0tFm8C31ICiJmEGdDOj+9AV1CKckn1E7UPUf4lugM+INEku0E1y/l5m585YWNDzxBGFz+yEtFX2vYjN1ljY/P9CwsTTyJx3UquQqEmF7IWMknTK59K2uLsrGXmpswyE3SBJGFEiognD8HwCmTMYYpPOA9X/SuRRmfJxrN5/AnE7GxfZZMT4/bZr3za0eGn3vBTtm5g0J7cvcuO7nvKbnvJldbTRck1jQ8h64fEUFqoryD4hiNHex1jlKuUVGjRN3wZegTqznfMZN1iVspVbRyGRPY/fmM2O9fb1NTFTZ/Z8iPTEB7SQzNePTk1Mm6BSHe8bTV2P2B5krQMPiO9lLBF/ERi7KBV5w9ZOHfSQqVpC5STFiChk2/xExVFgMJjdHgo+ql/C0avWBFH3QI8vqlvNRo2aeCMtq5vlT32+KP2lfu+aP/0T3+PVvRZPTlh1dQU+QbZP4hvEQ1QeBulvh5C9NEPNIQoDUimQo5Sg2J5mFJDW2IR/BXgZZnrtndusL6+zQCPMVsc3W9j+55415mxorH3j0RDYEbw0OHjdyRS6TfHSc4CmJYiiZdHF0g1m3adg0vzk5adHzfLLcKENPE/fVMQIxAg2sHJKiOUhNZwruo8VLOIoqE67TzVUso6mkO2tjNonW1h23TRRve0I9Qs/vLPP/QtOl28ZbPNjY+hdRkSPpI/rq/0T7iWn+9KOpEbgb5oH/C7hABkWNcVfCLNjEbaxTLeieSojdDsZa3kNKls9jcOHjz43y+++GIKvae//NAZsri4OLBr995HEbLeNkxUABwJm9vAiGhoKydnLE2JtLA4ZV4+0TBPwCB1wtkKRNHDK+yUWcGwYFrQFqQ5TFTl0bGgdp62Jh8hbJutaiLPOLHX1l+ywz78J//Nvvq1b5BgNlk3vmJ97yprD1csMT3ltKBETcWP6cFVQz3exQwYUQeiUWLod1m8TBU5EIwg+HUhcYzwugC6XAHa17pcrmyBQtV6MJWxSO1STrb79NnxQ6yHKIo6OTbxtkPHRv4qgClp7SDExDTnME8CBRkGADPmrJiYsCLO3HINZniCQarsUy6hRTABQFF4knxGHdNFaEVIGoRQKiZRZqUbsS3eRDhaws1kbeLobiuF6nY12nDxwOtcU0OQHCRHj1U2McZ5ybZpqAsANlY5t5y1T/GaQl3efSQi6tUKwBAtgk3ok6dKKP8lBLgJ2CVFsDBlLat6bXDDVkuVSSQ5T3NT/AYOOfcYkkwmO/YfOnYPFbYr2zs66yEkrEBtm3DRSrR2lrD3xeS8yweqZNvVfAoTRM5A10gZcyLpkxlRpKPmtpo+iEjA4hgrIp2SBTEnQmWFAKsu3hyPWAWHDxZuo/u/aTMnDlhLRw+aWDY/7SIB/ILwsDLasIxVCYZXhq/8g4jBaYyYoAYI5Th+zqcEUT4eeqNZgI+gAclaGqYuWiuNdus3bLEquNkooGNHPv9Wdvu2jeTLqZZ/V5MlrVhYWHrT8ZHxf5TENFHcka3O5ymj5vOWTS5Zli7D0sIYEdS8VWBErUyrJ8TyUSP3QXT5iwL2Q7m1CAa9IQjkwQHXMCfqoXKFC7LqOiBinfOXYJocrZDdCK8AuNT8QsKSiwecH/D5sPPU3aNok59kr8zN5dEUl6HjhwCGibZolJP/0HVguOrpAZghXBEV4dyql4gl/EOTavi2JFHhPChC50A3pjFqCwsLW6enp5t6enp+9I1yQNobxscnvrSYSG+WDVZXYDqF5MtOIeFLiUVLzU1bCZ9RT02bv5DAIdM3hRkKAQoK/pbPqFIgqshMSStw2MSiTlukKSpQyblyEOukNRCQY2GLZahXqPFB8EYAjYy3dhIpIemYNZd1I+0VPpdpeig+W+NQjUS+Qg2MPt4FKEoTJASCYxRQKMxW/xd7WBmTVUJdwtTX0wjT1Ogwjv6gxbv67cKLtlw5Pj794Xol+xJ2/hKv01p+4BqCVnRNTs5+eHho8o3O2YEnYUEwK5gdEZhWz1Jm0TIL05bHcVOQsBqvcjmDrIkQ1LYpm9YJMWVQRHiVZGUnKvgO1xkCU0swSOVbH0TEE0M1oi7Ze2ANaSEoFIhuyTVTlxmfEaeBLhyhSsiuXAKtKDMoR+ErQQHEj0gz0CqFuT60QTfto5gllgSU+cEE51O0jbPTYK9bshyXj6L5mVTaapQBKqlhUOQp67tgcODCiy+7YWxy7C2nxYlnd9LZv68FBnjD9KC15XJbUoupP0ylcq/Kqz8ToqjCJkeZItMuF9IOKi8uzTScN205Jdr9Xct/KQshqH1AGA8TVaHKVwW3Ukeh/IE6QSoQvUzcy/UcoVV6dbYdAskJy4vKqasdNMi7GCscqgyj8iiPGBlzLaaSQR1LhVAM5bxquA7DkCDXi4Lk8hVqo00wyHUz8gwilNMszq1FWXyugpkiolJ0NkMJoITZW7tpi625+EbbvOPapQ2XjHd63u0Q4/SXs9KQu+++23/lDTfdEAwH/vjQ0Mh1MzMQGSbEoyoCATvQ8yQgToNl0okFyyRAZnHaJUVPLtPO4gTzVoJJhFg4WY2AhUg8uEbDCmWVWXDVOWy4IqxCGX9BJCWGEAs4M1aFaGJCEFMisxXAXMUQBJkW1MeZSeUMalqQX6kUcmibIicRVMeRmUN9+QdlFn40QdoR4N7lG9SVIm1QxyLK4FRC72K2miE0DE7b5VdiQC6JqWlbAv0NjA/BvVBbLHrdLexwL6/TXs6YIXfddfcln/zMvfd+6p5/6+3q7rRVHW12wbp1tmH9IHa6HbWmEg2hFhlaNj0+YvkEUVMWNBVf4SthljBFJTSghI2v8cCeI6osDo8q24amyC8EkFhV6bIwTn27NV9MD4kUUm6FSYJIxEAhrrLnqvQJf5IdkXP2Y8oULQVAafUvTjYv4qkWXobBIn4Ixom4YoZMk3yd61xEe1QZFLP9bh/OxLWE9Ioh0jbHDN0y96xBPzFag6p0tbRDj852zCPanlyY/bhMOP5HD3Zay2kzRKbpg3/5N+9+05tuf9/1r3qT9Td3UolbbZds2wo21O96nRT9pHI5BrmM2whAYB5p8QhbC4sT9EklGMVE7oBTrMr44k+AcZ0k6wGVVOmunTOFWCpG5QplgEIIDWCosR5ljivjT2Sq5OQdjoWElyi/GiZOg2SkHGKKW9ACmRm45a4n6Zd3UceINAN6Y5nQSOyRc9bcgWRC0Zx6tWSmZA7l0PVSXV0mTC1EYML0akmrazhyuhjVTR9stcQi4GV31VYRZXGWDoZjX8NpHmnc0Kn/nhZDxIy/vvOjd/72r/3Rm6+77afr3X3rbP3AgF1OBtzbv9paaFZWZ/nw8BD9tkfs5PFjmAakEAmtpOfBoJLW4i9ZGWGOhoVBNYo/MisuZEWa6nL4enAerAJV0mnqHoTHqhCGVd/gWbSPn4EzGs8h6Y/TbYJnYX2BzDxAToC5gsoNxwhT8StBeOFHQKUH0pygHDuFKkm2XhUJBaTDczjGqWmCvRxTNaSwsR+7OCbJt6vDkePRXgUQ6qYPoLmuiSLcyvO1W0dnL8+BCcPR+xeWfomjf7AM+dxX7vu1d/ziL7zlkhtuQ8JpWMZk9K7utLV9va454PixY3b0+FF7+OFv2klg8miEGJ9sXEldB5W0Vro85BsiENPHg5BsQDg+ixiYGjlVJV01zpsjcVMfbpF3j6hJsb/CWGiDrUciMWnKT2LUykVomaoI+0S4hsyOoBNl0uqxUrO0TI86DuVXXA8WpqdhfkgOMf7qQJEpUr6jkFxdQnLSin3FHLFXJlQQDWrhzJjMK2wggsP84ZukPWol6gDD6u+ns3LdIFFOLwJFP1i5+jEYctrLKTVkcnIy9tZf+68ftnU7HA5EVQapKNCPFLOFpWn7+kMP2xN79toS6KyfhC7KqynQat2rV0HEVpufX7A5YAwf0UtbBYiEqMaHFtS9CtFMw97LLARxvFkwrQwmT/a5iW5EhbEax+H5ICJmKJXBnwA+CsSTDygWKbdCcGd+REDIpMa2xkudJmgX19W1styDeqpkHpejJTFN2gLnG+tl7sQATK+WRiQnJsMQXn7ux4GQ3G+OMSNL5B5BAolQAPPJMzX8HvkP9xylaa65vekVq/r6vu5Odpp/TsmQYydHX/LlTz9km3ZucOFnC9mtDyh6z1NPYi/n7YHHnrJWWjLDSHyWXqVsctFqU3M0OU/bAM5eGWsyWbIM5iEJAVrDProGG6rv42FUu67xsOpKVwLYFFfrTdhFMZJkSZ/MhhK4As5djAuCqGZhjNDZAPC7IiYRTYNrJN0iuPMdsKgsjSO60iLbL/8gR61jZP/rMFCOHz6wt3xGw1/ourovdZZoI1VM3TT/Ze48SgQ57iFjfZ0tmDvtIT0yG2Kce98l19HwsM5iq3v2sOqMllMyZHJm+qfNpkh8uqy3tx9iEuEUMnb8xJALR1dhk/PMhjAzM0slbwHzwSAZNGh85JClFi6yTdsuR82RfhLDPMWnZsyNTAvPC3FwzhJG2XIwpyCdJSE6DNVrW8IHyZQ4B8ojC24hJiKsxhSqHRRC67siIzl49MIxryZzwxV0fhFcplBfGv8gONs1rG05ZHaXh/C6F5FUDl7H6TTK/n0c74MJ0lqPKK7CZAK6F/UHN0WBZhCeGn5EdXgxRr1aQ0eeIQihwyXU+nJW3cXrtJdTMmR0dOyndDZhP228QmgCj4MNb2H8dtpGhocptSZxg7TqU4NQ31IoQmsneUMER6tM24dEF4uoeZmqHl7W1Rsgkf4LvdU+erAIQ81kyysECJJQEVJS/yyNnb2XGCpsrcI0v3PiIoMWQlK2ieAuaoPJETSXs4hVELrBCLlsaZMiJtbqdDAAZrC/XnLkOoZV3DdBgZodYAjfOC/AAi1FWTRVR7Y2qxNSJhyBoDtevC+C/JIBk/Wzrpx7KweeEUMkTt9z4caBd3zRrVe+1K6+6ioXGgpZVYi4iG8YGh21HCOUBHOoIySxtEgtY4mHqtD3KklXzF90+0ulRXw5R5mBBlFkAkiqYIRGzOqBqtQlZEakNYrGVBFUkintccMBMJnqtw2hqc5RK/JCrDVwM8w+YohL7nDYMjkyUY6YvIlhOBTHiGUCK5t3+QX7SQa0k2MM51Q2rpZVFcHgPyaKDknKBVyA6yEIaJDyqRJVTkH4gmCa6Petk29p2Fsxm7nxve9974o0fj7xT7VzbE1Pj119+XYSwA4nZYrPVbRZWqLezdDfttY47ylyBuYXoUS6ZvNai1PrWEoskcxVGTIWxZlmiV5w+AwFEHAoQiobF5gnh6zvknrZZA1By2YySGHGlmBuitYd+RYRulFDB3ei9q0oS13pfgKDxvkavqPRh4v8QlgnuZJechfhXmJIo0eXCiAmstF52HD0y4Rp+AzQXsHwoMI+n4YsgFlBcI0Rkb+X81ZRTO2qBWiQ5R51/7ofCWdJ/o1G7iQoxdvf/nbKiKe/nMpktc7OzNTTqSUngUJOS0hSmsre+MSkdXd1ErXGmEtkiHpyF2O4O5yzPXJi0t3BNRsv4jhmVJhbsGZXeZBGkIMoWkGaRCBJX4GHCUo80R6eCXNA6yZhpB+TkUhlgEpgpqBytKGGdriEEI3gAKdVOpcERQ5ZjJPE6+QKeaUh0gmZMW2T9C9n3Y0AoLHOdSZyCxovqP3FUKlvkQkH1IwtRFj+TGEyV9AeDupXgKDkUiGzerlKNGLPz4zZ+o2XurypUkm3c6M/GPh9an6+86kDB+hDYuQp4agSMul1Mp2xqcWMdff3MWVF1DZeuBWHxnDhBFJU8tlFmy+1tg7GSiBdI2OTDOjHATZHLU0lrZQk2mIgfwsDJ8ORBsQRwm80m+Bysm2Y7id5RCccwVe1tTqGiD4VggNJqwBDZzKw6QWgFUVy6ABHNLyCYHtV/Dw02dFVoS2LvI0LJMhVamiooP6A0Gj+qcPEdStqR5hXwnHXfWiAOuOxVwrB5S+YVgt/oVyIY6QR5FZ1bsoTw2FYuRawBFo+y1C67k3biT7DPZxxXKc9nWVFDUknk91TdAnGqENnMnRXgB8pXGyFSFdcxhg/cKQIEhJdfwEEUESiLJzwEKIlk2kbGpk0j4dQkljxGHfBzS7xoGl6mdKYkWCWOUiiAWvHBIUJK9Xp4erVULGAidBgfcl3nWuqr1aS6/wMxNfQMw3AVMCgaTPwFtxPxEVVDVyMx4fIsjEohjOPYkYjkZPpajj/orJ+aQ7b/IzGFXOkQdKoMuvE0cZgnQhMQeyI8CpEeNJ8MUlM1SL343p/ef4iJnyRsD+FH+m12lo2nXYZd0WGUA7tdxABD6aBLFkupIxUw7vCONsyNjUnRuG49fBVmtbUt1QiCcsQiYTjDEMDQihB0DTAqZLKSGsX/GLekBJ2Fr8TLkKCFsC/eJTB9+QjRCgaCqAhGgVFZXyWmVNYqbHiPqRS5sKZI7RGtl6RWCpNPQKzEaeDvYrgcEOOSAp7ZcoUyrp6PITmZI7ocv7SIBHfnVOEdfvCCJaAzCACF+bZwzy3/IsgdjHNMQ6GCVcLOkFE/3h2wfqIDY1zNIIvzNN/nFjnTnaaf1ZkSCFf7FugJBkFM9JNy2QVsed+CJnHhKVwuokkY7552DoPoqECeSS/r51Bloz/VkuMSiNNgG+9/b0U/TFbzMYzP0eTFASv0hHop54QJDdph9DKekswV+db9jV6SM6OlpGjIOkiooitl/4VJOFEVH5di2vnuQeSeIgiEov2MIZ7l3YsH+O4wDclo9J4jcISweU3pJkygYradCU1P7hOE4SiolCLfcRE5TIuAMD0CVxUIqpeU/E7lwZUxYAq+iPSW81pTntZkSGHDh5qW2Qw/IbBAffgagLTQ6TxCbqhUASp5iaUd2hKC0ELIYgjXEnqq/tXm48YKrMxMTFmqRTFKDArYVkVGqA1+KZIaVZDjMGFYYSYgdvEbEiyBZ0IwpDNDuNfviWZRE3qGFHNRElbhHvQIE1plEwZcs8xisz4hFlpEBdt4ZNrJ+KcIqKIK9MjFgpaiYB7qZClMYbCtSQAEhA9z3I4LcaLGTpWXS4NrRIzyH149iIAY9vqASLBVu4fApzBsiJDhodHfH3dqzlx3IV7fThxhY2TU5MUYrKEvjCGicD8dF4IHm8mnxCaW8OEqOKtGL3AK01tJLVErxWTwyi30OilGthPJEbTg1JKMC7lA2xC2hsSmcf8CQmOwhBJujNXgIkOZ4JhMlmqq2TxSSKmzIqIrpGxAmudfhCCViF6kH1doil2yF84k8ZuYjAnFyNY6wIK+YnlCCsCsd3FOU9ZPhLuCukVPiqkQOZcMLzTDhGd7bXqLMO2b7RNl1yOye5QVHpUm053WZEhPMSwJEWwtVRYGqBKmZgiKUkTgxcw9nUGuMhMVIBUsOQu81aomgcMLFFnFrqrkmyR2dk07YXKtFW0Y80FW6yNACGMLYqTVzTFZLLYzvWgoSvVyuSIITlqD4Lf2eTMI5RtSC6MlLdQWxEW32lJYzafhu8Ici1NySQw0g1/lvJwDpkhXceZYqLIGGZK22UBlGCKoaqha98gUq97KOWEtzUSSVa7qIrLyyw5RstMdXUOWBPzPXb0rnMDhOKxji+z62kvKzJk546dX3j3//j9P9t02U67csel9NumMTly4kiHhpItzQKViDAF27fnkHWt67AuEsUFGmllPoRPVZ3f0WSToLjcluAMLZpOSbMldNTj+BhJHeF0lpgfjYJjjeN52AJOvwgz8syFpUE24SjSrPoGxwVUKkaiNXeiMKYyZk3D1ar4JY0vF3QTBzX2oREK3RUAhmCEElKZOUm5IA9NWFNGQBZJ8JqI+KTZ1RBMRstFZNcsB+VV+/BR1w/TZa/EkBtw18PaIlRsg23r115kwR4xA7PV0X1HR/+acffAp/lnRYa88pU3n/jM57/02ocefPCzX/vGY3Zg9ze+47Qbt2y344f22Nt/9TeB5FfZ+//wQ9hPIjAQ4SytPogmEkcbDkMMypiGPE5T0s0YNTcpTBMPHgZqKQJL5MGBVM/QQH1XCwHZLdNIoA4TWqDQKghUbXLQvKqNZWYDUhZeZ5ygkF+8AgRCtnnBM5dZO+0GAFVTnbSkEZ0JIATyQGA0XpAZB2x2Ie3ynSaiR1VBln2EUF6ftIKXJErmSX4G1rpgQ7mOxjlWQbGxt4T5URvcfIm1dXbvb+8ZfMd3EOwUK1ZkiI79yZ941efGxsZib7x9+pUHDhz4lWwmez1oa7h3oB8/krZf/eVftO2XX2NXXX0VM+EwwSCDUFd3bLOpmbKtWt3nTJyy42Rizt3Klot3IvUlIq15GqKj1sHkL6sIe1vow6ozR4mcvFqC0snDBmZpKeRrehazwNHRDiRRERH4fddAM9Shia4gnxnBbMBoJUCiGibET9Dgmhf0FQGQlENnmEqTBIKhoCuKD4NNBCloO6XXFkWGaAD85yAloQ1NVTuQUprlCE9C42dMtAvF1QXJthA2NkMS2dTfYz3rLxxrrwSvwSTqTGe0nJIhOtvatWvxsKYBFm6QxdBQPbJ2vX31ox+963ptf91rX8MD1OzP/vQD+mqJRJLwuEzHCZR8znLTzbcRccXd/CDdXe3W2UxEg7DVS0vUF/YDyNHhzvfW7qutb83t1tW/zrp611q0pVVzlQw1x4MPFdNDL39mz67ee7/wdcJLmuqon1RqAHty9JgsRzn5GkyKchRpo9gkH6NGO7UWyfSEIKjmSimwLplRg14jTK6icQIU1bHosn0YKl+j3EX+xjGFMF3+VNGZT+fyMyIXDQ229dtlN75ypmd991bPWy2anfFyWgx5/lkHB70CXXnv+cTHP/E1bdt68Ta7bOdO+913/4F94I/eY3NgOcvLhs3b7TWv/UlMVrM9vX+fHXz6GYYhr2Gw5KIl5iasp7nXuge3wfRbrWfNBmvrGXh8dWfnne3tq5+oRwMTTU30DdmaIkSp1aleTqfGUt2t6yHOzfan7/6Erbs0Shsng3cqBB1c1I9QhgAc6zBIJk6JnCIn3DemSOZNIYCiLfGKfeChElC+Sbcgvl78YZs6aLSfzuPGFbJdIYaOIxiGKURYoVZ3TGr2hG289kWLg/07NnteR4Zdz2o5K4boSifHJ+b3H22AiMPA8FdcfY29+nWvs1t/7MdsfHTcQdWrV/fYyZMnXRY9MTFhj+09yFCAVntkz9P2suu22I/d9lq75sqdtnbdwCeYaPj/BKMdeyGGbM93XRbyj/3p9MTD/nb80faL19jG7Vvs6X2HrHdLGyFQo7Yex4I1kNpGNl6GuBoEpIxdjliJoEJVgCj8gqI3cDq2K7wWWivg0gGQMktwWLCKsCwBlmKUj6DBdd0zgovxhfgxtIzu/aY1V6S2XHb9Rq+jAwE6++WsGZIGG1FT3C0Q9StffcCeOXzUXvbym+3a6290Kq/WyjRwxj604mN/90kb2HaBAbzYxK499nvvfqfRTjS1aePa3/D7W7/Ig55SvfP5EwMjT/7tO8JESBXC57bWNttx1Rr7EgxpKxLwKiGUYBMlCdPStK4Fta+iEfIFcubyFyo2aQJM9nRl2BTRniYvCyvBc54KTSJgUF1esw2JIS6BZGueWojMmCJF9RD76DLxWtfawM4bMjsuv2lDy7pLE2fPisaRZ82QrRvWjKpTcJqu8s0bL7Ddew/YFJDIvgNHGdM3YY889oQd3/Ow9V6wzXZeu9X27DpoO6/bYb/9yY8MveyWl75h1apVu2GEwvtTLpgZb/LAP/xrrTwBmAmMQygdDjE10nocO4uvIsgcAkKsABKsifejDDeL+GnTUXSkyA1mBF0Sp3yKbBqNKlKnUedkcyyEL9MejbA4oCgN7fHDEE0akGZGoZnZvdbStcH6113gCmqeP4tv22Zbrn5zMdZ/4UZ6h+fdzXyff86aIYODg4X7779/892f/uy/7dpzoHNG6KFvzkbH77UR6usZHDsolk0x9wjzEds7f/0387/wlp9/3c6dl94HI2S0T3tJTtx3U2b+scuAZRhGKB+gENbobGH+XM6iCEezMEhAVPBSR2IToW4U5qk8LDzKVbzJPeQzVJVsVDOzQDhVawWJkOZJPASiRgg8ZNpShHmlRIbpYcN21c2vsb7+DrYR5VH3jzTR9VLpYDrBymtgxndMPX7aD/e8Hc+aITrPLbfcchTp7Xn44V1XPLnnyfc98vgTL7//m7stMzLF1qr9+GtfZVdefll1544d7+jpecWdl1/OVAtnuOjnh4499tS/MBklbISwMEQRleB2Zfg93YJAyBwEhyPWQmcFzyjz9jFsWfPw+ikT+NAOjcxVMUl9viXMGaexJrJweMgx4GtwuUiENjM3535FoaM3ZhsHB23d+h56rtBGQvOEStSMyIoCFVVrCwyDvvufJye/sLav78cbrS1n+HzP3/37YohO9qy0P87HV5CvRBOZzA5ftXoDmXCECVweJGTexT5nzIjlG504WH5xKXu4IxIQkKiWHxwF7NBU4PEmv/Vv6GEgDrAJ+Ynz0miPdpHPEP7FXxwvUAgMEfyufEJTiyvRE+hJXupajFQ6KJcWGGXVbduuudQGB9dYWzuDOpmiSZXLQrUxU0SEacTzIAzVGpOmcckm/1JHfmbPN+sPPHCld9NNZ5x3LD/n8vv3zZDlE+n92XxlFx/1+r4X+Y5DD/32R8I0ookNyroVsmrqVuFjcUKq7u52GuqP4JSpswieJezNohVNeQhJ8UsN2HV8jJqz5dCrwPsF+rSYqAHTpS7JnLWAp/WsW2ODG69goM0afqJCk2HSoEEkpTHvfvISTTijEbjKP3T8UjUGw4Bm0L5ULrljuHVYvzH1RgTmtPzi9yLOD5Qh3+siZ7t+4vgd/V5lepN+VgJ2IN1K2JQJ6B9hKSXWlrjPsscZhrBtHpOFJsATlQdi9ExpCiXpiDre1UmPiycEX8RcLeDEwzRl9Njg+gts48YB5vBtY8o+NBCzlM7OkGzCdH7URdVClZWF9AomUbNES6wVDVMbEiUHyN9CKTqdO/mGiWc+Lqji/Wf7vDrunGZIZn70bUG/frmArBri+JkDq4qqaDo/haV+HPKNN1zG7NLtlmeaJNX6l2jIUGTFXCYQEcgEggklFqfUzN2xOsark4JZO/kPHfydOGlMT72etCX9/JGIglYFYS7Tm2Dq6HYkGyfTdMIglLgdiCVfo8cApjQFCQCo5NRiS5Yu7v/juROf2Ne14Y1fOlum6Prn5IIm+A5+422FiBWZM1+t/dAUayDf4IpGmBtNxVcsIJ3JPPWWBJjXLIM79SsIgIFAIrCDfRUCU/snP1EI29bOPIld/PSRilBEYj5CY/kkdbBrDDshgdMCaUINJqpm4rQD5oToDYgTkWnfgnwOyWILDYOCrLgi60CIyahaVt98YevaW4+dDWHPWQ2ZPf6pQfBasAnmyOWfj7hAXe1ihjNb5AqCHCsMSa77mV8xsohDZlJkN3AG/yDzxkumTosMVjhCI1sLCR1aBy0ZQEQDAwU9RWSqeXBil0y6rJzryFy5/QTZs58boidjiR8SWKlhcHVUUEMmREj1vgTCTMQ599C+ubnPdXd1vfqMZ7Y+ZxmSzSZ/BtWo6yclRCjE8FuSKvKKGWJMkNpEmBniBHdIk1RgUqGJHBFiqtSqFtNGfqL5fdVUx8mc5KvoJigdeoO8YA6h/nKDt2atUxmZCNolki6X4TyuOwVmqD9L2irzyCxNnJO7IuNXObvK4JDi9Mlv1O+++wrv9jMbY6i7O+cWCM2cAfO/yyBQrJRQRXkAPogCUBM+0LfA6A1X+KDhE5AKc+JFIlGPudh5JyfHv+vFKq8p7vdiTUEPE+VRa2F7hLmwQmCEfg+CepQDPLowPSYV4PzOMLr15Dxck9jKVaOQCLbpc8BxoPEdAfCYEZv7qXE+2vXYzpQaXtg/u2P20pE/PlPinpMMyUw/1Wm1Rdq0lJXLscoXNN71XZohKXdAoVsvv6J9BBw2Pqs5Qj4iQrQVAhZpvNAmYHdBJ9pXqblDhKmPuA4SzunWc3ZdTxiWWlOX35fbVN11OF7rpWHSKn3Wer0ChF7Nyl/qi+9aOP6x686EKeckQ8rF+St99bTrTOEReUiSPUd4xwcJqv43XhCxsTSIqShI9l4OWMTXryA4OISfq5B5c/Nc4ZRFODX3lSjhqr1HP+Iis6RF0dkyE5bf5dyXmeMI/yzjtU4+J0d/gRjjjle3PB2RMe6hVhh5sD73uQbo5rau/Oec9CH5Yvr1qmirm1yxkmyzXsoDtOiviCJiSVlqQCoaDteI4pUrNMbHK/cQjX36OTxCWTFDnZVisPtdELpW6CPBB2nImzL8Bpd1/m8zAkgGoj/35YY2PLuvn23K+OWntE9j4d6EFHP/8VgpOJ/Y+2W0+sWcX6q94nLOaYjzH6XZNyLnPCSDPWkzIjcnW2YYHNT/trmSljReanRYJpgkWebKmSwSSk0xrkY2OXD9JJ4b066cxMHrmsiSLno5aHcutNFph1p7Gi8xUgwUg5xmLJsliN8QCvrQ6JYMETLTFg8nVWtHQHgJ8FTwFouUr0+M3/GzK3Li2Y3nHENs7pmmemWOEXJ0EnrkH4ze1TgMDfhXRCOGLBNP787WQzAcLRogIjakWTMOuVkdxBy6GlW0UvFJ0ZnGqGh/lZM1QkqLiNs4r4iv8wj/AtZ3oba0R9uf9VEcIyFQx6Sqh4JkqLgAtXAiro+1IqJAz3RqwvOgEk1Lf6yevJuugJWXZR1bea8f4tZMoLK+UsoD+km7eUEEvcl4aVlmgtaIiM6/uy3IJOZJRNOsEI31jaNkgrQsa5fmLpHPEDOFGi+fQ0RG59x6mTDXF/ysRjS0o+GndA9cyN2L7soxkne1DwWor+vHjtW6KrTZx/cqA2H1OJlM8rPcw43s33gY3dTzlnOOIaXC/LX1WrouOBxSOTaIJw4CcTe/bLb0TGKKVoo4DR/gyq/gK3p3zNFmR3GxRdol6ZZDb7R+uiCXkzgiyw9QTXSNctrPRVryVY3Pjimsl1YsLw1mq6MROIdbEtMVJSt6q9DYHGRIhn5B1P0KUH3p+vTU/1VjyMPLxz///ZxjSCU3/QZfrYC1oMyKo9bDq6tdBNRATZVk3WfMl7pAtIhwGtosgjVCUAjOetVOpANQu0Fw1jnCa5tjoo5Ds2RyOJfMnXyN+Kfvy/vq+goQ1A5UVccJxztz5M4hLWWbM58EC9yXADe1xBJrEIFRIKN2IpNZqfIzGOXZL9bru5mU5goAtu9cvs3q79z2Q1/Dg/nK2dmbPVBc6biY4UjqCC8SQyhCS4WZetDldxEBEkAwmRnlDfIXClMbjrnxLicspomckmvlGUKEBVSqvYeSL7VyfXbnYxcRWsGCtEomaNmHadCqWPodC+vln6ilwRRpFvvw0ugsfru4wRh/pqU4ceB/fcexz644pxiSSo23VQqLSKy8o/qj6OqQJvCgTgmcGZF5abykGWJaQ5IbDFlmQsO5K+ISXNJw9lr3XGbou5NuziPNkHaJIVqUKCp/cXMC873BKEEyIq7MptvNfV7+4nyQNIVf+vHouOQsMATQDJNXY7BSlUgxGORZagu/WZh96JWNM/z/f7+ryeKCEqHlZfmz3p//WVddXq93fV9et/xZIvn8z1r3nFeZ+wj6ElN7bqtV58GGCHdduw2OUeEjUi0CYF3cwjOzTn5C62VK5GyX1+ldu7k/vDcop/1ETPmlxvHap0FY9wud7KYu+EZuA4aFZsSZTFPXFaOkQe4cEg7XRKcb0rm4Gf7rKo4hir5w6ormpEgNpJhduB115gsgNcDNcnH4S6XsyV8Kxgb/iXMQSjaW/wcaEsvGjOMXYgAAAABJRU5ErkJggg==';
    img.onload = () => this.props.connectDragPreview(img);
  }

Обновляем страницу и проверяем.

React, Drag&Drop и performance - 4

Так, теперь все работает. Но коня, конечно, необходимо убрать. Заменим его на прозрачный пиксель.

Убираем коня

componentDidMount() {
    const img = new Image();
    img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAMAAAAoyzS7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6REY0OTM1NzczOTYzMTFFN0E3MDBBMUQ5N0U1QzgxMkIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6REY0OTM1NzYzOTYzMTFFN0E3MDBBMUQ5N0U1QzgxMkIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmRpZDpGQkE5MURDNjYzMzlFNzExOTBGM0IxRjlDRDFERkM0RCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpGQkE5MURDNjYzMzlFNzExOTBGM0IxRjlDRDFERkM0RCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PrL7xCIAAAAGUExURf///wAAAFXC034AAAABdFJOUwBA5thmAAAADElEQVR42mJgAAgwAAACAAFPbVnhAAAAAElFTkSuQmCC';
    img.onload = () => this.props.connectDragPreview(img);
  }

Интересное замечание. Если в Windows выбрать упрощенную цветовую схему «Windows Classic», то в браузере не будет отображаться тень (preview) при перемещении от drag&drop.

React, Drag&Drop и performance - 5

после установки прозрачного превью

Уже лучше, однако, все равно похоже на дешевую подделку. Что ж, будем реализовывать свое превью, чтобы не зависеть от особенностей браузеров и ОС.

В React-DnD для этого предусмотрен DragLayer — компонент, который будет отображаться при перемещении DragSource.

Листинг GridDragLayer

import React, { Component } from 'react';
import { DragLayer } from 'react-dnd';
import SubjectContent from './SubjectContent';

function collect(monitor) {
  return {
    item: monitor.getItem(),
    currentOffset: monitor.getSourceClientOffset(),
    isDragging: monitor.isDragging(),
  };
}

function getItemTransform(props) {
  const { currentOffset } = props;
  if (!currentOffset) {
    return {
      display: 'none',
    };
  }

  const { x, y } = currentOffset;
  const transform = `translate(${x}px, ${y}px) rotate(3deg)`;
  return {  
    position: 'fixed', 
    display: 'block',
    zIndex: 10000,
    transform: transform,
    WebkitTransform: transform,
    cursor: 'move',
  };
}

class GridDragLayer extends Component {
  constructor(props) {
    super(props);
    this.lastUpdate = +new Date();
  }

render() {
    const { item, isDragging } = this.props;

    if (!isDragging) {
      return null;
    }

    return (
      <div
        id="drag-placeholder"
        style={getItemTransform(this.props)}
      >
        <SubjectContent
          data={item.data}
          isDragging={isDragging}
        />
      </div>
    );
  }
}

export default DragLayer(collect)(GridDragLayer);

Помещаем наш GridDragLayer в метод ScheduleGrid.render

Листинг ScheduleGrid

render() {
    return (
      <div>
        <GridDragLayer />
        <div className="table-schedule table-schedule--days clearfix">
          {this.printColumns()}          
        </div>
        <div className="navigation-table-day">
          <ScrollButton type={'prev'} handleClick={this.scrollLeft} />
          <ScrollButton type={'next'} handleClick={this.scrollRight} />
        </div>         
      </div>
    );
  }

В очередной раз проверяем.

React, Drag&Drop и performance - 6

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

В Chrome DevTools переходим на вкладку Performance и включаем CPU 4x slowdown. Видим примерную картину того как будет работать у обычного пользователя.

React, Drag&Drop и performance - 7

Решаем проблему производительности

shouldComponentUpdate

Основная проблема заключается в том, что на каждое событие drag (а триггерится оно очень часто) React перерисовывает компонент GridDragLayer. Выполняется большое число ненужных операций.

Чтобы избавится от лишних перерисовок реализуем метод shouldComponentUpdate в GridDragLayer. Для плавности нам нужно обеспечить 60fps, т.е. одна перерисовка на 16 мс.

Листинг shouldComponentUpdate

constructor(props) {
    super(props);
    this.lastUpdate = +new Date();
    this.updateTimer = null;
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (+new Date() - this.lastUpdate > 16) {
      this.lastUpdate = +new Date();
      clearTimeout(this.updateTimer);
      return true;
    } else {
      this.updateTimer = setTimeout(() => {
        this.forceUpdate();
      }, 100);
    }
    return false;
  }

Возможные «залипания», когда компонент изменил свое состояние в интервале 16 мс, но не был перерисован, устраняются таймером и forceUpdate.

Проверяем при CPU 4x slowdown. Стало намного шустрее, но все равно недостаточно.

React, Drag&Drop и performance - 8

Реализуем drag placeholder на vanilla js

Для этого немного допишем наш SubjectItem. Реализуем функцию generatePlaceholder, которая возвращает нам разметку элемента. Также напишем обработчик createMouseMoveHandler, который будет изменять положение placeholder. В объект subjectSource добавим event listeners, которые будут реагировать на событие dragover.

Листинг SubjectItem

import React, { Component } from 'react';
import { DragSource } from 'react-dnd';
import classNames from 'classnames';
import equals from 'shallow-equals';
import SubjectContent from './SubjectContent';

import throttle from '../utils/throttle.js';

function generatePlaceholder(item) {
  const placeholder = document.createElement('div');
  placeholder.id = 'drag-placeholder';
  placeholder.style.cssText =
    'display:none;position:fixed;z-index:100000;pointer-events:none;';
  
  placeholder.innerHTML = `<span class="items">
					                    <span class="item subject">			                        
					                        ${item.data.SUBJECT_NAME || '(нет)'}
					                    </span>
					                    <span class="item teacher">
					                        ${item.data.TEACHER_SHORT_NAME || '(нет)'}
					                    </span>
					                    <span class="item lecture">
					                        ${item.data.CLASSROOM_NAME || '(нет)'}
					                    </span>
					                </span>`;
  return placeholder;
}

function createMouseMoveHandler() {
  let currentX = -1;
  let currentY = -1;

  return function(event) {
    let newX = event.clientX - 8;
    let newY = event.clientY - 2;

    if (currentX === newX && currentY === newY) {
      return;
    }

    const dragPlaceholder = document.getElementById('drag-placeholder');
    const transform = 'translate(' + newX + 'px, ' + newY + 'px) rotate(3deg)';

    dragPlaceholder.style.transform = transform;
    dragPlaceholder.style.display = 'block';
  };
}

const mouseMoveHandler = createMouseMoveHandler();

const throttledMoveHandler = throttle(createMouseMoveHandler(), 16);

const subjectSource = {
  beginDrag(props, monitor, component) {
    document.addEventListener('dragover', throttledMoveHandler);
    document.body.insertBefore(
      generatePlaceholder(props),
      document.body.firstChild
    );
    return props;
  },
  endDrag(props, monitor, component) {
    document.removeEventListener('dragover', throttledMoveHandler);
    let child = document.getElementById('drag-placeholder');
    child.parentNode.removeChild(child);

    if (!monitor.didDrop()) {
      return;
    }
    const item = monitor.getItem();
    const dropResult = monitor.getDropResult();

    props.moveSubject(item.data.ID, {
      day: dropResult.xPos,
      period: dropResult.yPos,
    });
  },
};

function collect(connect, monitor) {
  return {
    connectDragSource: connect.dragSource(),
    isDragging: monitor.isDragging(),
    connectDragPreview: connect.dragPreview(),
  };
}

class SubjectItem extends Component {

  componentDidMount() {
    const img = new Image();
    img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAMAAAAoyzS7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6REY0OTM1NzczOTYzMTFFN0E3MDBBMUQ5N0U1QzgxMkIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6REY0OTM1NzYzOTYzMTFFN0E3MDBBMUQ5N0U1QzgxMkIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmRpZDpGQkE5MURDNjYzMzlFNzExOTBGM0IxRjlDRDFERkM0RCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDpGQkE5MURDNjYzMzlFNzExOTBGM0IxRjlDRDFERkM0RCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PrL7xCIAAAAGUExURf///wAAAFXC034AAAABdFJOUwBA5thmAAAADElEQVR42mJgAAgwAAACAAFPbVnhAAAAAElFTkSuQmCC';
    img.onload = () => this.props.connectDragPreview(img);
  }
  
  onTooltipClick(event) {
    event.preventDefault();
    return false;
  }    

  render() {
    const { connectDragSource, isDragging, index, sectionData, data } = this.props;

    const itemClass = classNames({
      'technical-data': true,
      'l-separation': index === 0 && sectionData.length > 1,
    });

    return connectDragSource(
      <a
        href="#"
        style={{ opacity: isDragging ? 0 : 1 }}
        onClick={this.onTooltipClick}
        className={itemClass}
      >
        <div style={{ height: '100%' }}>
          <SubjectContent 
            data={data}
            isDragging={isDragging}
          />          
        </div>
      </a>
    );
  }
}

export default DragSource('subjectItem', subjectSource, collect)(SubjectItem);

Также проверяем все при CPU 4x slowdown.

React, Drag&Drop и performance - 9

Теперь все работает как надо!

Конечно, это отступление от good practices, ведь теперь мы полностью продублировали код нашего превью в функции generatePlaceholder. Зато интерфейсом стало пользоваться намного удобней и приятней.

Заключение

После обновления React до 16 версии наш интерфейс не стал работать быстрее. Поэтому мы остановились на варианте с placeholder на vanilla js, поскольку он заметно выигрывает по производительности.

Вот так все работает на production:

React, Drag&Drop и performance - 10

Надеемся, что наш опыт разработки интерфейсов с использованием drag&drop будет полезен и другим разработчикам.

Будем благодарны за ваши комментарии и участие в мини-опросе!

Автор: Владислав Петроченко

Источник [10]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/270369

Ссылки в тексте:

[1] https://habrahabr.ru/company/macte/blog/341726/: https://habrahabr.ru/company/macte/blog/341726/

[2] DraggableJS: https://shopify.github.io/draggable/

[3] dragula: https://bevacqua.github.io/dragula/

[4] interactjs.io: http://interactjs.io/

[5] React-DnD: https://github.com/react-dnd/react-dnd

[6] react-beautiful-dnd: https://github.com/atlassian/react-beautiful-dnd

[7] https://habrahabr.ru/company/edison/blog/339086/: https://habrahabr.ru/company/edison/blog/339086/

[8] HTML5-Backend: http://react-dnd.github.io/react-dnd/docs-html5-backend.html

[9] github: https://github.com/react-dnd/react-dnd-html5-backend/issues/57

[10] Источник: https://habrahabr.ru/post/344368/?utm_source=habrahabr&utm_medium=rss&utm_campaign=344368