Replies: 6 comments 17 replies
-
This makes a lot of sense, for Core but also for third-party plugins as you mentioned. Thanks for bringing this up!
Should a block preview be available as well? I think previews can provide a lot of value. As the saying goes "a picture is worth a thousand words" ; I could see it being just as useful as the block description for authors looking to insert a block in their content. |
Beta Was this translation helpful? Give feedback.
-
@jsnajdr, thank you for opening this discussion. It's a topic that's been explored several times since 2017. There is an existing tracking issue that was used in the past to coordinate efforts: #2768. I think you provided an excellent summary of the problem and outlined all the steps required to enable async loading for blocks. I only wanted to extend some information. We invested efforts into offering the |
Beta Was this translation helpful? Give feedback.
-
@youknowriad asks in #51778 (review):
First of all, the exact implementation of the import map and the loader is an "implementation detail", in the sense that it can be easily encapsulated and replaced at will with a different implementation. There is the server-side My goal was to have a prototype that really works, and I started with something that's simple and that I'm familiar with. In order to switch to native importmaps, here are the problems that need to be solved. How to load styles? Until very recently, I thought this is the major obstacle that blocks importmap adoption. We need to load not only JS modules, but also CSS stylesheets. Only after discussing import assertions in #53470 I learned that Chrome natively supports CSS modules! I can do: async function loadStyle( handle ) {
const { default: stylesheet } = await import( handle, { assert: { type: 'css' } } );
document.adoptedStyleSheets.push( stylesheet );
} The imported So, if we don't mind using experimental non-standard Chrome-only API, loading styles as ES modules is possible! How to load dependencies and inline scripts? A WordPress script is registered like: wp_register_script( 'my-script', './my-script.js', [ 'dep1', 'dep2' ] );
wp_add_inline_script( 'my-script', 'window.__experimentalEnableFoo = true', 'before' ); When generating an ESM importmap from such a registration, we can easily map the handle to the
but what about the dependencies and the inline script? There are two ways how to approach this. The first is not to use ESM importmaps, but a custom loader, and the "importmap" would contain the serialized script info:
The The second approach is to create a server-side endpoint for loading script handles, and use this endpoint's URLs to create the importmap:
The // load the dependencies first
import 'dep1';
import 'dep2';
// the 'before' inline scripts
(function() {
window.__experimentalEnableFoo = true;
})();
// the main script
(function() {
// contents of my-script.js
})(); First it imports the dependencies, and then ships the sub-scripts wrapped inside IIFEs. Just like when webpack bundles individual modules into chunks. The main problem I see here is that WordPress scripts can be registered differently on different route. The environment on a For blocks this is probably not such a problem, because in practice they are always (?) registered in the universal To conclude, I think that yes, native ESM importmaps are desirable and can be implemented, but it's a lot of work, both in the prototype stage and even more work in the production stage. And the async block loading concept can be developed very well even without them. |
Beta Was this translation helpful? Give feedback.
-
Thanks, everyone for providing feedback here so far - it's been particularly beneficial! I think it would be instrumental to get some feedback and thoughts from the @WordPress/performance folks. |
Beta Was this translation helpful? Give feedback.
-
Thanks @annezazu for looping in the Performance folks! I'd love to also bring in @felixarntz and @joemcgill here to review please (however noting they are away this week at WCUS). |
Beta Was this translation helpful? Give feedback.
-
Just wanted to make clear for anyone confused what "async" means here. In this context, |
Beta Was this translation helpful? Give feedback.
-
Motivation
A WordPress site with a few popular plugins, like Jetpack and WooCommerce, can have hundreds of registered and available blocks. And load all their scripts and styles into the editor, upfront, during the editor initialization. But a typical post uses only a handful of blocks, and it can be shown that the redundant blocks code typically constitutes about 50% of the editor size.
Solution
The solution we're proposing here is to load block scripts and styles asynchronously, just in time. On initial load, the editor would know only a very basic static info about all blocks, like their names, titles, icons. This is the "bootstrap" info sent by the server in the editor's HTML document That's enough to show a basic inserter UI for the block, and also to load the full block code when needed. The full block load would be triggered only just before a block is about to be created. That can happen when a content parser (for post or block pattern) encounters a block of given type, when a block is inserted from inserter, or created by a block transform.
To implement this solution, we need to make three broad changes in the Gutenberg editor:
title
ordescription
, on the server.select( 'core/blocks' ).getBlockType()
selector, or thecreateBlock
andswitchToBlockType
functions from theblocks
package, ortransform
functions on block transforms. Keeping a backward compatibility with the old sync versions will be a major challenge.Now let's expand on these three themes in more detail.
Server-side block registration
A typical server side block registration call looks like this:
This is the most minimal server registration, and it provides only the information that the server really needs to render the block when it appears in content. The
render_callback
is a PHP function that generates the block's dynamic HTML content. Thestyle
field is more interesting for us. It defines the style that should be enqueued when the block is rendered. When thefile:
syntax is used, it is a two step process. During the block registration, the referenced file is registered, usingwp_register_style
, with an auto-generatedexample-block-style
handle. Then, when the block is being rendered, WordPress will callwp_enqueue_style( 'example-block-style' )
to include the block's style in the rendered page.An alternative way to register the style is to register the handle yourself:
You register the style, then register the block, and then WordPress can enqueue the styles declared by the when it decides so. Note that here, for frontend styles and scripts, the assets are already enqueued dynamically, only when needed.
To register a full block in the browser client, someone needs to call the JS function
registerBlockType
. This is typically done by an "editor script", declared during server registration:When generating the HTML page for Post or Site Editor, WordPress will loop through all registered blocks and will enqueue all their
editor_script
andeditor_style
handles.The client-side block registration can be also completely decoupled from server-side registration. WordPress can simply enqueue a "random" script on the editor page, an if the script contains calls to
registerBlockType
, blocks are registered. This is unfortunately fairly typical. Not even Core blocks declare theireditor_script
s on individual blocks. There is onewp-block-library
script that bundles all Core blocks definitions, and the connection between the server-side block definitions and this script is not declared anywhere. Plugins like Jetpack or WooCommerce register their blocks the same way.If editor scripts and styles are to be loaded dynamically, this practice will need to be abandoned. Connection between a server-side block and client-side editor script needs to be declared and known, otherwise the "load the editor script for block X now" task cannot be performed.
Note that it is not strictly necessary that each block has its own individual, unique editor script. It's OK if multiple blocks declare the same
editor_script
, like:Here, dynamically loading any of the three registered Core block will trigger a load of the entire Block Library script, and registration of all Core blocks. This loads much more code than we really need for one individual blocks, but on the other hand it can be a very good tradeoff to bundle multiple blocks into one script. It's very similar to how JS bundlers like webpack bundle multiple modules into one chunk, trying to optimize things like number of parallel HTTP requests etc.
Generating an importmap
When blocks correctly declare and register all their script and style handles, they can be automatically enqueued, with
wp_enqueue_script
andwp_enqueue_style
, when generating the HTML page for the editor. But we don't want to do that, that's the existing sync block loading, and we want to be async. Instead, the server should expose to the client all the information that the client needs to load a given script handle, at any time when it needs it.For WordPress scripts, the registered information looks like this:
If the client knows this information, it can load the script. It will:
wp-element
andwp-i18n
wp-content/plugins/example/editor.min.js?ver=1.2.3
There can be a "loader" function, called like this:
After awaiting the
loadScript
call, we are sure that theexample/block
has been registered and can work with it.Loading styles works exactly the same way, the server sends the client the information about registered script handles, and a
loadScript
function can load them.ESM importmaps
Ideally, the "information that the client needs to load a given script handle" would be standard ESM importmap declared as part of the editor HTML page:
and loading the script would be done with a standard
import
statement:But there are many practical obstacles that make this approach only aspirational at this moment:
import
statements in its source.wp_add_inline_script
? WordPress commonly uses that to add additional data for a script, typically some config setup of the given library, or its localization.register_block_type
can declare multiple script handles for a given script, likeviewScript: [ 'file:./view.js', 'file:./view.modal.js' ]
. Both scripts needs to be loaded, i.e., a singleimport( 'example-block-view-script' )
call should load two different URLs. Similar to dependencies.WordPress blocks and scripts would need some major architectural cleanup before ESM importmaps are feasible.
Bootstrapped server info
The server-side registration info is available on the client as a result of the
bootstrapServerSideBlockDefinitions
function call, and also on the/block-types
REST endpoint. The client can therefore access some basic info about blocks even without loading their scripts. For example, to make the block inserter work, we need this block info to be properly filled:The block inserter needs this info to:
supports.inserter
,parent
andancestor
fields are needed for that.title
,description
oricon
are needed.We also need the same info for all block variations. The variations are often shown in the inserter as a regular block.
All the above fields are static strings or arrays that are easy to registered on the server, except
icon
andexample
.Icon
Block icons currently can't be registered on the PHP server, because they can be generic React components. A JSX element or a JSX component (function or class constructor) is a valid icons. They can't be expressed as PHP code and sent across network in a serializable form. We'll need to deprecate them, and support only Dashicons (referenced by a string identifier) and SVG, or more precisely, image URLs. The React components are almost always simple
<svg>
elements, so they should be easy to migrate to static strings ordata:
URLs. Blocks registered in the WP.org Block Directory also need to have a web-compatible icon registered, so this is a reasonable requirement.Example
The
example
field is a static structure of block attributes and inner blocks, possible to register on a PHP server, but it doesn't make much sense. Theexample
data are used to actually create a block with given attributes, so all sensible operations require the block to be loaded anyway. There's no benefit from the server-side registration. In Gutenberg UI, the example is used in theBlockPreview
popup shown when the user hovers over the block. So, displaying the inserter is possible with server-side data, but when hovering over the displayed inserter, blocks will get fully loaded in order to display their previews.Registering the
example
field on the server could be useful only if we wanted to generate the preview markup on the server. That's possible, the server knows everything it needs to know to generate a fully styled preview. It's similar to server-side rendering of embeds. Could be also a REST endpoint.When is a full block load triggered?
createBlock
)switchToBlockType
)All the affected APIs (mostly in the
@wordpress/blocks
package) will need to be switched toasync
function, returning promises.Block transforms
Block transforms are currently registered as a
transform
property of a block registration, but this is an architectural design error. Transforms are animals completely independent from blocks. I should be able to register a block transform individually. A block transform says "I can transform a block of type X (or multiple blocks) into a block of type Y", and we don't need block type X or Y to be loaded in order to register or perform such a transform. The loading will be done by thecreateBlock
calls inside thetransform
orconvert
functions.Beta Was this translation helpful? Give feedback.
All reactions