Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic Rendering Feature (performance improvement) #790

Open
evanmarshall opened this issue May 5, 2017 · 17 comments
Open

Dynamic Rendering Feature (performance improvement) #790

evanmarshall opened this issue May 5, 2017 · 17 comments

Comments

@evanmarshall
Copy link

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
Copy link

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
Copy link
Author

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
Copy link
Owner

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
Copy link
Author

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:

<Component
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
Copy link

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

@thesunny
Copy link
Collaborator

thesunny commented Dec 6, 2017

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
Copy link
Owner

@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
Copy link
Contributor

linonetwo commented Jun 25, 2019

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
Copy link
Contributor

linonetwo commented Jun 26, 2019

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 #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
Copy link
Contributor

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
Copy link

pgsill commented Nov 14, 2019

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

@shubham43MP
Copy link

shubham43MP commented Jul 15, 2021

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
Copy link

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?

@dylans
Copy link
Collaborator

dylans commented May 27, 2023

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
Copy link

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

slatejs.virtualization.POC.mov

@sensible-s
Copy link

@pau-not-paul 100% still interest!

@andyjakubowski
Copy link

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

Yes, definitely. Optimizing performance for long Slate documents has been an ongoing challenge in a project I’m involved in. Would you mind sharing your approach @pau-not-paul? Your PoC looks great.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests