React-dnd: Rendimiento lento de CustomDragLayer

Creado en 7 dic. 2016  ·  29Comentarios  ·  Fuente: react-dnd/react-dnd

¿Hay alguna razón por la que CustomDragLayer me esté dando un mal rendimiento? Es como si hubiera fps bajos, o más bien, el objeto arbitrario que estoy arrastrando está retrasado y no se ve fluido

Comentario más útil

Parece que el backend de react-dnd-html5 tiene un rendimiento terrible con un DragLayer personalizado, mientras que el backend de react-dnd-touch tiene un rendimiento aceptable.

Todos 29 comentarios

Yo también experimenté esto. El problema aquí es que, incluso si renderiza componentes puros que no cambian en la capa de arrastre, el cambio de desplazamiento desencadena al menos una reconciliación de reacción en cada movimiento del mouse. Esto es muy ineficiente si uno tiene el caso común, donde la capa de arrastre solo debe mostrar un elemento de vista previa personalizado que no cambia.

Para evitar eso, dupliqué el DragLayer.js que hace la representación compensada por mí y esto de una manera de alto rendimiento, lo que significa cambiar el estilo del contenedor que se mueve directamente alrededor de la vista previa.

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

Esto se puede utilizar de la siguiente manera:

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

La mejora del rendimiento es muy notable:

drag-layer-default

drag-layer-performant

Por supuesto, el valor predeterminado DragLayer es aún más flexible. Mi implementación más eficaz es simplemente más rápida, porque maneja un caso especial. Pero supongo que este caso especial es muy común.

@gaearon ¿Es interesante para usted integrar una implementación tan especializada pero mucho más eficaz como alternativa en react-dnd?

@choffmeister Hola. Gracias por tu publicación. Lo miré brevemente hace un día, pero solo lo miré en profundidad en ese momento. El código que publicó parece muy especializado dentro de su implementación, ¿podría señalar los aspectos importantes que se necesitan para esto? Y la forma en que se usa CustomDragLayerRaw también sería muy beneficiosa.

No estoy seguro de si todos esos complementos, bibliotecas y otros códigos que tiene son parte de la solución o no.

Gracias

He experimentado y he conseguido que funcione. Parece que el contenido que desea en su "cosa fantasma de arrastre" debe estar en el div 'desplazamiento'. No conozco la forma óptima de hacer esto, pero en mi prueba, solo puse un div similar al contenido arrastrado. Todavía tengo que hacer que el elemento de la capa de arrastre sea el mismo que el elemento arrastrado, pero pasar algunos datos y cambiar el estado del elemento de la capa de arrastre no es demasiado difícil de hacer.

El rendimiento es muy bueno, aunque solo funciona cuando se arrastra fuera del contenedor. Si arrastra en círculos alrededor del elemento, el rendimiento también es tan impecable como en el caso anterior (es decir, tiene una lista de 4 elementos, arrastrando y manteniendo el mouse en el área del 4º elemento). Sin embargo, si dibuja círculos con el mouse, arrastrando, a través de los 4 elementos, todavía se retrasa, como antes, pero con una ligera mejora de fps (sin embargo, todavía está retrasado). Pero esta implementación es un buen comienzo. Al menos para los casos más comunes en los que desea arrastrar de un contenedor a otro, esto definitivamente mantendrá la UX buena.

¿Qué quiere decir exactamente con retraso:

  1. ¿El FPS es demasiado bajo para que parezca tartamudear, o
  2. ¿El elemento se mueve suavemente, pero mientras se mueve siempre está a cierta distancia detrás del movimiento del mouse?

Yo diría que ambos

Tras un uso posterior, me doy cuenta de que la solución realmente no elimina el retraso por completo. Tengo una tarjeta div que tiene un poco de funciones. Su solución inicialmente brindó un gran aumento de fps en comparación con el ejemplo en los documentos, sin embargo, a medida que hago que mi div sea más complicado, comienza a retrasarse como si no tuviera esto.

No tengo idea si es la interfaz de usuario semántica la que causa esto o reacciona dnd. Si hay alguna otra forma más óptima de hacer que la capa de arrastre tenga un gran rendimiento, realmente me gustaría saberlo.

Además, si alguien quisiera probar y ver si también se retrasa para ellos, había usado una tarjeta de interfaz de usuario semántica, con muchos elementos dentro de ella. Mi tarjeta de marcador de posición tenía 2 títulos, 5 "etiquetas" semánticas, 3 "iconos" semánticos y una imagen de avatar.

Después de más pruebas con este ejemplo diferente en: http://react-trello-board.web-pal.com/ (su implementación es muy similar a lo que choffmeister había publicado anteriormente), todavía obtengo un retraso cuando trato de arrastrar mi complicado div . Me alegro de que esta sea la conclusión a la que he llegado, ya que no necesito experimentar más con el código; el problema radica en mi div, que se puede solucionar fácilmente.

Todavía estoy experimentando esto bastante mal usando un código MUY similar al ejemplo en los documentos. ¿Debe ser el cálculo de transformación que tiene cada pequeño movimiento del mouse?

¿Alguien más ha encontrado una solución a esto?

He tenido un problema de rendimiento, incluso con PureComponents. Al usar la función 'Actualizaciones destacadas' en el complemento Chrome React Dev para diagnosticar, parece que todos mis componentes se estaban actualizando cuando se cambiaba el accesorio currentOffset . Para mitigar, me aseguré de que el DragLayer que pasa en este accesorio solo contenga la vista previa de arrastre, aunque ya había un DragLayer en el nivel superior de la aplicación para rastrear qué elemento se está arrastrando.

No he intentado adaptar la solución de @choffmeister pero parece que también resolvería el problema. Creo que tal vez debería considerarse algo así para la biblioteca principal porque solo puedo imaginar que este es un problema común para cualquiera que implemente vistas previas de arrastre.

Me aseguré de que el DragLayer que pasa en este accesorio solo contenga la vista previa de arrastre, aunque ya había un DragLayer en el nivel superior de la aplicación.

¡No estoy seguro de seguirlo! Creo que solo tengo una capa de arrastre personalizada en mi aplicación.

Es un problema específico de mi caso de uso: ya se estaba usando una capa de arrastre antes de que se necesitara una vista previa de arrastre personalizada, para rastrear qué elemento se estaba arrastrando. Inicialmente pensé que tenía sentido usar la misma capa de arrastre para hacer la vista previa de arrastre personalizada cuando la agregué, pero como esto estaba en el nivel superior de mi aplicación, cambiar los accesorios para esto en cada ligero movimiento estaba causando mucho actualizaciones de componentes adicionales.

Entiendo, gracias.

Simplifiqué mi capa de arrastre a un solo componente tonto, por lo que puedo decir, es el cálculo constante de x/y lo que está causando el retraso.

Todavía no sigo lo que @choffmeister está haciendo arriba, pero parece que voy a tener que mirar más de cerca.

@gaearon Este parece ser un problema bastante decente para la biblioteca, ¿alguna sugerencia sobre cuál podría ser la solución adecuada?

Parece que el backend de react-dnd-html5 tiene un rendimiento terrible con un DragLayer personalizado, mientras que el backend de react-dnd-touch tiene un rendimiento aceptable.

Un poco obvio, pero ayuda:

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

Otra solución es usar el paquete react-throttle-render para acelerar el método de renderizado de DragLayer.

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

Gracias @aks427 Lo intentaré y veré si mejora aún más (usando una capa de arrastre personalizada como la anterior también que ayudó mucho en la MAYORÍA de los casos, pero no en todos)

El cambio de @dobesv a react-dnd-touch-backend resolvió el problema. También probé react-throttle-render, pero todavía no parece bueno.

Sí, sé que el backend táctil funciona, pero no es compatible con la carga de archivos de arrastrar y soltar, que quiero usar.

@dobesv usamos https://react-dropzone.js.org/ para arrastrar y soltar archivos :) tal vez también sea bueno para usted.

@dobesv ignora mis comentarios. react-dropzone solo admite archivos drop.

pero para escenarios más comunes, si es necesario cargar el archivo, el archivo debe estar fuera del navegador. solo necesitamos soltar archivos, debería estar bien.

Algo así como una obviedad, pero utilicé la corrección de la capa de arrastre de alto rendimiento y pasé un
MUCHO tiempo mirando mi código para limitar la cantidad de renderizados que fueron
siendo llamado por la aplicación (un montón de componentes de conmutación -> PureComponent) y
al final ni siquiera necesité usar el renderizador de acelerador de reacción.

DEFINITIVAMENTE recomiendo considerar el uso de PureComponent y tratar de
maximizar su código en lugar de buscar una solución fácil en este hilo si
tu render es lento! Publicaré más si mejoramos el modelo Performant en
aunque todo!

>

@framerate Incluso usar PureComponents es un problema porque el algoritmo de reconciliación es costoso de ejecutar en cada movimiento del mouse. Simplemente perfile su aplicación con Chrome DevTools y la aceleración de la CPU a 4X, que es una desaceleración realista para los dispositivos móviles de gama media.

Para la posteridad y cualquier otra persona que esté luchando con el rendimiento de arrastre y no quiera extraer mucho código como en la solución @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 ¡ Gracias por la gran respuesta! Desafortunadamente, la solución no funciona en IE11 para mí. subscribeToOffsetChange no parece llamar a la devolución de llamada que le pasamos. Afortunadamente, pude solucionarlo al no usar subscribeToOffsetChange , sino simplemente configurar las traducciones dentro del recopilador de esta manera:

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 Veo que renderComponent no está definido. ¿Era esto parte de un archivo más grande? (React no se importa también)

@choffmeister ¿Ha actualizado para versiones posteriores de dnd? Parecería que los cambios en el contexto han roto su implementación y quería probarlo y compararlo con lo que estaba haciendo @stellarhoof

@stellarhoof ¡ Gracias por la gran respuesta! Desafortunadamente, la solución no funciona en IE11 para mí. subscribeToOffsetChange no parece llamar a la devolución de llamada que le pasamos. Afortunadamente, pude solucionarlo al no usar subscribeToOffsetChange , sino simplemente configurar las traducciones dentro del recopilador de esta manera:

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

¡Esto funcionó para mí!
Estoy usando esta versión: 9.4.0

Las otras soluciones parecen funcionar solo para versiones anteriores.

Hola chicos,

No intente hacer que un componente animado css se pueda arrastrar, o al menos elimine la propiedad de transición antes de continuar.

Después de eliminar la propiedad de transición onStart y volver a agregarla onEnd, todo funcionó correctamente

Para las personas que usan ganchos (gancho useDragLayer) y aterrizan aquí: abrí un ticket específicamente sobre la implementación del gancho, con una propuesta de solución alternativa aquí: https://github.com/react-dnd/react-dnd/issues/2414

¿Fue útil esta página
0 / 5 - 0 calificaciones