React-dnd: Desempenho lento de CustomDragLayer

Criado em 7 dez. 2016  ·  29Comentários  ·  Fonte: react-dnd/react-dnd

Existe algum motivo pelo qual o CustomDragLayer para mim está me dando um desempenho ruim? É como se houvesse fps baixo, ou melhor, o objeto arbitrário que estou arrastando está atrasado e não parece suave

Comentários muito úteis

Parece que o react-dnd-html5-backend tem um desempenho terrível com um DragLayer personalizado, enquanto o react-dnd-touch-backend tem um desempenho OK.

Todos 29 comentários

Eu também experimentei isso. O problema aqui é que, mesmo se você renderizar componentes puros que não mudam na camada de arrastar, a mudança de deslocamento aciona uma reconciliação de reação a cada movimento do mouse. Isso é muito ineficiente se houver o caso comum, em que a camada de arrasto deve exibir apenas um elemento de visualização personalizado inalterável.

Para contornar isso, dupliquei o DragLayer.js que faz a renderização de deslocamento para mim e isso de uma maneira de alto desempenho, ou seja, alterando o estilo do contêiner que se move diretamente na visualização.

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

Isso pode então ser usado da seguinte maneira:

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

A melhoria de desempenho é muito perceptível:

drag-layer-default

drag-layer-performant

Claro que o padrão DragLayer ainda é mais flexível. Minha implementação mais eficiente é apenas mais rápida, porque lida com um caso especial. Mas acho que esse caso especial é muito comum.

@gaearon Uma implementação tão especializada, mas com muito mais desempenho, é interessante para você ser integrada como alternativa ao react-dnd?

@choffmeister Olá. Obrigado por sua postagem. Eu dei uma breve olhada nele um dia atrás, mas só olhei em profundidade naquele momento. O código que você postou parece muito especializado em sua implementação, você poderia apontar os aspectos importantes necessários para isso? E como esse CustomDragLayerRaw é usado também seria muito benéfico.

Não tenho certeza se todos esses plugins, bibliotecas e outros códigos que você possui fazem parte da solução ou não.

Obrigado

Eu experimentei ao redor e consegui fazê-lo funcionar. Parece que o conteúdo que você deseja em sua "coisa de drag ghost" deve estar no div 'offset'. Não sei a maneira ideal de fazer isso, mas no meu teste, apenas coloquei uma div semelhante ao conteúdo arrastado. Eu ainda tenho que fazer o item da camada de arrastar o mesmo que o item arrastado, mas passar alguns dados e alterar o estado do item da camada de arrastar não é muito difícil de fazer.

O desempenho é muito bom, embora funcione apenas ao arrastar para fora do contêiner. Se você arrastar em círculos ao redor do item, o desempenho também é tão impecável quanto no caso anterior (ou seja, você tem uma lista de 4 itens, arrastando e mantendo o mouse na área do 4º item). No entanto, se você desenhar círculos com o mouse, arrastando, por todos os 4 elementos, ainda ficará lento, como antes, mas com uma ligeira melhoria de fps (ainda assim, ainda está lento). Mas esta implementação é um bom começo. Pelo menos para os casos mais comuns em que você deseja arrastar de um contêiner para outro, isso definitivamente manterá o UX bom.

O que exatamente você quer dizer com lag:

  1. O FPS está tão baixo que parece engasgar, ou
  2. o elemento está se movendo suavemente, mas enquanto o move está sempre a uma certa distância do movimento do mouse?

eu diria os dois

Após mais uso, percebo que a solução não elimina completamente o atraso. Eu tenho um cartão div que tem um pouco de recursos silenciosos nele. Sua solução inicialmente forneceu um grande aumento de fps em comparação com o exemplo nos documentos, no entanto, à medida que eu complico meu div, ele começa a ficar lento como se eu não tivesse isso.

Não tenho ideia se é ui semântica causando isso ou reagir dnd. Se houver alguma outra maneira mais ideal de ter a camada de arrasto com muito desempenho, eu realmente gostaria de saber.

Além disso, se alguém quiser tentar ver se também está atrasado, usei um cartão de interface do usuário semântico, com muitos elementos dentro dele. Meu cartão de espaço reservado tinha 2 títulos, 5 "rótulos" semânticos, 3 "ícones" semânticos e uma imagem de avatar.

Após mais testes com este exemplo diferente em: http://react-trello-board.web-pal.com/ (sua implementação é muito semelhante ao que o choffmeister postou anteriormente), ainda obtenho lag ao tentar arrastar meu div complicado . Fico feliz que esta seja a conclusão a que cheguei, pois não preciso fazer mais experimentos com código; o problema está no meu div, que pode ser facilmente corrigido.

Ainda estou experimentando isso muito mal usando código MUITO semelhante ao exemplo nos documentos. Deve ser o cálculo de transformação com cada pequeno movimento do mouse?

Alguém mais encontrou uma solução para isso?

Eu tive um problema de desempenho, mesmo com PureComponents. Usar o recurso 'Destacar atualizações' no plug-in Chrome React Dev para diagnosticar parece que todos os meus componentes estavam sendo atualizados quando o prop currentOffset estava sendo alterado. Para mitigar, certifiquei-me de que o DragLayer que passa neste prop contém apenas a visualização de arrastar em si, mesmo que já houvesse um DragLayer no nível superior do aplicativo para rastrear qual item está sendo arrastado.

Eu não tentei adaptar a solução do @choffmeister , mas parece que também resolveria o problema. Acho que talvez algo assim deva ser considerado para a biblioteca principal porque só posso imaginar que esse é um problema comum para quem implementa visualizações de arrastar.

Certifiquei-me de que o DragLayer que passa neste prop contenha apenas a própria visualização de arrastar, mesmo que já houvesse um DragLayer no nível superior do aplicativo

Não tenho certeza se estou seguindo! Eu só tenho uma camada de arrastar personalizada no meu aplicativo, eu acredito!

É um problema específico do meu caso de uso – uma camada de arrastar já estava sendo usada antes que uma visualização de arrastar personalizada fosse necessária, para rastrear qual item estava sendo arrastado. Inicialmente, pensei que fazia sentido usar a mesma camada de arrastar para fazer a visualização personalizada de arrastar quando a adicionei, mas, como isso estava no nível superior do meu aplicativo, alterar os adereços para isso em cada movimento leve estava causando muito atualizações de componentes extras.

Entendi, obrigado.

Simplifiquei minha camada de arrasto para um único componente burro, então, até onde posso dizer, é o cálculo x/y constante que está causando o atraso.

Eu ainda não sigo o que @choffmeister está fazendo acima, mas parece que vou ter que dar uma olhada mais de perto.

@gaearon Este parece ser um problema bastante decente para a lib, alguma sugestão sobre qual poderia ser a correção adequada?

Parece que o react-dnd-html5-backend tem um desempenho terrível com um DragLayer personalizado, enquanto o react-dnd-touch-backend tem um desempenho OK.

Meio óbvio, mas ajuda:

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

Outra solução é usar o pacote react-throttle-render para acelerar o método de renderização do DragLayer.

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

Obrigado @aks427 Vou tentar isso e ver se melhora ainda mais (usando uma camada de arrastar personalizada como acima também, o que ajudou muito na MAIORIA dos casos, mas não em todos)

@dobesv mudança para react-dnd-touch-backend resolveu o problema. Eu também tentei react-throttle-render, mas ainda não parece bom.

Sim, eu sei que o back-end de toque funciona, mas não suporta uploads de arquivos de arrastar e soltar, que eu quero usar.

@dobesv usamos https://react-dropzone.js.org/ para arrastar e soltar arquivos :) talvez seja bom para você também.

@dobesv ignore meus comentários. react-dropzone suporta apenas arquivos drop.

mas para um senário mais comum, se o arquivo precisar ser carregado, o arquivo deve estar fora do navegador. precisamos apenas soltar arquivos deve estar ok.

Meio que um acéfalo, mas eu usei a correção da camada de arrastar de alto desempenho e gastei um
MUITO tempo olhando meu código para limitar o número de renderizações que foram
sendo chamado pelo aplicativo (muita troca de Componente -> PureComponent) e
no final nem precisei usar o react-throttle-render.

EU DEFINITIVAMENTE recomendo olhar para o uso do PureComponent e tentar
maximize seu código em vez de procurar uma correção fácil neste tópico se
sua renderização está lenta! Postarei mais se melhorarmos o modelo Performant em
tudo embora!

>

@framerate Mesmo usando PureComponents é um problema porque o algoritmo de reconciliação é caro para rodar em cada movimento do mouse. Basta criar o perfil do seu aplicativo com o Chrome DevTools e a aceleração da CPU para 4X, o que é uma desaceleração realista para dispositivos móveis de médio porte.

Para a posteridade e qualquer outra pessoa que esteja lutando com o desempenho do arrasto e você não deseja extrair muito código como na solução @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 Obrigado pela ótima resposta! Infelizmente, a solução não funciona no IE11 para mim. subscribeToOffsetChange não parece chamar o retorno de chamada que passamos para ele. Felizmente, consegui corrigi-lo não usando subscribeToOffsetChange , mas apenas definindo as traduções dentro do coletor assim:

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 Percebo que renderComponent não está definido. Isso fazia parte de um arquivo maior? (React não é importado também)

@choffmeister Você atualizou para versões posteriores do dnd? Parece que as mudanças no contexto interromperam sua implementação e eu queria experimentá-lo e comparar com o que @stellarhoof estava fazendo

@stellarhoof Obrigado pela ótima resposta! Infelizmente, a solução não funciona no IE11 para mim. subscribeToOffsetChange não parece chamar o retorno de chamada que passamos para ele. Felizmente, consegui corrigi-lo não usando subscribeToOffsetChange , mas apenas definindo as traduções dentro do coletor assim:

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

Isso funcionou para mim!
Estou usando esta versão: 9.4.0

As outras soluções parecem funcionar apenas para versões mais antigas.

Ei pessoal,

Não tente tornar um componente animado CSS arrastável ou, pelo menos, remova a propriedade de transição antes de continuar.

Depois de remover a propriedade de transição onStart e adicioná-la novamente onEnd, tudo funcionou corretamente

Para pessoas que usam hooks (useDragLayer hook) e chegam aqui: Abri um ticket especificamente sobre a implementação do hook, com uma proposta de solução alternativa aqui: https://github.com/react-dnd/react-dnd/issues/2414

Esta página foi útil?
0 / 5 - 0 avaliações