React-dnd: Performances lentes de CustomDragLayer

Créé le 7 déc. 2016  ·  29Commentaires  ·  Source: react-dnd/react-dnd

Y a-t-il une raison pour laquelle CustomDragLayer me donne de mauvaises performances ? C'est comme s'il y avait peu de fps, ou plutôt, l'objet arbitraire que je traîne est en retard et n'a pas l'air lisse

Commentaire le plus utile

Il semble que le react-dnd-html5-backend ait des performances terribles avec un DragLayer personnalisé alors que le react-dnd-touch-backend a des performances correctes.

Tous les 29 commentaires

J'ai également vécu cela. Le problème ici est que même si vous rendez des composants purs qui ne changent pas dans la couche de glissement, le changement de décalage déclenche au moins une réconciliation de réaction à chaque mouvement de souris. Ceci est très inefficace si l'on a le cas courant, où le calque de glissement doit simplement afficher un élément d'aperçu personnalisé non changeant.

Pour contourner ce problème, j'ai dupliqué le DragLayer.js le rendu décalé pour moi et cela de manière très performante, ce qui signifie changer le style du conteneur qui se déplace directement dans l'aperçu.

// 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);
  };
}

Celui-ci peut alors être utilisé de la manière suivante :

// 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)

L'amélioration des performances est très notable :

drag-layer-default

drag-layer-performant

Bien sûr, la valeur par défaut DragLayer est encore plus flexible. Mon implémentation plus performante est juste plus rapide, car elle gère un cas particulier. Mais je suppose que ce cas particulier est très courant.

@gaearon Une implémentation aussi spécialisée mais bien plus performante est-elle intéressante pour vous d'être intégrée comme alternative à react-dnd ?

@choffmeister Salut. Merci pour votre message. J'y ai jeté un bref coup d'œil il y a un jour, mais je ne l'ai regardé en profondeur qu'à ce moment-là. Le code que vous avez posté semble très spécialisé dans votre implémentation, pourriez-vous s'il vous plaît souligner les aspects importants nécessaires pour cela ? Et la façon dont CustomDragLayerRaw est utilisé serait également très bénéfique.

Je ne sais pas si tous ces plugins, bibliothèques et autres codes que vous avez font partie de la solution ou non.

Merci

J'ai expérimenté et j'ai réussi à le faire fonctionner. Il semble que le contenu que vous voulez dans votre "chose fantôme traînée" devrait être dans la div "décalage". Je ne connais pas la manière optimale de le faire, mais dans mon test, je viens de mettre une div similaire au contenu glissé. Je n'ai pas encore rendu l'élément de calque de glissement identique à l'élément glissé, mais transmettre certaines données et modifier l'état de l'élément de calque de glissement n'est pas trop difficile à faire.

