React-dnd: Langsame Leistung von CustomDragLayer

Erstellt am 7. Dez. 2016  ·  29Kommentare  ·  Quelle: react-dnd/react-dnd

Gibt es einen Grund, warum der CustomDragLayer für mich eine schlechte Leistung liefert? Es ist wie dort niedrige fps, oder besser gesagt, das willkürliche Objekt, das ich herumschleppe, verzögert und sieht nicht glatt aus

Hilfreichster Kommentar

Es scheint, als hätte das React-dnd-html5-Backend eine schreckliche Leistung mit einem benutzerdefinierten DragLayer, während das React-dnd-touch-Backend eine gute Leistung hat.

Alle 29 Kommentare

Das habe ich auch erlebt. Das Problem dabei ist, dass selbst wenn man reine Komponenten rendert, die sich im Drag-Layer nicht ändern, die Offset-Änderung bei jeder einzelnen Mausbewegung zumindest einen Reaktionsabgleich auslöst. Dies ist sehr ineffizient, wenn man den üblichen Fall hat, in dem der Drag-Layer nur ein sich nicht änderndes benutzerdefiniertes Vorschauelement anzeigen soll.

Um das zu umgehen, habe ich das DragLayer.js dupliziert, das das Offset-Rendering für mich erledigt, und zwar auf hochperformante Weise, was bedeutet, dass ich den Stil des Containers geändert habe, der sich direkt in der Vorschau bewegt.

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

Diese kann dann wie folgt verwendet werden:

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

Die Leistungssteigerung ist sehr auffällig:

drag-layer-default

drag-layer-performant

Natürlich ist die Voreinstellung DragLayer noch flexibler. Meine performantere Implementierung ist nur schneller, weil sie einen Sonderfall behandelt. Aber ich denke, dieser Sonderfall ist sehr häufig.

@gaearon Ist eine so spezialisierte, aber viel performantere Implementierung für Sie interessant, um sie als Alternative in React-Dnd zu integrieren?

@choffmeister Hallo. Danke für deinen Beitrag. Ich habe es mir vor einem Tag kurz angesehen, aber erst damals eingehend betrachtet. Der von Ihnen gepostete Code scheint innerhalb Ihrer Implementierung sehr spezialisiert zu sein. Könnten Sie vielleicht auf die wichtigen Aspekte hinweisen, die dafür erforderlich sind? Und wie dieses CustomDragLayerRaw verwendet wird, wäre auch sehr vorteilhaft.

Ich bin mir nicht sicher, ob all diese Plugins, Bibliotheken und anderer Code, den Sie haben, Teil der Lösung sind oder nicht.

Danke

Ich habe herumexperimentiert und es geschafft, dass es funktioniert. Es scheint, dass der Inhalt, den Sie in Ihrem "Drag-Ghost-Ding" haben möchten, im Div "Offset" sein sollte. Ich weiß nicht, wie das am besten geht, aber in meinem Test habe ich einfach ein ähnliches div in den gezogenen Inhalt eingefügt. Ich muss das Drag-Layer-Element noch mit dem gezogenen Element identisch machen, aber das Übergeben einiger Daten und das Ändern des Zustands des Drag-Layer-Elements ist nicht allzu schwierig.

Die Performance ist sehr gut, funktioniert aber nur beim Ziehen außerhalb des Containers. Zieht man Kreise um das Item herum, ist die Performance auch so tadellos wie im vorigen Fall (dh man hat eine Liste mit 4 Items, zieht und hält die Maus im Bereich des 4. Items). Wenn Sie jedoch mit Ihrer Maus Kreise ziehen und über alle 4 Elemente ziehen, verzögert es immer noch wie zuvor, aber mit einer leichten fps-Verbesserung (trotzdem ist es immer noch verzögert). Aber diese Implementierung ist ein guter Anfang. Zumindest für die häufigsten Fälle, in denen Sie von einem Container in einen anderen ziehen möchten, bleibt die UX auf jeden Fall gut.

Was genau meinst du mit lagg:

  1. Sind die FPS zu niedrig, sodass es stotternd aussieht, bzw
  2. Bewegt sich das Element reibungslos, aber während der Bewegung bleibt es immer etwas hinter der Mausbewegung zurück?

Ich würde sagen beides

Bei weiterer Verwendung stelle ich fest, dass die Lösung Verzögerungen nicht wirklich vollständig beseitigt. Ich habe eine Karte Div, die ein paar Funktionen enthält. Ihre Lösung hat anfangs einen enormen fps-Boost im Vergleich zum Beispiel in den Dokumenten bereitgestellt, aber wenn ich mein div komplizierter mache, beginnt es zu verzögern, als hätte ich das nicht.

