Open ItsJonQ opened 4 years ago
That's amazing, @ItsJonQ! Thanks for starting this issue.
I wonder why so many things are being re-rendered by just moving the cursor around? I guess there's some internal state that is updated on mouse over, move etc. But is it really necessary?
@diegohaz Most likely something tracking a selected or hovered block state. That part makes sense. But perphaps... not everything needs to subscribe (or be exposed to) to that bit of state 🙈
To get a better sense of performance improvement strategies... I wanted to examine something that was both isolated(ish) and substantial (something bigger than an input
).
I think the Block Inserter matches these attributes.
Upon initial inspection, it looks like every piece of UI (search, individual grid items, icons, etc...) re-render upon hover
. The hover trigger makes sense, as we need to render a preview of a block. However, I don't think it's necessary that the entire view re-renders.
With a couple of memo
, useMemo
, and useCallback
updates, I was above to achieve something that's closer to should be re-rendering. For example, the individual grid items no longer re-render.
It already feels zippier. To double check, I ran some performance tests within Chrome. In the tests, I just mouseover'ed some items (like the initial GIF demo)
Notice in the after, the frames stay consistent at around 60fps (green) and the computation segments (yellow) are less scary.
I can imagine that this particular example (Block Inserter) will progressively degrade as:
My local development is on a well spec'ed MacBook Pro, running MAMP. Also, Gutenberg builds React in production mode.
In other words... these results basically represent a best case scenario.
Note: There are probably some CSS-based layout improvements we can do as well. This thread focuses specifically on controlling React re-renders.
☝️ continuing from the updates above...
I've also optimized search/filter rendering. These updates apply to both the QuickInserter and BlockInserter.
Here are the results...
BlockInserter: Mouseover + searching
QuickInserter: Mouseover + searching
BlockInserter: Mouseover + searching
QuickInserter: Mouseover + searching
More Chrome performance tests. I performed the following actions for these tests:
Mousing over QuickInserter. Launching BlockInserter. Mousing over BlockInserter.
Bonus: I've included 4x CPU throttled results.
There were both... pretty bad. However, the "Before" implementation got absolutely REKT.
Last series of updates!
This is after optimizing hover events some more (and other bits and bobs)
The following tests showed me moving my mouse over the first 3 items in the Block Inserter. The top left UI is Chrome's FPS meter.
Notice the drop in frames.
Much smoother :)
P.S. Here's the branch if you're curious: https://github.com/WordPress/gutenberg/tree/try/block-inserter-performance-improvements
The commits need refactoring. I committed just enough to test out the perf. ideas
Love it! What would it take to get this improvements into a PR we can ship?
@noahtallen Yay! I can create a PR from the work I submitted in this branch: https://github.com/WordPress/gutenberg/tree/try/block-inserter-performance-improvements
Before I do, I'd refactor things as my initial attempt was more on the exploratory side.
I'm unsure if there would be side-effects from the approach though. I believe some tests were unhappy with my updates 🤔
That would probably be the biggest challenge.
makes sense :D I love performance investigations like these :) I'm sure there are lots of aspects of the block editor which could benefit from this.
Updates!! Some promising results from experiments within G2 Components: https://github.com/ItsJonQ/g2/issues/57#issuecomment-705120565
I prefer to React.memo
every component and useCallback
all the functions. Even if they are small components or small functionalities. It's easier to remove them later than adding them later. Overusing memo
, useCallback
and useMemo
is possible, but as I said it's a lot easier to remove them later.
@david-szabo97 Totally agree. I believe that all of the components from the base set (@wordpress/components
) should be memoized by default. I've seen pretty substantial gains from doing this in G2 Components.
Overusing memo, useCallback and useMemo is possible
I've only found that problematic a super super low level. As in.. for a particular function that fires often that's part of an HOC used by EVERY component kind of thing. As you mentioned, it's quite easy to identify and remove memoization, thanks to performance/FPS/memory tools from browsers like Chrome :)
Hi all! For the past week, I've been exploring performance optimizations in the context of UI, Design Tools, and ultimately the Editor itself.
I started sharing some of my findings in the G2 Components project.
This morning, I started looking into Gutenberg to take a peek at how it's currently doing.
(Note: Performance has many many facets and nuances. From time-to-interaction, load times, bundle sizes, DOM paint management, etc... I'm going to be focusing specifically on React re-renders as it relates to state updates)
♻️ (Basically) Everything Rerenders
📹 Video demo of re-rendering https://d.pr/v/rc6Pud
I did a quick performance test with the React DevTools enabled, specifically the "Highlight updates when components render" option. Just by selecting a block and moving my mouse around (without entering content), we can see the entire editor flashing.
For debugging purposes, I've added some console.logs to the BlockControls component to log every time it re-renders. (It happens a lot).
On my MacBook Pro, things seem to render okay (under real-world use). Albeit, I haven't needed to write long posts or compose something with a mixture of custom/complex block types. I do notice sluggishness when I need to adjust values (quickly) related to layouts, such as column or height values.
To simulate the experience of a less powerful machine (perhaps an older computer or a mobile device), I typically throttle the CPU to 4X slower (via Chrome's performance tools).
That's when you can truly see and feel the pain.
I suspect performance will (exponentially) degrade as blocks (both core and 3rd party) gain richer features, and as the Editor is used in a wider context (e.g. Full Side Editing).
💪We can fix this!
📹 Video demo of improvements https://d.pr/v/Nq8NDw
In the video clip above, I made a tiny adjustment to the Cover block containing custom BlockControls. You can see that it no longer re-renders (see console.log) as I mouseover the Cover block (yay!)
(This was achieved by using React.memo)
(It's one tiny example. We need to do a whole lot more than that!)
Ultimately, the solution is to be mindful and deliberate with techniques like memoization and state -> prop consumption.
The tricky thing is, these techniques are often meticulous to implement and cumbersome to test. This would probably feel even more difficult for 3rd party block developers, especially for those who don't have a lot of experience with the nuances of React rendering (who can blame them... it's hard, haha).
🛣 A path forward
I think some things we can do would be to:
Component optimization would start at the foundational level (within
@wordpress/components
), trickly upwards to controls and tools seen in areas like@wordpress/block-library
.If you're curious about component library level performance optimization, you can check out examples from G2: https://github.com/ItsJonQ/g2/tree/master/packages/components/src/Text
It's very intention in managing props, composing hooks, memoization, creating and consuming contexts, and keeping both the React and DOM trees as shallow as possible.
Some of these techniques were borrowed from libraries like Reakit
The best example of #2 in action would be React Redux, which is what WordPress/data is modelled after.
Ignoring the (polarizing) verbosity of the library... One thing I'd like to highlight would be the mapStateToProps + connect HOC. It's a brilliant mechanic. It abstracts away a lot of the memoization fiddling into a single entry point.
Blocks currently do this as a way to retrieve attributes from WP/Data. That's awesome! However, I feel like more can be after within the Edit portion of the block, especially for blocks with many attributes and controls.
From my experiments, one way to achieve this would be to effectively create a "mini" Redux store within the Block Edit component. Using the same mechanics of only consuming/updating what you need.
🤔 It's going to be interesting
I only started looking into performance within Gutenberg (from a rendering perspective). I acknowledge and applaud the performance-related work that has been achieved by folks before me. These examples can be seen in solutions like WP Data.
It's a solution that excels in data syncing (with WordPress), expansion (block registration), and distribution (passing along values to blocks). One way to describe these features would be "macro" features. Focusing on the "big" stuff.
With a lot of those aspects solved and tested, I think it gives us the ability to focus and refine the "micro" features of data handling. Such as, how attributes can be accessed/updated/rendered in an isolated and high-performant manner. Controlling how micro-interactions (like mouse movements or keyboard updates) propagate render cycles to larger contexts. And systematizing it all, so that all the nitty-gritty aspects of performance handling happens seamlessly (or as seamless as we can make it).
P.S.
🙈 Pre optimization...
There's that famous quote!
Considering the editor can be laggy today... I don't think the ideas I've proposed above are pre optimization for the sake of optimization. We kinda need it now 😅