React-dnd: CustomDragLayer 性能缓慢

创建于 2016-12-07  ·  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中。 我不知道执行此操作的最佳方法,但在我的测试中,我只是在拖动的内容中放置了一个类似的 div。 我还没有使拖动层项与被拖动项相同,但是传递一些数据并更改拖动层项的状态并不难。

性能非常好,虽然它只在拖出容器时才有效。 如果您在项目周围拖动圆圈,则性能也与前一种情况一样完美(即您有一个包含 4 个项目的列表,将鼠标拖动并保持在第 4 个项目的区域)。 但是,如果您用鼠标在所有 4 个元素上拖动绘制圆圈,它仍然像以前一样滞后,但 fps 略有提高(尽管它仍然滞后)。 但是这个实现是一个好的开始。 至少对于最常见的情况,当您想从一个容器拖动到另一个容器时,这肯定会保持用户体验良好。

laggy 到底是什么意思:

  1. FPS 是否太低以至于看起来卡顿,或者
  2. 元素是否平滑移动,但在移动时它总是在鼠标移动之后有一段距离?

我会说两者

进一步使用后,我意识到该解决方案并没有真正完全消除滞后。 我有一个卡片 div,里面有一些安静的功能。 与文档中的示例相比,您的解决方案最初确实提供了巨大的 fps 提升,但是,当我使我的 div 更复杂时,它开始像我没有这个一样滞后。

我不知道它的语义 ui 是否会导致此问题或反应 dnd。 如果有任何其他更优化的方式让拖动层非常高效,我真的很想知道。

此外,如果有人想尝试看看它是否也落后于他们,我使用了语义 UI 卡,里面有很多元素。 我的占位卡有 2 个标题、5 个语义“标签”、3 个语义“图标”和一个头像图像。

在对这个不同的示例进行进一步测试后: http ://react-trello-board.web-pal.com/(它的实现与 choffmeister 之前发布的非常相似),当我尝试拖动我的复杂 div 时仍然会出现延迟. 我很高兴这是我得出的结论,因为我不需要对代码做更多的试验; 问题出在我的 div 上,可以轻松修复。

我仍然在使用与文档中的示例非常相似的代码时遇到这种情况。 一定是每次微小鼠标移动的变换计算?

有没有其他人找到解决这个问题的方法?

即使使用 PureComponents,我也遇到了性能问题。 当currentOffset属性发生变化时,使用 Chrome React Dev 插件中的“突出显示更新”功能进行诊断似乎我的所有组件都在更新。 为了减轻影响,我确保传递此道具的 DragLayer 仅包含拖动预览本身,即使应用程序的顶层已经有一个 DragLayer 来跟踪正在拖动的项目。

我还没有尝试调整@choffmeister的解决方案,但看起来它也可以解决问题。 我认为也许应该为核心库考虑类似的事情,因为我只能想象这对于任何实现拖动预览的人来说都是一个常见问题。

我确保传递这个道具的 DragLayer 只包含拖动预览本身,即使应用程序的顶层已经有一个 DragLayer

我不确定我是否跟随! 我相信我的应用程序中只有一个自定义拖动层!

这是一个特定于我的用例的问题——在需要自定义拖动预览之前已经使用了一个拖动层,以便跟踪正在拖动的项目。 我最初认为在添加它时使用相同的拖动层来进行自定义拖动预览是有意义的,但是由于这是我的应用程序的顶层,因此在每次轻微移动时更改此道具会导致很多额外的组件更新。

明白了,谢谢。

我将拖动层简化为单个哑组件,据我所知,这是导致延迟的恒定 x/y 计算。

我还不太了解@choffmeister在上面所做的事情,但看起来我必须仔细看看。

@gaearon这对于lib来说似乎是一个相当不错的问题,关于正确修复的任何建议可能是什么?

似乎 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 解决了这个问题。 我也尝试过 react-throttle-render,但似乎仍然不太好。

是的,我知道触摸后端可以工作,但它不支持我想使用的拖放文件上传。

@dobesv我们使用 https://react-dropzone.js.org/ 拖放文件 :) 也许它对你也有好处。

@dobesv忽略我的评论。 react-dropzone 仅支持放置文件。

但是对于更常见的场景,如果需要上传文件,则文件应该在浏览器之外。 我们只需要放置文件就可以了。

有点简单,但我使用了高性能的拖动层修复并花了
很多时间查看我的代码以限制渲染的数量
被应用程序调用(大量切换组件 - > PureComponent)和
最后,我什至不需要使用 react-throttle-render。

我绝对建议研究使用 PureComponent 并尝试
最大化你的代码,而不是在这个线程上寻找一个简单的修复,如果
你的渲染很慢! 如果我们改进 Performance 模型,我会发布更多信息
尽管如此!

>

@framerate即使使用 PureComponents 这也是一个问题,因为在每次鼠标移动时运行协调算法都很昂贵。 只需使用 Chrome DevTools 和 CPU 节流到 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 并将其添加回 onEnd 后,一切正常

对于使用钩子(useDragLayer 钩子)并在此处着陆的人:我打开了一张专门关于钩子实现的票,并在此处提供了解决方法建议: https ://github.com/react-dnd/react-dnd/issues/2414

此页面是否有帮助?
0 / 5 - 0 等级