atom/atom — pull request #2258 by nathansobo (2014-05-15 21:30:51+00:00)
Title: Improve scroll performance of the React editor

This PR improves scrolling performance dramatically over the existing React editor. We're still a bit slower than the original editor for large scrolls, but I have some optimizations in mind that I think can put us over the top. Fundamentally, I think getting the lines on the GPU will be the key to feeling smooth, but the GPU is not without its own corner cases and overhead that need to be overcome. Here are some of my findings in reaching the current design:

* Putting each line and line number on the GPU as its own layer and moving each line individually rather than their containing layer feels really smooth. But unfortunately GPU layers with text on them need to have opaque backgrounds to handle sub-pixel antialiasing correctly on non-retina displays. This means selections can no longer be easily rendered behind the text, and have to be broken up into individual nodes on each line layer. This is really complex, hurts the performance of selecting pretty badly, and causes rendering artifacts on the GPU when we remove selections from lines due to bugs in Chromium. Layer-per-line is a non-option.

* This puts us back to moving a single large `.lines` layer on the GPU that contains the visible lines absolutely positioned in the correct location. We actually need to render ~8 lines above and below the visible viewport in order to prevent Chromium from grouping the repaints when adding removing lines while scrolling.

* Removing the last background selection from the lines layer causes rendering artifacts on the GPU, so we need to leave an empty selection div on the DOM even if there are no selections. This would be fine fine, except it seems to worsen layer compositing time for some reason. I think this might be caused by the GPU considering the entire region that encompasses both the empty selection and the visible lines, which can be quite large when scrolling far into the document. I want to experiment with moving the selection into the range of the visible lines and see if that improves this.

* The overhead of React's virtual DOM seems to be prohibitive for the lines and line numbers. I've opted to update the DOM manually in the `componentDidUpdate` hook for the `GutterComponent` and `LinesComponent`. I manually construct and parse the HTML, then add and remove nodes from the DOM. Since everything is absolutely positioned, the actual location of lines in the DOM doesn't matter, so I just append them to the end always. This cuts our JS time pretty significantly, but I still think there's room for improvement by recycling DOM nodes by changing their absolute position and `innerHTML` rather than creating and discarding so many nodes when scrolling. Despite needing to resort to such low-level operations, I'm still pretty happy about React. The virtual DOM is a fantastic tool, and it's really easy to drop down to low-level DOM manipulation where needed.

This PR improves the React editor, but it's not yet the holy grail of performance I'm hoping for. That will come next. There's a few more things to do before merging:

* [ ] Fix an issue that causes the gutter to be the wrong width in certain situations.
* [ ] Fix the go-to-line view.
* [ ] Preserve the scroll target to prevent sudden halting of velocity scrolling.
* [ ] Get specs passing again after tearing everything apart for performance.