Ich habe keine Ahnung, ob seine semantische Benutzeroberfläche dies verursacht oder dnd reagiert. Wenn es einen anderen, optimaleren Weg gibt, um die Drag-Layer sehr performant zu machen, würde ich es wirklich gerne wissen.

Wenn jemand versuchen möchte, ob es auch für ihn verzögert, habe ich eine semantische UI-Karte mit vielen darin enthaltenen Elementen verwendet. Meine Platzhalterkarte hatte 2 Titel, 5 semantische „Etiketten“, 3 semantische „Symbole“ und ein Avatarbild.

Nach weiteren Tests mit diesem anderen Beispiel unter: http://react-trello-board.web-pal.com/ (seine Implementierung ist sehr ähnlich zu dem, was choffmeister zuvor gepostet hatte), erhalte ich immer noch Verzögerungen, wenn ich versuche, mein kompliziertes div zu ziehen . Ich bin froh, dass ich zu diesem Schluss gekommen bin, da ich nicht mehr mit Code experimentieren muss; das problem liegt bei meinem div, das sich leicht beheben lässt.

Ich erlebe dies immer noch ziemlich schlecht, wenn ich SEHR ähnlichen Code wie das Beispiel in der Dokumentation verwende. Es muss die Transformationsberechnung sein, die jede winzige Mausbewegung hat?

Hat sonst noch jemand eine Lösung dazu gefunden?

Ich hatte ein Leistungsproblem, sogar mit PureComponents. Die Verwendung der Funktion „Updates hervorheben“ im Chrome React Dev-Plug-in zur Diagnose scheint so, als ob alle meine Komponenten aktualisiert wurden, als die currentOffset -Prop geändert wurde. Um dies abzumildern, habe ich sichergestellt, dass der DragLayer, der diese Requisite weitergibt, nur die Drag-Vorschau selbst enthält, obwohl es bereits einen DragLayer auf der obersten Ebene der App gab, um zu verfolgen, welches Element gezogen wird.

Ich habe nicht versucht, die Lösung von @choffmeister anzupassen, aber es sieht so aus, als würde sie das Problem auch lösen. Ich denke, vielleicht sollte so etwas für die Kernbibliothek in Betracht gezogen werden, da ich mir nur vorstellen kann, dass dies ein häufiges Problem für alle ist, die Drag-Vorschauen implementieren.

Ich habe dafür gesorgt, dass der DragLayer, der diese Requisite weitergibt, nur die Drag-Vorschau selbst enthält, obwohl es bereits einen DragLayer auf der obersten Ebene der App gab

Ich bin mir nicht sicher, ob ich dir folge! Ich glaube, ich habe nur die eine benutzerdefinierte Drag-Ebene in meiner App!

Es ist ein spezifisches Problem für meinen Anwendungsfall – ein Drag-Layer wurde bereits verwendet, bevor eine benutzerdefinierte Drag-Vorschau benötigt wurde, um zu verfolgen, welches Element gezogen wurde. Ich hatte ursprünglich gedacht, dass es sinnvoll wäre, dieselbe Drag-Ebene für die benutzerdefinierte Drag-Vorschau zu verwenden, als ich sie hinzufügte, aber da sich dies auf der obersten Ebene meiner Anwendung befand, verursachte das Ändern der Requisiten dafür bei jeder leichten Bewegung eine Menge zusätzliche Komponenten-Updates.

Okay, danke.

Ich habe meine Drag-Layer auf eine einzige dumme Komponente reduziert, also ist es, soweit ich das beurteilen kann, die konstante x/y-Berechnung, die die Verzögerung verursacht.

Ich kann noch nicht ganz verfolgen, was @choffmeister oben macht, aber es sieht so aus, als müsste ich genauer hinschauen.

@gaearon Dies scheint ein ziemlich anständiges Problem für die Bibliothek zu sein. Irgendwelche Vorschläge, was die richtige Lösung sein könnte?

Es scheint, als hätte das React-dnd-html5-Backend eine schreckliche Leistung mit einem benutzerdefinierten DragLayer, während das React-dnd-touch-Backend eine gute Leistung hat.

Irgendwie offensichtlich, aber es hilft:

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

Eine andere Problemumgehung besteht darin, das react-throttle-render -Paket zu verwenden, um die Rendermethode des DragLayer zu drosseln.

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

