React-dnd: Низкая производительность CustomDragLayer

Созданный на 7 дек. 2016  ·  29Комментарии  ·  Источник: react-dnd/react-dnd

Есть ли причина, по которой CustomDragLayer для меня дает плохую производительность? Это похоже на низкий fps, или, скорее, произвольный объект, который я перетаскиваю, отстает и не выглядит гладким.

Самый полезный комментарий

Похоже, что у react-dnd-html5-backend ужасная производительность с пользовательским DragLayer, тогда как у react-dnd-touch-backend нормальная производительность.

Все 29 Комментарий

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

Чтобы обойти это, я продублировал DragLayer.js , который выполняет рендеринг смещения для меня, и это высокопроизводительно, что означает изменение стиля контейнера, который перемещается непосредственно вокруг предварительного просмотра.

// PerformantDragLayer.js
/* eslint-disable */
import React, { Component, PropTypes } from 'react';
import shallowEqual from 'react-dnd/lib/utils/shallowEqual';
import shallowEqualScalar from 'react-dnd/lib/utils/shallowEqualScalar';
import isPlainObject from 'lodash/isPlainObject';
import invariant from 'invariant';
import checkDecoratorArguments from 'react-dnd/lib/utils/checkDecoratorArguments';
import hoistStatics from 'hoist-non-react-statics';

function layerStyles(isDragging) {
  return {
    position: 'fixed',
    pointerEvents: 'none',
    zIndex: 1,
    left: 0,
    top: 0,
    width: isDragging ? '100%' : 0,
    height: isDragging ? '100%' : 0,
    opacity: isDragging ? 1 : 0
  };
}

export default function DragLayer(collect, options = {}) {
  checkDecoratorArguments('DragLayer', 'collect[, options]', ...arguments);
  invariant(
    typeof collect === 'function',
    'Expected "collect" provided as the first argument to DragLayer ' +
    'to be a function that collects props to inject into the component. ',
    'Instead, received %s. ' +
    'Read more: http://gaearon.github.io/react-dnd/docs-drag-layer.html',
    collect
  );
  invariant(
    isPlainObject(options),
    'Expected "options" provided as the second argument to DragLayer to be ' +
    'a plain object when specified. ' +
    'Instead, received %s. ' +
    'Read more: http://gaearon.github.io/react-dnd/docs-drag-layer.html',
    options
  );

  return function decorateLayer(DecoratedComponent) {
    const { arePropsEqual = shallowEqualScalar } = options;
    const displayName =
      DecoratedComponent.displayName ||
      DecoratedComponent.name ||
      'Component';

    class DragLayerContainer extends Component {
      static DecoratedComponent = DecoratedComponent;

      static displayName = `DragLayer(${displayName})`;

      static contextTypes = {
        dragDropManager: PropTypes.object.isRequired
      }

      getDecoratedComponentInstance() {
        return this.refs.child;
      }

      shouldComponentUpdate(nextProps, nextState) {
        return !arePropsEqual(nextProps, this.props) ||
          !shallowEqual(nextState, this.state);
      }

      constructor(props, context) {
        super(props);
        this.handleOffsetChange = this.handleOffsetChange.bind(this);
        this.handleStateChange = this.handleStateChange.bind(this);

        this.manager = context.dragDropManager;
        invariant(
          typeof this.manager === 'object',
          'Could not find the drag and drop manager in the context of %s. ' +
          'Make sure to wrap the top-level component of your app with DragDropContext. ' +
          'Read more: http://gaearon.github.io/react-dnd/docs-troubleshooting.html#could-not-find-the-drag-and-drop-manager-in-the-context',
          displayName,
          displayName
        );

        this.state = this.getCurrentState();
      }

      componentDidMount() {
        this.isCurrentlyMounted = true;

        const monitor = this.manager.getMonitor();
        this.unsubscribeFromOffsetChange = monitor.subscribeToOffsetChange(
          this.handleOffsetChange
        );
        this.unsubscribeFromStateChange = monitor.subscribeToStateChange(
          this.handleStateChange
        );

        this.handleStateChange();
      }

      componentWillUnmount() {
        this.isCurrentlyMounted = false;

        this.unsubscribeFromOffsetChange();
        this.unsubscribeFromStateChange();
      }

      handleOffsetChange() {
        if (!this.isCurrentlyMounted) {
          return;
        }

        const monitor = this.manager.getMonitor();
        const offset = monitor.getSourceClientOffset();
        const offsetDiv = this.refs.offset;
        if (offset && offsetDiv) {
          offsetDiv.style.transform = `translate(${offset.x}px, ${offset.y}px)`;
        }
      }

      handleStateChange() {
        if (!this.isCurrentlyMounted) {
          return;
        }

        const nextState = this.getCurrentState();
        if (!shallowEqual(nextState, this.state)) {
          this.setState(nextState);
        }
      }

      getCurrentState() {
        const monitor = this.manager.getMonitor();
        return {
          collected: collect(monitor),
          isDragging: monitor.isDragging()
        };
      }

      render() {
        return (
          <div style={layerStyles(this.state.isDragging)}>
            <div ref='offset'>
              {this.state.isDragging && <DecoratedComponent {...this.props} {...this.state.collected} ref='child' />}
            </div>
          </div>
        );
      }
    }

    return hoistStatics(DragLayerContainer, DecoratedComponent);
  };
}

Затем это можно использовать следующим образом:

// MyCustomDragLayer.js
import PerformantDragLayer from './PerformantDragLayer'
import React, {PropTypes} from 'react'

class CustomDragLayerRaw extends React.Component {
  static propTypes = {
    item: PropTypes.any,
    itemType: PropTypes.string
  }

  render () {
    const {item, itemType} = this.props
    return <div>{itemType}</div>
  }
}

export default PerformantDragLayer((monitor) => ({
  item: monitor.getItem(),
  itemType: monitor.getItemType()
})(CustomDragLayerRaw)

Улучшение производительности очень заметно:

drag-layer-default

drag-layer-performant

Конечно, значение по умолчанию DragLayer еще более гибкое. Моя более производительная реализация просто быстрее, потому что она обрабатывает особый случай. Но я думаю, что этот частный случай очень распространен.

@gaearon Интересна ли вам такая специализированная, но гораздо более производительная реализация для интеграции в качестве альтернативы в react-dnd?

@choffmeister Привет. Спасибо за ваш пост. Я бегло просмотрел его день назад, но только тогда рассмотрел его подробно. Код, который вы разместили, кажется очень специализированным в вашей реализации, не могли бы вы указать важные аспекты, необходимые для этого? И то, как этот CustomDragLayerRaw используется, тоже было бы очень полезно.

Я не уверен, что все эти плагины, библиотеки и другой код являются частью решения или нет.

Спасибо

Я экспериментировал вокруг и сумел заставить его работать. Кажется, что контент, который вы хотите в своей «вещи-призраке», должен находиться в div «offset». Я не знаю оптимального способа сделать это, но в своем тесте я просто поместил аналогичный элемент div в перетаскиваемый контент. Мне еще предстоит сделать элемент слоя перетаскивания таким же, как перетаскиваемый элемент, но передать некоторые данные и изменить состояние элемента слоя перетаскивания не так уж сложно.

Производительность очень хорошая, хотя работает только при перетаскивании за пределы контейнера. Если вы перетаскиваете круги вокруг элемента, производительность также безупречна, как и в предыдущем случае (т.е. вы имеете список из 4 элементов, перетаскивая и удерживая мышь в области 4-го элемента). Однако, если вы рисуете круги с помощью мыши, перетаскивая все 4 элемента, он все еще лагает, как и раньше, но с небольшим улучшением частоты кадров (тем не менее, он все еще лагает). Но эта реализация — хорошее начало. По крайней мере, для наиболее распространенных случаев, когда вы хотите перетащить из одного контейнера в другой, это определенно сохранит хороший UX.

Что именно вы подразумеваете под отставанием:

  1. Является ли FPS настолько низким, что он выглядит заикающимся, или
  2. элемент движется плавно, но при движении он всегда отстает от движения мыши?

я бы сказал оба

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

Я понятия не имею, вызывает ли это семантический пользовательский интерфейс или реагирует dnd. Если есть какой-либо другой более оптимальный способ сделать слой перетаскивания очень производительным, мне бы очень хотелось знать.

Кроме того, если кто-то захочет попробовать и посмотреть, не отстает ли он для них, я использовал карту семантического пользовательского интерфейса с множеством элементов внутри. У моей карточки-заполнителя было 2 заголовка, 5 семантических «ярлыков», 3 семантических «значка» и изображение аватара.

После дальнейшего тестирования с этим другим примером по адресу: http://react-trello-board.web-pal.com/ (его реализация очень похожа на то, что ранее публиковал choffmeister), я все еще получаю отставание при попытке перетащить мой сложный div . Я рад, что пришел к такому выводу, поскольку мне больше не нужно экспериментировать с кодом; проблема заключается в моем div, который можно легко исправить.

Я все еще испытываю это довольно плохо, используя ОЧЕНЬ похожий код на пример в документах. Это должно быть вычисление преобразования с учетом каждого крошечного движения мыши?

Кто-нибудь еще нашел решение этого?

У меня были проблемы с производительностью, даже с PureComponents. Использование функции «Выделить обновления» в подключаемом модуле Chrome React Dev для диагностики кажется, что все мои компоненты обновлялись, когда изменялась поддержка currentOffset . Чтобы смягчить ситуацию, я убедился, что DragLayer, который передается этой опоре, содержит только сам предварительный просмотр перетаскивания, хотя на верхнем уровне приложения уже был DragLayer для отслеживания того, какой элемент перетаскивается.

Я не пробовал адаптировать решение @choffmeister , но похоже, что оно также решит проблему. Я думаю, возможно, что-то подобное следует рассмотреть для базовой библиотеки, потому что я могу только представить, что это общая проблема для всех, кто реализует предварительный просмотр перетаскиванием.

Я убедился, что DragLayer, который проходит через эту опору, содержит только сам предварительный просмотр перетаскивания, хотя на верхнем уровне приложения уже был DragLayer.

Я не уверен, что следую! У меня есть только один настраиваемый слой перетаскивания в моем приложении, я полагаю!

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

Попался, спасибо.

Я упростил свой слой перетаскивания до одного тупого компонента, так что, насколько я могу судить, причиной задержки является постоянное вычисление x/y.

Я пока не совсем понимаю, что делает @choffmeister выше, но, похоже, мне придется присмотреться повнимательнее.

@gaearon Кажется, это довольно приличная проблема для библиотеки, есть предложения по правильному исправлению?

Похоже, что у react-dnd-html5-backend ужасная производительность с пользовательским DragLayer, тогда как у react-dnd-touch-backend нормальная производительность.

Вроде очевидно, но помогает:

let updates = 0;
@DragLayer(monitor => {
  if (updates++ % 2 === 0) {
    return {
      currentOffset: monitor.getSourceClientOffset()
    }
  }
  else {
    return {}
  }
})

Другой обходной путь — использовать пакет react-throttle-render для регулирования метода рендеринга DragLayer.

https://www.npmjs.com/package/react-throttle-render

Спасибо @ aks427, я попробую это и посмотрю, улучшится ли оно дальше (с использованием пользовательского слоя перетаскивания, как указано выше, который очень помог в БОЛЬШИНСТВЕ случаев, но не во всех)

Изменение @dobesv на react-dnd-touch-backend решило проблему. Я также пробовал реагировать на дроссельную заслонку, но все равно не очень хорошо.

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

@dobesv мы используем https://react-dropzone.js.org/ для перетаскивания файлов :), возможно, это хорошо и для вас.

@dobesv игнорирует мои комментарии. react-dropzone поддерживает только перетаскиваемые файлы.

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

Вроде ежу понятно, но я использовал исправление производительного слоя перетаскивания и потратил
МНОГО времени просматривал мой код, чтобы ограничить количество рендеров, которые были
вызывается приложением (много переключений Component -> PureComponent) и
в конце концов мне даже не нужно было использовать react-throttle-render.

Я ОПРЕДЕЛЕННО рекомендую изучить использование PureComponent и попытаться
максимизируйте свой код вместо того, чтобы искать легкое исправление в этом потоке, если
ваш рендер работает медленно! Я опубликую больше, если мы улучшим модель Performance на
все же!

>

@framerate Даже при использовании PureComponents это проблема, потому что алгоритм согласования требует больших затрат для запуска при каждом движении мыши. Просто профилируйте свое приложение с помощью Chrome DevTools и дросселирования ЦП до 4X, что является реальным замедлением для мобильных устройств среднего уровня.

Для потомков и всех, кто борется с производительностью перетаскивания и не хочет тянуть много кода, как в решении @choffmeister :

let subscribedToOffsetChange = false;

let dragPreviewRef = null;

const onOffsetChange = monitor => () => {
  if (!dragPreviewRef) return;

  const offset = monitor.getSourceClientOffset();
  if (!offset) return;

  const transform = `translate(${offset.x}px, ${offset.y}px)`;
  dragPreviewRef.style["transform"] = transform;
  dragPreviewRef.style["-webkit-transform"] = transform;
};

export default DragLayer(monitor => {
  if (!subscribedToOffsetChange) {
    monitor.subscribeToOffsetChange(onOffsetChange(monitor));
    subscribedToOffsetChange = true;
  }

  return {
    itemBeingDragged: monitor.getItem(),
    componentType: monitor.getItemType(),
    beingDragged: monitor.isDragging()
  };
})(
  class CustomDragLayer extends React.PureComponent {
    componentDidUpdate(prevProps) {
      dragPreviewRef = this.rootNode;
    }

    render() {
      if (!this.props.beingDragged) return null;
      return (
        <div
          role="presentation"
          ref={el => (this.rootNode = el)}
          style={{
            position: "fixed",
            pointerEvents: "none",
            top: 0,
            left: 0
          }}
        >
          {renderComponent(
            this.props.componentType,
            this.props.itemBeingDragged
          )}
        </div>
      );
    }
  }
);

@stellarhoof Спасибо за отличный ответ! К сожалению, это решение не работает для меня в IE11. subscribeToOffsetChange , похоже, не вызывает обратный вызов, который мы передаем в него. К счастью, я смог исправить это, не используя subscribeToOffsetChange , а вместо этого просто установив переводы внутри сборщика следующим образом:

let dragLayerRef: HTMLElement = null;

const layerStyles: React.CSSProperties = {
  position: 'fixed',
  pointerEvents: 'none',
  zIndex: 100,
  left: 0,
  top: 0,
  width: '100%',
  height: '100%',
};

const dragLayerCollector = (monitor: DragLayerMonitor) => {
  if (dragLayerRef) {
    const offset = monitor.getSourceClientOffset() || monitor.getInitialClientOffset();

    if (offset) {
      dragLayerRef.style["transform"] = `translate(${offset.x}px, ${offset.y}px)`;
    } else {
      dragLayerRef.style["display"] = `none`;
    }
  }

  return {
    item: monitor.getItem(),
    isDragging: monitor.isDragging(),
  };
};

export default DragLayer(dragLayerCollector)(
  (props): JSX.Element => {
    if (!props.isDragging) {
      return null;
    }

    return (
      <div style={layerStyles}>
        <div ref={ (ref) => dragLayerRef = ref }>test</div>
      </div>
    );
  }
);

@stellarhoof Я заметил, что renderComponent не определено. Была ли это часть большого файла? (React также не импортируется)

@choffmeister Вы обновились для более поздних версий dnd? Казалось бы, изменения в контексте нарушили вашу реализацию, и я хотел попробовать и сравнить с тем, что делал @stellarhoof

@stellarhoof Спасибо за отличный ответ! К сожалению, это решение не работает для меня в IE11. subscribeToOffsetChange , похоже, не вызывает обратный вызов, который мы передаем в него. К счастью, я смог исправить это, не используя subscribeToOffsetChange , а вместо этого просто установив переводы внутри сборщика следующим образом:

let dragLayerRef: HTMLElement = null;

const layerStyles: React.CSSProperties = {
  position: 'fixed',
  pointerEvents: 'none',
  zIndex: 100,
  left: 0,
  top: 0,
  width: '100%',
  height: '100%',
};

const dragLayerCollector = (monitor: DragLayerMonitor) => {
  if (dragLayerRef) {
    const offset = monitor.getSourceClientOffset() || monitor.getInitialClientOffset();

    if (offset) {
      dragLayerRef.style["transform"] = `translate(${offset.x}px, ${offset.y}px)`;
    } else {
      dragLayerRef.style["display"] = `none`;
    }
  }

  return {
    item: monitor.getItem(),
    isDragging: monitor.isDragging(),
  };
};

export default DragLayer(dragLayerCollector)(
  (props): JSX.Element => {
    if (!props.isDragging) {
      return null;
    }

    return (
      <div style={layerStyles}>
        <div ref={ (ref) => dragLayerRef = ref }>test</div>
      </div>
    );
  }
);

Это сработало для меня!
Я использую эту версию: 9.4.0

Другие решения, похоже, работают только для более старых версий.

Привет ребят,

Не пытайтесь сделать анимированный компонент css перетаскиваемым или, по крайней мере, удалите свойство перехода, прежде чем продолжить.

После удаления свойства перехода onStart и добавления его обратно на End все заработало правильно

Для людей, которые используют хуки (useDragLayer hook) и приземляются здесь: я открыл тикет конкретно о реализации хука с предложением обходного пути здесь: https://github.com/react-dnd/react-dnd/issues/2414

Была ли эта страница полезной?
0 / 5 - 0 рейтинги