-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Babel Plugin Async Load: Lazy load react components when necessary #8017
Conversation
@@ -0,0 +1,78 @@ | |||
import { Component } from '@wordpress/element'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This higher-order component has been added to the compose
package for now. I was wondering if it should be its own package.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should it be part of the Babel transform package? If it's specifically meant for handling asynchronous components, including the component loader doesn't seem too farfetched.
This still lacks some unit tests and style fixes but I wanted to have feedback. |
Could you elaborate on the benefits of using a babel plugin? Why don't we manually wrap the imported component with (This is very cool, though 🙂) |
@noisysocks because the exported package in npm shouldn't have lazy loading built-in. it should be just a regular npm dependency. (There's no WordPress script handles or WordPress Url in npm packages, they should be generic and can be used in any application) |
Seeing it and the comment from @noisysocks, I thought that an alternative would be to use withFilters HOC and wrap the original code with LazyLoading part. I didn’t try it myself, but according to the description it might be an option. |
@gziolo That's probably an option, it would make |
But we can wrap the component on the calling side, no? e.g. // core-blocks/html/index.js
import { CodeEditor } from '@wordpress/components';
import { lazyLoad } from '@wordpress/compose';
const LazyCodeEditor = lazyLoad( ... )( CodeEditor );
...
edit() {
...
return <LazyCodeEditor />;
...
} Or is the idea that we don't want to rely on |
Exactly, wrapping on the calling side means the calling side can't be exposed as a generic npm module. |
Babel Plugin Async Load | ||
====== | ||
|
||
Some WordPress Scipts and styles are heavy. To avoid having to load all the scripts and styles synchronously, we can lazy load some of these scripts. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo: "Scipts" -> "scripts"
@@ -0,0 +1,58 @@ | |||
const lazyLoadTemplate = ` | |||
var COMPONENT_NAME = wp.compose.lazyLoad( SCRIPTS, STYLES, BASE_URL)( WRAPPED_COMPONENT ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor: Mismatched whitespace (missing before first closing parenthesis).
COMPONENT_NAME: t.identifier( 'LazyLoad' + subPath.node.local.name ), | ||
SCRIPTS: t.arrayExpression( componentConfig.scripts.map( ( script ) => t.stringLiteral( script ) ) ), | ||
STYLES: t.arrayExpression( componentConfig.styles.map( ( style ) => t.stringLiteral( style ) ) ), | ||
BASE_URL: t.identifier( state.opts.siteURLSource ), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Initial reaction: Why does a Babel plugin care about a WordPress site URL?
Edit: Second reaction: Can't we just allow this to be defined as some generic option which is serialized and passed through to the base component via the template call?
Edit: Third reaction: Okay, duh, I understand now; the loading is satisfied by calling load-scripts.php
. I still wonder if, as part of this being a generic non-WordPress-specific Babel plugin, we could change this to be satisfied by an implementation provided by the consumer. Maybe a global function which returns a promise when called, that the lazyLoad
wrapper will execute (and pass serialized options for specific components, that we could use to pass the script handles?)
const importVisitor = { | ||
ImportSpecifier: function( subPath ) { | ||
state.opts.components.forEach( ( componentConfig ) => { | ||
if ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like a good candidate for early return to avoid excessive indentation.
`; | ||
|
||
export default function( babel ) { | ||
const { types: t, template } = babel; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TIL about template
. Neat 👍
|
||
// 2. Replace all occurences | ||
const replaceOccurences = { | ||
JSXIdentifier: function( subPath ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove : function
? (Object function shorthand)
STYLES: t.arrayExpression( componentConfig.styles.map( ( style ) => t.stringLiteral( style ) ) ), | ||
BASE_URL: t.identifier( state.opts.siteURLSource ), | ||
} ); | ||
const parent = subPath.findParent( ( parentPath ) => !! parentPath.insertAfter ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this predicate doing? (Add as a code comment)
@@ -0,0 +1,78 @@ | |||
import { Component } from '@wordpress/element'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should it be part of the Babel transform package? If it's specifically meant for handling asynchronous components, including the component loader doesn't seem too farfetched.
let hasAlreadyLoadedAssets = false; | ||
|
||
function loadAssets() { | ||
if ( hasAlreadyLoadedAssets ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This won't handle the case where another instance of the component is mounted while loading is still occurring.
We should just set the scope variable to the promise itself. That way it can repeatedly be .then
'd, even if it's already resolved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should there be retry if a previous load attempt failed?
@@ -168,6 +168,10 @@ function gutenberg_register_scripts_and_styles() { | |||
filemtime( gutenberg_dir_path() . 'build/compose/index.js' ), | |||
true | |||
); | |||
wp_add_inline_script( | |||
'wp-compose', | |||
sprintf( 'window._wpSiteURL = %s;', json_encode( site_url() ) ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wonder if this is something where we could just assign onto the component prototype itself, rather than having to have this be part of the transform.
i.e.
sprintf( 'wp.components.CodeEditor.siteURL = %s;', json_encode( site_url() ) )
I'm not certain about this approach anymore mainly because it works only for Core registered styles and scripts. We should probably see if we can make it work for custom scripts/styles first before pushing this forward. |
Seems relevant to my third point in #8017 (comment) |
I took another look at this one. I don't know that we should concern ourselves too much with trying to leverage or improve Where the challenges I see with this, at least with the code editor, are in determining what it is that the CodeEditor component is depending upon. Is it an actual npm package? Or the assumed presence of a global, as we have today? If it's a package, is it the A few naive thoughts / opinions:
wp_add_inline_script(
'wp-components',
implode(
"\n",
array(
'wp.components.CodeEditor = wp.compose.withAsyncLoad( function() {',
' return Promise.all( [',
' ' . implode(
','
array_merge(
array_map(
'gutenberg_get_script_load_js_promise',
array(
'wp-codemirror',
'code-editor',
'htmlhint',
'htmlhint-kses',
'csslint',
'jshint',
)
),
array_map(
'gutenberg_get_style_load_js_promise',
array( 'wp-codemirror', 'code-editor' )
)
)
),
' ] );',
'} )( wp.components.CodeEditor );',
)
)
); Note here:
|
@youknowriad pointed me to #8252 😄 |
A long time ago I opened #2768 where I was debating with myself that we could take this direction. If we would split block's registration into two parts - server and client - we could easily achieve it marking the component as |
Closing as not needed now, but we might want to revisit at some point. |
This PR tries to solve a blocker we have at the moment to ensure some of the packages are generic enough but at the same time allow lazy-loading some of their dependencies in the runtime code.
It does so by swapping the component with a lazy-loaded version at build time. Details on the README.
Testing instructions