ianstormtaylor / slate

A completely customizable framework for building rich text editors. (Currently in beta.)
http://slatejs.org
MIT License
29.83k stars 3.24k forks source link

Dynamic Rendering Feature (performance improvement) #790

Open evanmarshall opened 7 years ago

evanmarshall commented 7 years ago

Do you want to request a feature or report a bug?

Feature.

The proposed idea is to dynamically render content within slate. This means that slate would only render visible blocks and not render blocks hidden within the y-overflow. The benefits that this would provide for performance are huge. By far the slowest part of the initial render is mounting all of the components (if I'm reading the timeline correctly). You could effectively bound the number of dom nodes rendered at any given time and perhaps find other optimizations as a result.

screen shot 2017-05-04 at 11 06 20 pm

slate-large.zip

What's the current behavior?

All of the components are rendered.

What's the desired behavior?

Only visible (with a given padding of elements to support smooth scrolling

The Ace Editor: https://github.com/ajaxorg/ace Does this by rendering two divs: a full height "scroller" div and then a "content" div that dynamic adjusts based on scrolls while updating it's content (removing hidden dom nodes and adding newly shown dom nodes).

Here's how the position of the content div updates: https://github.com/ajaxorg/ace/blob/master/lib/ace/virtual_renderer.js#L838 Here's how it calculates the properties of the content div: https://github.com/ajaxorg/ace/blob/master/lib/ace/virtual_renderer.js#L1025

Here's an example of dynamic rendering in react: https://github.com/clauderic/react-tiny-virtual-list

optimistiks commented 7 years ago

Interesting. I wonder how it's possible to preserve capability of selecting everything with Ctrl+A with that feature. It seems that in Ace editor all features are implemented from scratch (caret, selection, etc), and Slate is using contenteditable, so it may be more difficult to implement such feature in Slate.

evanmarshall commented 7 years ago

You could preserve Ctrl+A pretty easily by handling that case specifically. You could have a virtual selection and then a rendered selection. I think the larger question is to what degree would you have to implement features and handle edge cases.

This would seem to be a significant change but I think you would be able to handle most of the caret & selection with offsets.

ianstormtaylor commented 7 years ago

Hey @egmracer01 I think this sounds very interesting, going to leave it open for people to discuss how it might be done. I haven't done any virtualization with React before, so I have nothing to contribute right now.

I'd be curious to know which pieces of the rendering could be avoided with virtualization. i think some of that logic might not be avoided, and there might be lowering-hanging fruit elsewhere?

evanmarshall commented 7 years ago

one piece of lower hanging fruit might be related to: https://medium.com/missive-app/45-faster-react-functional-components-now-3509a668e69f . To summarize the problem, pure rendering components are simply wrappers around normal components and follow the same code paths. By calling the function Component(props) instead of <Component {...props} /> you can get a big speed improvement without consequences. This appears to be something that the react team is looking into already.

From my understanding, nodes with custom types are pure: for example: https://github.com/ianstormtaylor/slate/blob/master/examples/rich-text/index.js#L20 .

This means that when we render these components: https://github.com/ianstormtaylor/slate/blob/17ea3ed349963e91454b3d788a5242036565ab55/src/components/node.js#L272 assuming Component is pure, we actually could speed this up by changing it to something like:

Component({
        attributes,
        key: node.key,
        editor,
        parent,
        node,
        readOnly,
        state,
        children
})
ms88privat commented 7 years ago

I would wait for React 0.16.0 and then have a new evaluation on it.

thesunny commented 6 years ago

I just wanted to chime in here with a few points on what I believe is and isn't possible and a really good low hanging fruit IMO.

I don't think it's possible to arbitrarily render any part of a Slate Document at a given scroll position because it is difficult, possibly impossible, to tell how much space a Slate block will take until after it is rendered. Ace editor is an editor for monospace text so it is easy to predict how much space text will take before we render it.

That said, what could work and could improve performance, is to render the visible portion of the page first before giving control back to the user. The rest of the content could fill in afterwards.

I think this might be a good low hanging fruit. Just render up to the visible portion, then use setTimeout to render a few blocks at a time for the rest without blocking the UI.

This would give the appearance of being usable quickly on very long documents. The only thing you'd see is the scrollbar getting longer.

ianstormtaylor commented 6 years ago

@thesunny good points!

For anyone needing this, I would highly recommend looking into other performance improvements in the general rendering case first, because they are probably much lower-hanging fruit.

linonetwo commented 5 years ago

https://github.com/bvaughn/react-window Might be a solution to this, but it requires you to hand over the rendering to it:

/// fake example
import { VariableSizeList as List } from 'react-window';

function AFastEditor() {
  const getItemSize = index => editor.blocks[index].getHeight();

  // style is used to absolutely locate elements in the long parent
  const getBlock = ({ index, style }) => React.cloneElement(editor.blocks[index], { style });

  return (
    <List
      width={800}
      height={600}
      itemCount={editor.blocks.length}
      itemSize={getItemSize}
    >
      {getBlock}
    </List>
  );
}

I tried to use react-window today, here is the plugin I created, it can render huge document smoothly, though I don't know how to get the height of paragraph:

import React from 'react';
import { Mark, Value } from 'slate';
import { Set } from 'immutable';
import { VariableSizeList } from 'react-window';
import calculateSize from 'calculate-size';
import AutoSizer from 'react-virtualized-auto-sizer';

interface DeserializeProps {
  warperBlockType?: string;
  defaultBlock?: string;
  defaultMarks?: any[];
  delimiter?: string;
  toJSON?: boolean;
}

export function deserializeHugeText(inputString: string, props: DeserializeProps = {}): Value {
  let { defaultMarks = [] } = props;
  const { warperBlockType = 'huge-document', defaultBlock = 'line', delimiter = '\n' } = props;
  if (Set.isSet(defaultMarks)) {
    defaultMarks = defaultMarks.toArray();
  }

  defaultMarks = defaultMarks.map(Mark.create);

  const json = {
    object: 'value' as 'value',
    document: {
      object: 'document' as 'document',
      data: {},
      nodes: [
        {
          type: warperBlockType,
          object: 'block' as 'block',
          data: {},
          nodes: inputString.split(delimiter).map(line => {
            return {
              type: defaultBlock,
              object: 'block' as 'block',
              data: {},
              nodes: [
                {
                  object: 'text' as 'text',
                  text: line,
                  marks: defaultMarks,
                },
              ],
            };
          }),
        },
      ],
    },
  };

  return Value.fromJSON(json as any);
}

export const windowlongTextPlugin = ({
  warperBlockType = 'huge-document',
  warperTagName = 'article',
  fontSize = '18px',
  font = 'Arial',
} = {}) => ({
  renderBlock: (props, editor, next) => {
    const { attributes, children, node } = props;

    switch (node.type) {
      case warperBlockType: {
        const RenderRow = ({ index, style }) => <div style={style}>{children[index]}</div>;
        return (
          <AutoSizer>
            {({ height, width }) => (
              <VariableSizeList
                outerElementType={warperTagName}
                height={height}
                itemCount={children.length}
                itemSize={index =>
                  calculateSize(children[index].props.node.nodes.getIn(['0', 'text']), {
                    font,
                    fontSize,
                    width: `${width}px`,
                  }).height + 20
                }
                width={width}
              >
                {RenderRow}
              </VariableSizeList>
            )}
          </AutoSizer>
        );
      }
      default:
        return next();
    }
  },
});

/// And in the file using this plugin, give Editor a min-height
const StyledEditor = styled(Editor)`
  min-height: 100vh;
`;

But I found using this does not help slate to gain more performance when loading the ValueJSON, it is still quite slow.

linonetwo commented 5 years ago

Rendering is very fast, almost instantly, with or without react-window, and deserializeHugeText is also fast. But before rendering, loading it to slate is slow.

屏幕快照 2019-06-26 下午2 03 15

Slate spends lots of time doing normalize -> withoutNormalize -> normalizeNodeByPath -> setLength:

屏幕快照 2019-06-26 下午2 05 46

Maybe https://github.com/ianstormtaylor/slate/issues/2658 can solve this.

I'm curious why normalizeNodeByPath is called so many times on my every move, even when I'm just doing selection, annotation.


屏幕快照 2019-07-22 下午3 43 09

transformed.toArray() takes a long time.

mmdonaldson commented 5 years ago

Interested to know everyones thoughts on a deferred rendering method (Such as described in this article on Twitter Lite) https://medium.com/@paularmstrong/twitter-lite-and-high-performance-react-progressive-web-apps-at-scale-d28a00e780a3

Not as utopian as discussed above, but this could improve perceived rendering performance?

Attached from the article:

import hoistStatics from 'hoist-non-react-statics';
import React from 'react';

/**
 * Allows two animation frames to complete to allow other components to update
 * and re-render before mounting and rendering an expensive `WrappedComponent`.
 */
export default function deferComponentRender(WrappedComponent) {
  class DeferredRenderWrapper extends React.Component {
    constructor(props, context) {
      super(props, context);
      this.state = { shouldRender: false };
    }

    componentDidMount() {
      window.requestAnimationFrame(() => {
        window.requestAnimationFrame(() => this.setState({ shouldRender: true }));
      });
    }

    render() {
      return this.state.shouldRender ? <WrappedComponent {...this.props} /> : null;
    }
  }

  return hoistStatics(DeferredRenderWrapper, WrappedComponent);
}
const DeferredTimeline = deferComponentRender(HomeTimeline);
render(<DeferredTimeline />);
pgsill commented 4 years ago

@mmdonaldson I would absolutely love to see this. If the current Slate behaviors could be kept with deferred rendering it'd be fantastic.

shubham43MP commented 3 years ago

Any Updates to efficiently render Long documents with slate editor here? I think this is important from the UX perspective as the editing experience might be compromised for long documents if the Virtualisation is not there.

AliMamed commented 1 year ago

Hi all. I was experimenting with this at the renderElement prop of Editable in the following way:

What is the community best practice on this? Is this feature still under consideration, or maybe I am missing some point?

dylans commented 1 year ago

Hi all. I was experimenting with this at the renderElement prop of Editable in the following way:

* use IntersectionObserver to look if element which is direct child of editor is close enough to the viewport

* if not render a placeholder and don't render any children of this node
  Not rendering of children breaks some workflows in `slate-react`. (Looks like it looks for those nodes in weak-maps and crashes when can't find them in DOM)

What is the community best practice on this? Is this feature still under consideration, or maybe I am missing some point?

Still under consideration, but I don't think we've had a recent PR that would make this work. Would be happy to review or discuss further as it would be a nice improvement.

pau-not-paul commented 3 days ago

I have a proof of concept. Is there still interest in this?

https://github.com/user-attachments/assets/d4082ff0-e3f7-485a-a7aa-d7c3bcb9fcad

sensible-s commented 3 days ago

@pau-not-paul 100% still interest!