Danke @aks427. Ich werde das versuchen und sehen, ob es sich weiter verbessert (unter Verwendung einer benutzerdefinierten Drag-Layer wie oben, was in den MEISTEN Fällen sehr geholfen hat, aber nicht in allen).

@dobesv Wechsel zu React-dnd-Touch-Backend löste das Problem. Ich habe auch React-Throttle-Render ausprobiert, scheint aber immer noch nicht gut zu sein.

Ja, ich weiß, dass das Touch-Backend funktioniert, aber es unterstützt keine Datei-Uploads per Drag & Drop, die ich verwenden möchte.

@dobesv wir verwenden https://react-dropzone.js.org/ für Drag-and-Drop-Dateien :) vielleicht ist es auch gut für dich.

@dobesv ignoriere meine Kommentare. React-Dropzone unterstützt nur Drop-Dateien.

aber für häufigere Szenarien, wenn die Datei hochgeladen werden muss, sollte die Datei außerhalb des Browsers sein. Wir brauchen nur Drop-Dateien, die in Ordnung sein sollten.

Irgendwie ein Kinderspiel, aber ich habe den performanten Drag-Layer-Fix verwendet und einen ausgegeben
VIEL Zeit beim Betrachten meines Codes, um die Anzahl der Renderings zu begrenzen
wird von der App aufgerufen (viele Schaltkomponente -> PureComponent) und
Am Ende musste ich nicht einmal den React-Throttle-Renderer verwenden.

Ich empfehle DEFINITIV, sich mit der Verwendung von PureComponent zu befassen und es zu versuchen
maximieren Sie Ihren Code, anstatt in diesem Thread nach einer einfachen Lösung zu suchen, wenn
Ihr render ist langsam! Ich werde mehr posten, wenn wir das Performant-Modell unter verbessern
doch alles!

>

@framerate Selbst bei Verwendung von PureComponents ist dies ein Problem, da die Ausführung des Abstimmungsalgorithmus bei jeder Mausbewegung teuer ist. Profilieren Sie einfach Ihre App mit Chrome DevTools und CPU-Drosselung auf das 4-fache, was eine realistische Verlangsamung für mobile Geräte der Mittelklasse darstellt.

Für die Nachwelt und alle anderen, die mit der Drag-Performance zu kämpfen haben und nicht viel Code ziehen möchten, wie in der @choffmeister- Lösung:

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 Vielen Dank für die tolle Antwort! Leider funktioniert die Lösung auf IE11 bei mir nicht. subscribeToOffsetChange scheint den Callback, den wir übergeben, nicht aufzurufen. Glücklicherweise konnte ich das Problem beheben, indem ich nicht subscribeToOffsetChange verwendete, sondern einfach die Übersetzungen innerhalb des Kollektors wie folgt festlegte:

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 Mir ist aufgefallen, dass renderComponent nicht definiert ist. War dies Teil einer größeren Datei? (React wird auch nicht importiert)

@choffmeister Haben Sie für spätere Versionen von dnd aktualisiert? Es scheint, dass Änderungen am Kontext Ihre Implementierung beschädigt haben, und ich wollte es ausprobieren und mit dem vergleichen, was @stellarhoof tat

@stellarhoof Vielen Dank für die tolle Antwort! Leider funktioniert die Lösung auf IE11 bei mir nicht. subscribeToOffsetChange scheint den Callback, den wir übergeben, nicht aufzurufen. Glücklicherweise konnte ich das Problem beheben, indem ich nicht subscribeToOffsetChange verwendete, sondern einfach die Übersetzungen innerhalb des Kollektors wie folgt festlegte:

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

Das hat bei mir funktioniert!
Ich verwende diese Version: 9.4.0

Die anderen Lösungen scheinen nur für ältere Versionen zu funktionieren.

Hallo Leute,

Versuchen Sie nicht, eine animierte CSS-Komponente ziehbar zu machen, oder entfernen Sie zumindest die Übergangseigenschaft, bevor Sie fortfahren.

Nachdem die Übergangseigenschaft onStart entfernt und wieder onEnd hinzugefügt wurde, funktionierte alles ordnungsgemäß

Für Leute, die Hooks (useDragLayer-Hook) verwenden und hier landen: Ich habe ein Ticket speziell zur Hook-Implementierung geöffnet, mit einem Workaround-Vorschlag hier: https://github.com/react-dnd/react-dnd/issues/2414

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen