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

Render <BlockPreview> statically #54999

Open
kevin940726 opened this issue Oct 3, 2023 · 15 comments
Open

Render <BlockPreview> statically #54999

kevin940726 opened this issue Oct 3, 2023 · 15 comments
Labels
[Feature] Patterns A collection of blocks that can be synced (previously reusable blocks) or unsynced [Type] Performance Related to performance efforts

Comments

@kevin940726
Copy link
Member

kevin940726 commented Oct 3, 2023

What problem does this address?

Currently, the <BlockPreview> component internally renders a full-blown block editor inside an iframe. That's a lot of overhead for a UI-only component that doesn't allow user interactions. It's especially true for UIs or pages that render multiple <BlockPreview>s at the same time. For instance, the "patterns" tab in the inserter, and the "Patterns" page in the site editor.

To mitigate this, we often use workarounds like useAsyncList to split the rendering of the previews into multiple time slices. However, the browser still needs to perform the rendering and all the scripting for unnecessary work. The user might feel a bit more responsive, but the content is still delayed, thus hurting metrics like LCP or CLS. "Slicing" the work also raises questions regarding accessibility for screen readers which need to know when new content is appearing.

What is your proposed solution?

I have some ideas that could be done separately and they fix different problems:

Removing the block editor

Instead of rendering a block editor for a given blocks instance, we can simply render the HTML using dangerouslySetInnerHTML. This saves us a lot of overhead for rendering a full editor for each preview.

We can get the HTML string from backend. Take patterns for instance, we can call something like $html = apply_filters('the_content', $pattern->content); to render the content on the server side. Most APIs already do this for other reasons, so we just need to expose them.

There might be some edge cases that we can't represent without an editor though. For instance, a preview that renders an empty image block will show a placeholder in the editor but nothing with HTML string. IMHO this is acceptable as these are merely "previews" and don't have to match the editor UI exactly. We can audit the comparisons for different patterns in a separate issue to determine if the result is acceptable.

Removing the iframe

The iframe was needed to encapsulate styles within the preview. However, it also adds the overhead of attaching a document and all its memory along with it. Some popular browser extensions also inject their scripts into every frame on a page, which could make it significantly slower for some users.

The solution is to use Shadow DOM to prevent the style from "leaking" out. Using Shadow DOM also means that we will no longer be limited by the <iframe>'s layout, and we can use simpler CSS like width: 100% to style our previews.

The only gotcha I can think of right now is that some viewport-specific units will not behave correctly. If the preview internally uses 100vh somewhere, for instance, the value will still be calculated based on the browser's viewport, as opposed to the preview's sizes. One possible solution is to make the preview "full size" of the window viewport, then "scale" it down to appropriate sizes. It still has some quirks like unexpected results when resizing though. If this wouldn't work for some previews then we might have to settle on iframes in the end.

Sharing the styles

Currently, each preview requests the same set of common styles and scripts for each iframe. Even though those requests are often cached by the browser, they still need to make the HTTP requests, parse the responses, construct the stylesheets, and then finally apply them on each iframe. It's especially noticeable when a developer works on the page with devtools and "Disable cache" enabled, which is a common practice.

We can leverage Constructable Stylesheets to share those reusable stylesheets for each preview (and even the editor canvas itself). It works for both Shadow DOM and iframes so we can take advantage of this technique whatever the above solutions we settle on. The best part of this trick is that theoretically it has near-zero breaking changes and we can start experimenting with that today.


Thoughts? Other ideas? Concerns? Perhaps we can start by recording the performance metrics for the previews in some critical paths, so that we can compare them and know which change is impactful for the trade-offs presented.

@kevin940726 kevin940726 added [Type] Performance Related to performance efforts [Feature] Patterns A collection of blocks that can be synced (previously reusable blocks) or unsynced labels Oct 3, 2023
@talldan
Copy link
Contributor

talldan commented Oct 3, 2023

The only gotcha I can think of right now is that some viewport-specific units will not behave correctly.

Yep, I've encountered this with shadow DOM. I think rems have a similar issue to vh and vw.

@annezazu
Copy link
Contributor

annezazu commented Oct 5, 2023

FYI to @WordPress/performance folks and cc @ellatrix who worked on iframing.

@jsnajdr
Copy link
Member

jsnajdr commented Nov 3, 2023

One thing that could help make the previews faster: instead of mounting a full "live" React UI, use the server-side rendering approach to render a static markup and then insert the markup into the preview iframe.

The full live React UI is able to react to changes: it subscribes to @wordpress/data stores, registers DOM listeners for various input events, and is able to transition from one state to another. The preview doesn't need to do that. It could do a one-pass render of the Block Editor UI for the pattern, without running any effects etc.

In #55337 we were investigating and optimizing the number of subscriptions to @wordpress/data stores that a fully mounted editor created. When I tried out my debugging code from that PR on the pattern previewer, I found that there are approx 300 store subscriptions being created for every previewed pattern. But none of them are needed, as the preview doesn't need to react to changes.

@draganescu
Copy link
Contributor

