WordPress / gutenberg

The Block Editor project for WordPress and beyond. Plugin is available from the official repository.
https://wordpress.org/gutenberg/
Other
10.52k stars 4.21k forks source link

Gutenberg: Improving Performance on Renders #25780

Open ItsJonQ opened 4 years ago

ItsJonQ commented 4 years ago

Illustration of Gutenberg lagging and glitching

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:

  1. Optimize components, especially those designed to be used by 3rd party authors
  2. Automate/systematize performance techniques (through things like Hooks and higher-order components)

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...

Premature Optimization Is the Root of All Evil

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 😅

diegohaz commented 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?

ItsJonQ commented 4 years ago

@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 🙈

ItsJonQ commented 4 years ago

Block Inserter

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.

1️⃣ Before

Screen Capture on 2020-10-05 at 12-22-17

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.

2️⃣ After

Screen Capture on 2020-10-05 at 12-20-50

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.

🏃‍♂️ Actual performance differences?

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)

1️⃣ Before

Screen Shot 2020-10-05 at 12 23 16 PM

2️⃣ After

Screen Shot 2020-10-05 at 12 24 06 PM

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.

ItsJonQ commented 4 years ago

Block Inserter (Update)

☝️ 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...

1️⃣ Before

BlockInserter: Mouseover + searching

before-001

QuickInserter: Mouseover + searching

before-002

2️⃣ After

BlockInserter: Mouseover + searching

after-001

QuickInserter: Mouseover + searching

after-002

🏃‍♂️ Actual performance differences?

More Chrome performance tests. I performed the following actions for these tests:

Mousing over QuickInserter. Launching BlockInserter. Mousing over BlockInserter.

1️⃣ Before

before-003

2️⃣ After

after-003

Bonus: I've included 4x CPU throttled results.

There were both... pretty bad. However, the "Before" implementation got absolutely REKT.

1️⃣ Before

before-004-4x-throttle

2️⃣ After

after-004-4x-throttle
ItsJonQ commented 4 years ago

Block Inserter (Update) Part 2

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.

1️⃣ Before

before-005

Notice the drop in frames.

2️⃣ After

after-005

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

noahtallen commented 4 years ago

Love it! What would it take to get this improvements into a PR we can ship?

ItsJonQ commented 4 years ago

@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.

noahtallen commented 4 years ago

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.

ItsJonQ commented 4 years ago

Updates!! Some promising results from experiments within G2 Components: https://github.com/ItsJonQ/g2/issues/57#issuecomment-705120565

Video Screencast

david-szabo97 commented 4 years ago

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.

ItsJonQ commented 4 years ago

@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 :)