nhn / tui.editor

🍞📝 Markdown WYSIWYG Editor. GFM Standard + Chart & UML Extensible.
http://ui.toast.com/tui-editor
MIT License
17.19k stars 1.75k forks source link

React Editor memory leak #1076

Open live951 opened 4 years ago

live951 commented 4 years ago

Describe the bug

When using the react-editor component memory does not appear to be freed when the component is unmounted. Every 10 mount/unmount cycles of the Editor appears to consistently increase memory usage on the heap (as reported by Chrome Developer Tools) by approximately 1.6MB even after triggering manual garbage collection.

The only scenario in which memory appears to be properly freed on multiple mount/unmount cycles is when the 'remove' method is called prior to unmounting the react-editor component--but only if no text has been typed into the editor. A single character typed into the Editor, and then calling the remove method before unmounting, results in a memory leak too.

To Reproduce

Steps to reproduce the behavior:

  1. Create basic React project with npx create-react-app and remove boilerplate leaving just index.js and App.js in src and install tui and all tui editor plugins via npm as described in tui documentation.
  2. Copy/paste the example code at the bottom of this bug report into App.js and index.js respectively.
  3. npm start to start development server and load the basic React app in Chrome (latest version). I closed and restarted Chrome to start with no other web pages loaded for testing. I did testing with a production build instead of development to see if it made any difference and it did not.
  4. Open Chrome Developer Tools and click on "Memory" tab. Under "Select profiling type" select "Heap snapshot" if not already selected by default. Press the "Take heap snapshot" button (it looks like a circle) 2-3 times until it reports a base amount of heap memory being used by the app that is consistent between taking snapshots.
  5. Press the "Toggle Editor" button once to mount the Editor, then again to unmount the editor and then repeat step 4. I take this as the 'starting' value for measuring subsequent memory leak issues.
  6. Return to the App and press the "Toggle Editor" button to show the Editor, then again to hide the Editor. Repeat this process 5 times (or however many times you would like, it seems the memory leak simply scales with the # of times the editor is mounted/unmounted).
  7. Repeat step 4 (for me it takes 3 snapshots before I get consistent heap measurements between subsequent snapshots). Compare this amount of memory used to the amount recorded in step 5 as the 'baseline' value.
  8. Repeat step 6 and 7 as many times as desired and note the ever-increasing memory usage in heap snapshots.
  9. (Alternative 1 to step 5, no memory leak): Press "Toggle Editor" to show the editor, then press the "Remove & Toggle Editor" button (do not input any text into Editor while doing this step). Notice that memory usage does not grow.
  10. (Alternative 2 to step 5, with memory leak): Follow step 9, but type some text into the Editor instead of not inputting any text. Notice how now the memory usage grows.

Expected behavior

Repeatedly mounting/unmounting the react-editor component, especially if first calling the remove method of the Editor instance, should not result in ever increasing memory usage on the heap (with or without having typed in the Editor).

Desktop (please complete the following information):

Sample code

Here is the App component I used for testing:

import "codemirror/lib/codemirror.css";
import "highlight.js/styles/github.css";
import "@toast-ui/editor/dist/toastui-editor.css";
import "tui-chart/dist/tui-chart.css";
import "tui-color-picker/dist/tui-color-picker.css";

import React from "react";
import { Editor } from "@toast-ui/react-editor";
import chart from "@toast-ui/editor-plugin-chart";
import codeSyntaxHightlight from "@toast-ui/editor-plugin-code-syntax-highlight";
import colorSyntax from "@toast-ui/editor-plugin-color-syntax";
import tableMergedCell from "@toast-ui/editor-plugin-table-merged-cell";
import uml from "@toast-ui/editor-plugin-uml";
import hljs from "highlight.js";

export default class App extends React.Component {
  editorRef = React.createRef();
  constructor(props) {
    super(props);
    this.state = {
      showEditor: false,
    };
  }

  editorToggle = () => {
    this.setState({ showEditor: !this.state.showEditor });
  };

  removeToggleEditor = () => {
    this.editorRef.current.getInstance().remove();
    this.editorToggle();
  };

  render() {
    return (
      <div className="App">
        <button onClick={this.editorToggle}>Toggle Editor</button>
        <button onClick={this.removeToggleEditor}>
          Remove & Toggle Editor
        </button>
        {this.state.showEditor ? (
          <Editor
            usageStatistics={false}
            initialValue=""
            previewStyle="vertical"
            height="600px"
            initialEditType="markdown"
            useCommandShortcut={true}
            ref={this.editorRef}
            plugins={[
              colorSyntax,
              chart,
              [codeSyntaxHightlight, { hljs }],
              tableMergedCell,
              uml,
            ]}
          />
        ) : null}
      </div>
    );
  }
}

Here is index.js:

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);
seonim-ryu commented 4 years ago

@live951 Thanks for the detailed explanation. Can you check if the same issue occurs even if the plugins option is not used?

live951 commented 4 years ago

Sorry I forgot to mention it in my original report. Yes, the memory leak occurs in the same way even with no plugins, but the memory leak is about half the amount of memory (~0.8MB per 10 mount/unmount cycles vs ~1.6MB with all plugins). Here is the App.js sample code I used:


import "codemirror/lib/codemirror.css";
import "@toast-ui/editor/dist/toastui-editor.css";

import React from "react";
import { Editor } from "@toast-ui/react-editor";

export default class App extends React.Component {
  editorRef = React.createRef();
  constructor(props) {
    super(props);
    this.state = {
      showEditor: false,
    };
  }

  editorToggle = () => {
    this.setState({ showEditor: !this.state.showEditor });
  };

  removeToggleEditor = () => {
    this.editorRef.current.getInstance().remove();
    this.editorToggle();
  };

  render() {
    return (
      <div className="App">
        <button onClick={this.editorToggle}>Toggle Editor</button>
        <button onClick={this.removeToggleEditor}>
          Remove & Toggle Editor
        </button>
        {this.state.showEditor ? (
          <Editor
            usageStatistics={false}
            initialValue=""
            previewStyle="vertical"
            height="600px"
            initialEditType="markdown"
            useCommandShortcut={true}
            ref={this.editorRef}
          />
        ) : null}
      </div>
    );
  }
}
ats1999 commented 4 years ago

Hi, can you solve this issue? Github issue

My problem is coming when i want to render data from state. When state data is fetched from an api, then it won't render anything.

See issue live: code sand box