Les performances sont très bonnes, même si cela ne fonctionne que lorsque vous faites glisser à l'extérieur du conteneur. Si vous faites des cercles autour de l'élément, la performance est également aussi impeccable que le cas précédent (c'est-à-dire que vous avez une liste de 4 éléments, en faisant glisser et en gardant la souris dans la zone du 4ème élément). Cependant, si vous dessinez des cercles avec votre souris, en faisant glisser, sur les 4 éléments, il est toujours à la traîne, comme avant, mais avec une légère amélioration du fps (néanmoins, il est toujours à la traîne). Mais cette implémentation est un bon début. Au moins pour les cas les plus courants lorsque vous souhaitez faire glisser d'un conteneur à un autre, cela maintiendra certainement l'UX en bon état.

Qu'entendez-vous exactement par lag :

  1. Le FPS est-il trop bas pour qu'il paraisse saccadé, ou
  2. l'élément se déplace-t-il en douceur, mais en se déplaçant, il est toujours à une certaine distance derrière le mouvement de la souris ?

je dirais les deux

Après une utilisation ultérieure, je me rends compte que la solution n'élimine pas vraiment le décalage. J'ai une carte div qui contient un peu de fonctionnalités. Votre solution a initialement fourni une énorme augmentation de fps par rapport à l'exemple de la documentation, cependant, à mesure que je complique ma div, elle commence à prendre du retard comme si je n'avais pas cela.

Je n'ai aucune idée si son interface utilisateur sémantique cause cela ou réagit dnd. S'il existe un autre moyen plus optimal d'avoir une couche de traînée très performante, j'aimerais vraiment le savoir.

De plus, si quelqu'un voulait essayer de voir si cela retardait également pour lui, j'avais utilisé une carte d'interface utilisateur sémantique, avec de nombreux éléments à l'intérieur. Ma carte d'espace réservé avait 2 titres, 5 "étiquettes" sémantiques, 3 "icônes" sémantiques et une image d'avatar.

Après d'autres tests avec cet exemple différent sur: http://react-trello-board.web-pal.com/ (son implémentation est très similaire à ce que choffmeister avait posté plus tôt), j'obtiens toujours un décalage lorsque j'essaie de faire glisser ma div compliquée . Je suis content que ce soit la conclusion à laquelle je sois parvenu, car je n'ai pas besoin d'expérimenter davantage avec le code; le problème réside dans ma div, qui peut être facilement résolue.

J'éprouve toujours cela assez mal en utilisant un code TRÈS similaire à l'exemple dans les docs. Ce doit être le calcul de transformation ayant chaque petit mouvement de souris ?

Quelqu'un d'autre a-t-il trouvé une solution à cela?

J'ai eu un problème de performance, même avec PureComponents. L'utilisation de la fonctionnalité "Mises à jour en surbrillance" dans le plug-in Chrome React Dev pour diagnostiquer semble que tous mes composants ont été mis à jour lorsque le prop currentOffset a été modifié. Pour atténuer, je me suis assuré que le DragLayer qui passe sur cet accessoire ne contient que l'aperçu du glisser lui-même, même s'il y avait déjà un DragLayer au niveau supérieur de l'application pour suivre quel élément est glissé.

Je n'ai pas essayé d'adapter la solution de @choffmeister mais il semble que cela résoudrait également le problème. Je pense que quelque chose comme ça devrait peut-être être envisagé pour la bibliothèque principale, car je ne peux qu'imaginer qu'il s'agit d'un problème courant pour quiconque implémente des aperçus de glissement.

Je me suis assuré que le DragLayer qui passe sur cet accessoire ne contient que l'aperçu du glisser lui-même, même s'il y avait déjà un DragLayer au niveau supérieur de l'application

Je ne suis pas sûr de suivre ! Je n'ai qu'un seul calque de glissement personnalisé dans mon application, je crois !

C'est un problème spécifique à mon cas d'utilisation - un calque de glissement était déjà utilisé avant qu'un aperçu de glissement personnalisé ne soit nécessaire, afin de suivre quel élément était déplacé. J'avais d'abord pensé qu'il était logique d'utiliser le même calque de glissement pour faire l'aperçu de glissement personnalisé lorsque je l'ai ajouté mais, comme c'était au niveau supérieur de mon application, changer les accessoires pour cela à chaque léger mouvement causait beaucoup de mises à jour de composants supplémentaires.

J'ai compris, merci.

J'ai simplifié mon calque de traînée en un seul composant stupide, donc pour autant que je sache, c'est le calcul constant x/y qui cause le décalage.

Je ne suis pas encore tout à fait ce que @choffmeister fait ci-dessus, mais il semble que je vais devoir y regarder de plus près.

@gaearon Cela semble être un problème assez décent pour la bibliothèque, des suggestions sur ce que pourrait être le correctif approprié?

Il semble que le react-dnd-html5-backend ait des performances terribles avec un DragLayer personnalisé alors que le react-dnd-touch-backend a des performances correctes.

Un peu évident, mais ça aide:

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

Une autre solution consiste à utiliser le package react-throttle-render pour limiter la méthode de rendu de DragLayer.

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

Merci @ aks427, je vais essayer cela et voir si cela s'améliore encore (en utilisant un calque de glissement personnalisé comme ci-dessus également, ce qui a beaucoup aidé dans la PLUPART des cas, mais pas tous)

@dobesv changer pour react-dnd-touch-backend a résolu le problème. J'ai aussi essayé react-throttle-render, mais cela ne semble toujours pas bon.

Oui, je sais que le backend tactile fonctionne, mais il ne prend pas en charge les téléchargements de fichiers par glisser-déposer, que je souhaite utiliser.

@dobesv nous utilisons https://react-dropzone.js.org/ pour les fichiers glisser-déposer :) peut-être que c'est bon pour vous aussi.

@dobesv ignore mes commentaires. react-dropzone ne prend en charge que les fichiers de dépôt.

mais pour un scénario plus courant, si le fichier doit être téléchargé, le fichier doit être hors du navigateur. nous n'avons besoin que de déposer des fichiers devrait être ok.

Un peu comme une évidence, mais j'ai utilisé le correctif performant de la couche de glissement et j'ai passé un
BEAUCOUP de temps à regarder mon code pour limiter le nombre de rendus qui ont été
être appelé par l'application (beaucoup de commutation Component -> PureComponent) et
à la fin je n'ai même pas eu besoin d'utiliser le render-throttle-render.

Je recommande DÉFINITIVEMENT d'utiliser PureComponent et d'essayer de
maximiser votre code au lieu de chercher une solution facile sur ce fil si
ton rendu est lent ! Je posterai plus si nous améliorons le modèle Performant à
tout pourtant !

>

@framerate Même en utilisant PureComponents, c'est un problème car l'algorithme de réconciliation coûte cher à exécuter à chaque mouvement de souris. Il suffit de profiler votre application avec Chrome DevTools et la limitation du processeur à 4X, ce qui est un ralentissement réaliste pour les appareils mobiles de milieu de gamme.

Pour la postérité et tous ceux qui ont du mal avec les performances de glisser et que vous ne voulez pas extraire beaucoup de code comme dans la solution @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 Merci pour la grande réponse! Malheureusement, la solution ne fonctionne pas sur IE11 pour moi. subscribeToOffsetChange ne semble pas appeler le rappel que nous lui transmettons. Heureusement, j'ai pu résoudre le problème en n'utilisant pas subscribeToOffsetChange , mais en définissant simplement les traductions à l'intérieur du collecteur comme suit :

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 Je remarque que renderComponent n'est pas défini. Cela faisait-il partie d'un fichier plus volumineux ? (React n'est pas importé non plus)

@choffmeister Avez-vous mis à jour les versions ultérieures de dnd ? Il semblerait que les changements de contexte aient cassé votre implémentation et je voulais l'essayer et comparer à ce que faisait @stellarhoof

@stellarhoof Merci pour la grande réponse! Malheureusement, la solution ne fonctionne pas sur IE11 pour moi. subscribeToOffsetChange ne semble pas appeler le rappel que nous lui transmettons. Heureusement, j'ai pu résoudre le problème en n'utilisant pas subscribeToOffsetChange , mais en définissant simplement les traductions à l'intérieur du collecteur comme suit :

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>
    );
  }
);

Cela a fonctionné pour moi!
J'utilise cette version : 9.4.0

Les autres solutions semblent ne fonctionner que pour les anciennes versions.

Salut les gars,

N'essayez pas de rendre un composant animé css déplaçable, ou au moins de supprimer la propriété de transition avant de continuer.

Après avoir supprimé la propriété de transition onStart et l'avoir rajoutée onEnd, tout a fonctionné correctement

Pour les personnes qui utilisent des hooks (hook useDragLayer) et atterrissent ici : j'ai ouvert un ticket spécifiquement sur l'implémentation du hook, avec une proposition de contournement ici : https://github.com/react-dnd/react-dnd/issues/2414

Cette page vous a été utile?
0 / 5 - 0 notes