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

Block and Component Lazy Loading #2768

Open
1 of 3 tasks
gziolo opened this issue Sep 22, 2017 · 34 comments
Open
1 of 3 tasks

Block and Component Lazy Loading #2768

gziolo opened this issue Sep 22, 2017 · 34 comments
Assignees
Labels
[Feature] Blocks Overall functionality of blocks Framework Issues related to broader framework topics, especially as it relates to javascript [Status] In Progress Tracking issues with work in progress [Type] Performance Related to performance efforts [Type] Tracking Issue Tactical breakdown of efforts across the codebase and/or tied to Overview issues.

Comments

@gziolo
Copy link
Member

gziolo commented Sep 22, 2017

Tasks List

Issue Overview

Initially raised by @mtias in #665 and something that @dmsnell has already started exploring when working on CodeMirror block. It would be beneficial to have a standardized way to defer loading optional parts of the block and/or the whole block in general when it is resource heavy. I might even risk saying that it would make sense to have all external blocks by default loaded on demand using chunk splitting technique. It would help us ensure that only essential parts of Gutenberg are loaded on initial page load, which would lead to better performance and overall first-time impression. We could start pre-loading optional blocks when the browser is idle or whenever a user is about to select such block using the drop-down menu or / command. All that should help scale Gutenberg as the number of available blocks grows.

Related Issues, PRs and discussions

@gziolo gziolo added [Feature] Blocks Overall functionality of blocks [Type] Question Questions about the design or development of the editor. labels Sep 22, 2017
@gziolo gziolo self-assigned this Sep 22, 2017
@gziolo gziolo added the Framework Issues related to broader framework topics, especially as it relates to javascript label Dec 12, 2017
@mtias mtias added [Type] Task Issues or PRs that have been broken down into an individual action to take and removed [Type] Question Questions about the design or development of the editor. labels Jan 4, 2018
@noisysocks
Copy link
Member

We can probably get some big wins by lazily loading in the assets that are queued when we call:

@mtias
Copy link
Member

mtias commented Jan 9, 2018

Yes, this seems more and important to get in place.

@mtias mtias mentioned this issue Feb 6, 2018
9 tasks
@noisysocks
Copy link
Member

Two outstanding issues to bear in mind when we eventually revisit how asynchronous components are handled in Gutenberg:

@noisysocks
Copy link
Member

I've been thinking about this a little and thought I'd dump some of the contents of my brain. It would be good to get the conversation going. I recall that @youknowriad was mulling over some ideas in this area at WordCamp Europe.

There's two things we're talking about in this issue:

  1. Lazily loading assets that are bundled as part of Gutenberg. For example, the code that defines a gallery block
  2. Lazily loading assets that are included in WordPress. For example, the code that defines wp.codeEditor.

Gutenberg assets

To do this, we can make use of dynamic imports and the import() function.

import( /* webpackChunkName: "wp-block-gallery" */ './gallery' ).then( ( { name, settings } ) => {
	registerBlockType( name, settings );
} );

Here, the code in core-blocks/gallery/index.js will be bundled as a seperate webpack chunk and won't be loaded by the browser until import() is called.

WordPress assets

This is trickier.

One idea is that we could write our own webpack loader which lets you specify a list of WordPress script or style assets that you wish to load.

import 'wp-script-loader!wp-codemirror,code-editor,htmlhint,csslint,jshint,htmlhint-kses';
import 'wp-style-loader!wp-codemirror,code-editor';

The webpack loader would return a script that, when executed by the browser, inserts a <script> element into the DOM which loads the specified assets using the existing /wp-admin/load-scripts.php or /wp-admin/load-styles.php.

We can then combine this technique with the import() function as above to enable lazy asset loading.

Promise.all( [
	import( /* webpackMode: "eager" */ 'wp-script-loader!wp-codemirror,code-editor,htmlhint,csslint,jshint,htmlhint-kses' ),
	import( /* webpackMode: "eager" */ 'wp-style-loader!wp-codemirror,code-editor' ),
] ).then( () => {
	wp.codeEditor.initialize( textarea, settings );
} );

Here, the <script> tag which loads the assets won't be inserted into the DOM until import() is called.

As cool as this is, it's not clear to me that it's a better approach than e.g. writing a wp.components.withLazyAssets HOC which injects a <script> tag into the DOM. The benefits of a HOC is that it's easier to understand and can be be used by third party developers.

@dmsnell
Copy link
Member

dmsnell commented Jul 4, 2018

thanks for the discussion @noisysocks

one idea this leads me to is a realization that we have some resources we need as soon as possible and some that can load later.

for example we want to know the name, category, icon, and other meta details about a block so that we can do things like load the block inserter. however, since some blocks are complicated they might only need to load on first use (which could be a preview in the block inserter or could be in the editor itself).

on that note it also seems reasonable that we could encourage coding blocks that have dynamically loaded elements. this could actually solve the dilemma while leaving open the door that some plugins will abuse it by loading everything up front.

registerBlockType( 'my-cool-block', {
	title: __( 'My Cool Block' ),
	attributes: [  ],
	save: dynamicLoad( './my-cool-save' ),
	edit: dynamicLoad( './my-cool-edit' ),
} );

with this we can provide a generic loading view for components which are loading but immediately replace them once loaded. behind the scenes we use import() or whatever mechanism is available and then individual authors need not worry about it. (maybe this is the idea of withLazyAssets() - I'm not familiar with that)

@aaronjorbin
Copy link
Member

I am putting the future label on this, but if it's something that would be really beneficial for 5.0, please move it into the appropriate milestone.

@gziolo
Copy link
Member Author

gziolo commented Oct 7, 2018

It’s going to be essential for blocks discovery at some point, 5.1 should be fine as target.

@gziolo
Copy link
Member Author

gziolo commented Nov 23, 2018

Bringing comment from @ockham shared in #12232:

Is your feature request related to a problem? Please describe.
While working on Jetpack Gutenberg blocks and plugins, we've recently found a few instances where some sort of async fetching of resources prior to block registration would come in handy. Examples include:

  1. Translation files. This is relevant when Gutenberg is used outside of wp-admin, e.g. in Calypso. We have a Calypso-specific workaround (Gutenberg: Load Jetpack block translations in Calypso synchronously Automattic/wp-calypso#28304) that takes care of loading translation files prior to loading Gutenberg, but we were wondering if more native tooling might be in order here. (We can't simply load translations after the editor and rely on React re-rendering strings afterwards for things like block names and descriptions that we pass to registerBlockType).
  2. Information relevant for block availability. We might want to register blocks conditionally, depending on criteria obtained from a REST API endpoint.

Describe the solution you'd like
A bit fuzzy, but maybe allow registerBlockType() (and registerPlugin()) to return a promise, and wait for that to be resolved?

Describe alternatives you've considered
For 1. -- see the workaround mentioned there.
For 2. -- In wp-admin, we're using wp_localize_script to set a global variable to contain relevant information
(None of which are particularly nice.)

/cc @sirreal @tyxla @simison @lezama @enejb

@gziolo gziolo changed the title Blocks: Consider using async loading for components Blocks: Consider using async loading for block and components Nov 23, 2018
@gziolo gziolo changed the title Blocks: Consider using async loading for block and components Blocks: Consider using async loading for blocks and components Nov 23, 2018
@gziolo gziolo changed the title Blocks: Consider using async loading for blocks and components Consider using async loading for blocks and components Nov 23, 2018
@gziolo gziolo changed the title Consider using async loading for blocks and components Allow async loading for blocks and components Nov 23, 2018
@dmsnell
Copy link
Member

dmsnell commented Nov 26, 2018

it's also worth discussing the implications of our editing flow and our block invalidation flow. until our editing flow is asynchronous we might still run into pretty massive issues trying to make one part of it asynchronous.

in #7970 I proposed an asynchronous parsing flow which @aduth helped me with but we ran into the same issue that the entire stack is built upon a synchronous model. the issue of the editor invalidating blocks between unregistering on and re-registering highlights the problem well.

it would be really helpful for us to discuss the semantics of an asynchronous editing session and nail down some common vocabulary before hacking away at it - actually I'm sure this is something we should defer until later and not try to get in before "the merge" or even shortly afterwards.

the implications here of moving to an asynchronous model are numerous:

  • we can load components in chunks and dynamically as-needed
  • we can stream the parser and do things like only parse up until "the fold"
  • we can use alternate parsing models, such as the Rust parser @mntio wrote and compiled to WASM
  • we can more easily handle collaborative editing sessions since we'd already have a model for asynchronous block-level data structures

in the short-term for block registration we might be able to get away with creating a replaceBlockType function that swaps a block's implementation in a way where we don't re-render until it's over.

type Block
  = Pending BlockInfo
  | Loaded BlockInfo

type Document
  = Uninitialized
  | Parsing (Stream [Block])
  | Parsed [Block]

type Editor
  = Uninitialized
  | LoadingCoreEditor
  | Pending Document
  | Running Document

Practically some of these states will connote things like "I can or cannot edit this block right now" and "we should wait to invalidate the block content until it's ready" and "the editor itself is changing to stop all input processing momentarily" etc…

@gziolo gziolo removed their assignment Feb 8, 2019
@mtias mtias added the [Type] Performance Related to performance efforts label Nov 17, 2019
@sarayourfriend
Copy link
Contributor

Thinking about this some more, I think rather than introducing a new __experimentalLazyScripts property, it would be more expedient to just use editor_script, considering that will provide the lowest uplift for existing blocks and are scripts that are already registered in a way that we understand.

I don't think we should introduce a new concept for that. I'm a little confused at the moment about how editor_script should work, however. I'm struggling to find in the codebase where these handles actually get enqueued. But according to https://developer.wordpress.org/block-editor/tutorials/block-tutorial/writing-your-first-block-type/ I should just have to register the script and add it as the editor_script to be enqueued right? Is that documentation out of date?

@youknowriad
Copy link
Contributor

So I think that editor_script basically refers to the script that will register the block and that is loaded on the editor. So while it's close to what we're looking for here, If we do want to split things say registration and edit function, it might not serve that purpose right now.

@sarayourfriend
Copy link
Contributor

sarayourfriend commented Aug 4, 2020

@youknowriad Do you think it would be worthwhile to revisit your original solution over the one I've proposed? Given what you know of each approach, do you have a sense of whether one will be better off long term? Trying to make sure my energy in this is spent wisely.

I've been thinking more over the weekend and I think if I continue down the path I'm proposing I would go down the strategy pattern path and implement a provider prop that would be passed by the editor itself rather than into LazyLoad. We'd also have LazyLoad automatically wrapping edit functions on blocks that declare themselves __experimentalLazy. Then the lazinessProvider (or some other name) would be passed as part of settings to the editor initialization. Then the WordPressLazinessProvider would be what I'd been working on, and a similar NPMLazinessProvider could be implemented as an option that uses something like an asynchronous import or React's lazy/suspense. The lazinessProvider would get passed to LazyLoad which would wrap the blockType.edit in block-edit when rendered at the end of the function (for lazy blocks).

The only problem with this is that I'm not sure the static analysis required to make something like the NPMLazinessProvider would be possible. It should be possible to provide, for example, a simpler CDNLazinessProvider. Not totally sure what kinks would need to be sorted out to make importing from npm directly possible. It certainly wouldn't happen transparently/automagically as it seemed like your original solution would be able to support.

@youknowriad
Copy link
Contributor

I know it's not helpful but the answer is really I don't know.

NPM registry supposes that everything just works out of the box, which means the dependencies are static there import something from "something". Anything removing that means we're giving up on npm support for blocks and at the moment I don't really see the provider API proposal as a solution for that tbh.

That said, I do wonder how important is it for us to support these use-cases. For WordPress, it is definitely not important but are we ok giving up on tools like asblocks using core blocks, drupal Gutenberg using core blocks...

Maybe others have thoughts here @mcsf @mtias

@sarayourfriend
Copy link
Contributor

I don't really see the provider API proposal as a solution for that tbh

Agreed. I'll take some time this week to revisit your previous attempt and try to revive it.

One thing to note though is that the classic block currently depends silently and heavily on WordPress specific window variables. Would we want to slate work to eventually untie the classic block from WordPress's loading behavior or alternatively should we put the classic block into it's own package?

@youknowriad
Copy link
Contributor

Would we want to slate work to eventually untie the classic block from WordPress's loading behavior or alternatively should we put the classic block into it's own package?

Yes, there are some blocks that are very WP-dependent, and Classic block is one of them. At some point we should try to separate these blocks from generic ones.

@kevin940726
Copy link
Member

The React core team recently shared their top-level overview of how SSR would look like in React 18: reactwg/react-18#37

It might worth a read, and be taken into consideration when designing our async loading strategy.

@dmsnell
Copy link
Member

dmsnell commented Jun 10, 2021

hi @kevin940726 - thanks for the link. was there something specific in there that you thought was important for this issue?

@kevin940726
Copy link
Member

was there something specific in there that you thought was important for this issue?

Nothing specific, it's just a reminder 😅 . since SSR in React will get a significant update, I just want to make sure that we're leveraging the best practices and not re-inventing the wheels. :)

@dmsnell
Copy link
Member

dmsnell commented Jun 11, 2021

Thanks for sharing @kevin940726 - I don't think these two systems will overlap much. I'm not even sure there's any likely scenario in which we would apply SSR to this stage of Gutenberg loading, or if that would be practical/possible/beneficial.

@kevin940726
Copy link
Member

It's not only related to SSR though, React 18 will also include Concurrent Rendering and Suspense etc. These are not separate features though, they are related to each other and we should pay attention to how we integrate them into Gutenberg.

For instance, we should really use React.lazy and <Suspense> for asynchronously loading components by default, rather than creating our own <LazyLoad> component. We could potentially also get partial hydration working if we do things right.

That said, it's just a reminder. I don't know how much it applies to our architecture yet. But let's just keep an eye on it. Maybe it doesn't have anything to do with this issue, then we can just ignore my comments 😛.

@gziolo
Copy link
Member Author

gziolo commented Mar 29, 2023

@tyxla started PR #48315 with an initial experiment that approaches async block loading in a radically simple way.

@tyxla
Copy link
Member

tyxla commented Mar 29, 2023

Indeed, @gziolo, although that PR still needs some polishing and exploration. It is indeed meant to be a very simple way to deal with the async loading of blocks, likely serving temporarily until we have a better solution.

I think we still should explore using JS modules and import maps as a more robust way to manage scripts and styles through a better long-term solution for WordPress as a whole.

@tyxla
Copy link
Member

tyxla commented Aug 7, 2023

@jsnajdr is experimenting with async block loading in this PR and has kicked off a discussion about the approaches and decisions there - it could definitely use some additional eyes and feedback.

@gziolo gziolo changed the title Allow async loading for blocks and components Block and Components Lazy Loading Sep 5, 2023
@gziolo gziolo changed the title Block and Components Lazy Loading Block and Component Lazy Loading Sep 5, 2023
@gziolo gziolo mentioned this issue Sep 5, 2023
58 tasks
@gziolo gziolo added the [Status] In Progress Tracking issues with work in progress label Sep 5, 2023
@gziolo gziolo self-assigned this Sep 5, 2023
@gziolo
Copy link
Member Author

gziolo commented Sep 5, 2023

Update

@westonruter suggested using a different phrase than "async loading" in #53260 (comment). We concluded that "Lazy Loading" fits best and aligns with how people refer to it in the JavaScript ecosystem. I updated the title to reflect that.

Last week, @jsnajdr landed #53807 with refactoring to the block registration as a preparation for the follow-up work to allow lazy loading parts of the block definition.

I started looking in WordPress/wordpress-develop#5118 at ways in WordPress Core to automate consuming shared scripts generated in the build pipeline. My main motivation was to figure out a way to fit the concepts of code splitting (runtime chunk, split chunks, dynamic imports) into the existing API shaped around wp_register_script for blocks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Blocks Overall functionality of blocks Framework Issues related to broader framework topics, especially as it relates to javascript [Status] In Progress Tracking issues with work in progress [Type] Performance Related to performance efforts [Type] Tracking Issue Tactical breakdown of efforts across the codebase and/or tied to Overview issues.
Projects
None yet
Development

No branches or pull requests