I did do an exploration on statically rendering and the impact was instantly visible - basically no UI lag. My exploration did not put the markup in an iframe and missed some CSS - but the effect was so big that no matter the additions the path looks very promsing.

@ellatrix
Copy link
Member

ellatrix commented Nov 3, 2023

I don't like using shadow DOM because, like you said, relative CSS units are messed up.

A solution I shared before somewhere could be reusing the same iframe for all previews, take a picture of then content, and then load all previews as images. What do you think? Any downsides? Could still take a while to generate a picture if you're waiting for resources to load.

@ellatrix
Copy link
Member

ellatrix commented Nov 3, 2023

I'm not sure if this could help Block editor iframe: cache object URL

@ellatrix
Copy link
Member

ellatrix commented Nov 3, 2023

One possible solution is to make the preview "full size" of the window viewport, then "scale" it down to appropriate sizes. It still has some quirks like unexpected results when resizing though. If this wouldn't work for some previews then we might have to settle on iframes in the end.

Could be worth trying

@draganescu
Copy link
Contributor

In #55850 the rendering on the server and then setting the html in the iframe of the previews makes the experience much snappier.

It seems the removal of MemoizedBlockList which is a block list block with all the blocks is really releasing a lot of resources.

The iframes themselves seem to not have a big impact and they're quite useful for styles.

@kevin940726
Copy link
Member Author

A solution I shared before somewhere could be reusing the same iframe for all previews, take a picture of then content, and then load all previews as images.

I don't think there's a web API for taking screenshots of DOM, probably for good reasons too (security?). In addition, generating the images and loading them might just well be slower and more computational-heavy than rendering the HTML directly (which is what the browsers do best). Images are also often bigger in terms of size and not performant for caching.

@draganescu
Copy link
Contributor

I had a week of pause around this work, will resume.

@spencerfinnell
Copy link

spencerfinnell commented Dec 19, 2023

Just noting that I stumbled upon this issue after realizing <BlockPreview> does indeed render a bit slowly. I already had static rendering on the frontend, and definitely found just loading that src URL in <iframes> to be much faster.

@draganescu
Copy link
Contributor

The PR advancing this issue (#55850.), has gone through a lot of work, all trying to land this feature. At this point the bits that hold it back are:

  • Outside of block bindings we don't have an all encompassing API of hooking into block attribute values before the block is rendered.
    • There are some hooks and filters but nothing implemented at a more basic level to have effect on all blocks.
  • We need to hook into block attributes because empty blocks on the server can’t render placeholders so we need to provide some content.
  • Hence the server side preview would be implemented as a block binding.
    • Technically, a binding that is added when there is a previewing context, where the block example data is the source (or some other source is used for previewing such as a post id for featured image).
  • Block bindings use the incoming HTML API to render changes for blocks which need to update HTML.
  • The HTML API does not but will support full CSS selectors.
  • Until then server side block previewing should land as an experiment (see Add static block previews experiment #57977) so we can iterate on it in trunk while block bindings and the HTML API continue to mature.

A quick reminder of the challenges in server side block rendering:

  • patterns and templates contain all kinds of blocks
  • some blocks - like the query block - need a global context to render their contents, such as an archive or a post id
  • some blocks just need some example data to use when in preview context so that they’re not empty.
  • some blocks need example data handlers, such as site title or featured image block, where we mock up the options they bind to in case they’re not set, to create an high fidelity preview.

@kevin940726
Copy link
Member Author

We need to hook into block attributes because empty blocks on the server can’t render placeholders so we need to provide some content.

I know this was long time ago and forgive me if I forget the reasoning completely 😅. Could we just render a block-provided empty content if we encounter such cases?

For instance, the Image block renders nothing on the server if it doesn't have a src attribute. What if we render a pre-defined HTML for empty state (something like <img src="...">)? We can define it per block via an optional placeholder or empty field, and can also only render it when we're in preview mode.

I think this would solve the empty state issue, which I think might be the 80% missing cases here. For the rest I think we'd still want to explore the HTML API in the future.

The proposed solution comes from the assumption that rendering statically for 99% of the patterns with a few fallbacks is still better than rendering all the patterns in editors.

@Mamaduka
Copy link
Member

We can define it per block via an optional placeholder or empty field, and can also only render it when we're in preview mode.

Blocks already have the example property, which is used to render block example previews. The same data could be used for empty blocks.

@kevin940726
Copy link
Member Author

kevin940726 commented Aug 14, 2024

Blocks already have the example property, which is used to render block example previews. The same data could be used for empty blocks.

I think the problem with the example property is that the PHP backend doesn't know how to render a block with only the attributes provided. It still needs the JS to generate the markup first. That brings us back to the HTML API solution again though.

If we support adding the full markup in the example field then it feels redundant. I believe we can still find a way to add fallback support for core blocks though. For third-party blocks, we might still need the HTML API or something else.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Patterns A collection of blocks that can be synced (previously reusable blocks) or unsynced [Type] Performance Related to performance efforts
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants