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

Interactivity API: Use modules instead of scripts in the frontend #56143

Conversation

luisherranz
Copy link
Member

@luisherranz luisherranz commented Nov 15, 2023

What?

Built on top of #56118.

This is the third of a series of experiments to understand the requirements of a hypothetical Modules API. More info in the Tracking Issue:

This experiment aims to implement an API that is more complex than the ones implemented in the first and second experiments, adding a required server dependency graph to be able to print only the necessary modules in the import map, apart from preloading modules to avoid initial waterfalls:

  • An API to register modules using:
    • A module identifier (required)
    • The full URL or the path (required)
    • The static and dynamic dependencies (required)
    • A version (optional)
  • An API to enqueue modules.
  • Only the modules needed for the enqueued modules have an entry in the import map.
  • Enqueued modules add a <script> tag in the head.
  • The static dependencies are preloaded when the module is enqueued using a <link rel="modulepreload"> tag in the head.

Why?

In the first experiment, it's not easy to know which modules need to be preloaded and, therefore, it's not easy to avoid the initial waterfalls.

In the second experiment, all the registered modules are added to the import map, even the ones that are not used on the page. In this version, declaring the dependencies is required, so we can know all the modules that are going to be used on the page and only include those in the import map.

How?

By leveraging a new Gutenberg_Modules class that acts as a singleton and a couple of functions to expose the functionality:

  • gutenberg_register_module to register modules and add entries to the import map.
  • gutenberg_enqueue_module to enqueue modules.

Then, we print three times using the wp_head action:

  • The import map.
  • The enqueued modules.
  • The preloaded modules.

Testing Instructions

  • Add some interactive blocks: Navigation, Query or Image block.
  • Check that in the frontend, there is an import map script.
  • Check that scripts with type="module" are loaded by the interactive blocks.
  • Check that the @wordpress/interactivity module is loaded by the other modules using import ... from "@wordpress/interactivity".
  • Check that the @wordpress/interactivity module is downloaded at the same time as the rest (instead of after, like in the first experiment), and that there is a link tag with rel="modulepreload" in the head.
  • Check that the import map only has the @wordpress/interactivity module.

Follow-up tasks

  • Replace the syntax of the dependencies array with this one.
  • Filter enqueued modules when printing the preloaded modules.

@luisherranz luisherranz added the [Type] Experimental Experimental feature or API. label Nov 15, 2023
@luisherranz luisherranz self-assigned this Nov 15, 2023
@luisherranz luisherranz merged commit fe82fa0 into trunk Nov 29, 2023
56 checks passed
@luisherranz luisherranz deleted the experiment/modules-and-import-maps-api-required-server-dependency-graph branch November 29, 2023 15:48
@github-actions github-actions bot added this to the Gutenberg 17.2 milestone Nov 29, 2023
@briangrider
Copy link

Hi all, I'm a third party dev who has been experimenting with the Interactivity API the past few versions of Gutenberg and notice that with this RC, the API is no longer working for third party blocks (at least the way it used to with just adding a viewScript and the data-wp-interactive attribute to interactive blocks). I'm getting the following errors with blocks/code that was working previously:

VM3154 index.min.js:1 Uncaught SyntaxError: Unexpected token 'export' (at VM3154 index.min.js:1:33535)
external window ["wp","interactivity"]:1 Uncaught TypeError: Cannot read properties of undefined (reading 'interactivity')
at @wordpress/interactivity (external window ["wp","interactivity"]:1:1)
at webpack_require (bootstrap:19:1)
at make namespace object:7:1
at view.js?ver=99a0ba6c6dba75ed65c4:179:3
at view.js?ver=99a0ba6c6dba75ed65c4:181:12
@wordpress/interactivity @ external window ["wp","interactivity"]:1
webpack_require @ bootstrap:19
(anonymous) @ make namespace object:7
(anonymous) @ view.js?ver=99a0ba6c6dba75ed65c4:179
(anonymous) @ view.js?ver=99a0ba6c6dba75ed65c4:181
thing/:169

Can someone point me to the new method for registering/using interactive third party blocks? Please let me know if there is a better place to ask this question. Thanks!

@luisherranz
Copy link
Member Author

luisherranz commented Nov 30, 2023

I'm working on a migration guide to explain how to use modules. I'll let you know once it's ready.

@briangrider
Copy link

Thanks @luisherranz, greatly appreciated. 🙏 Moving back to 17.02 for the time being.

@youknowriad
Copy link
Contributor

Looks like this PR had a small but noticeable impact on LCP - TTFB metrics https://www.codevitals.run/project/gutenberg

@luisherranz
Copy link
Member Author

In the Editor? That doesn't make sense…

@youknowriad
Copy link
Contributor

@luisherranz no in the frontend.

@luisherranz
Copy link
Member Author

That would imply that modules are slower than deferred scripts, which should not be.

What is it testing? Which site/theme/blocks?

@luisherranz
Copy link
Member Author

That would imply that modules are slower than deferred scripts, which should not be.

Oh, there is a difference indeed, the polyfill!

Let me make a PR to load it only when there's an import map and only when the browser doesn't support it, to see if the numbers improve.

@youknowriad
Copy link
Contributor

@oandregal knows more about this test but I believe we're used a fixed version of "twentytwentythree“ with the default content.

@luisherranz
Copy link
Member Author

Is there a way to see if this PR improves those numbers?

@youknowriad
Copy link
Contributor

Yes, you can check the logs of the performance job https://github.com/WordPress/gutenberg/actions/runs/7051192339/job/19193537419?pr=56699

But there's a margin of error, if the change is small, it's better to confirm it in the graphs post merge.

@luisherranz
Copy link
Member Author

It didn't improve the metrics, so I'll do more tests. I'll report my findings in the new PR.

@briangrider
Copy link

briangrider commented Dec 1, 2023

@luisherranz I was able to figure out how to get this all working with the module api/interactivity. Very cool features and I can see how huge this is for WP. What's the best way to stop webpack from trying to bundle the @wordpress/interactivity import so view.js files can still be built without breaking on the front end?

So far I've tried this (and every variant I could come up with including no externalsType, no experiments, "wp-interactivity", ["wp", "interactivity"], etc.) in webpack.config.js but no luck:

const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );

const { getWebpackEntryPoints } = require( '@wordpress/scripts/utils/config' );

module.exports = {
	...defaultConfig,
	entry: {
		...getWebpackEntryPoints(),
	},
	externalsType: 'module',
	experiments: {
		outputModule: true,
	},
	externals: {
		'@wordpress/interactivity': '@wordpress/interactivity',
	},
};

I'm sure it's something dumb but looking forward to any input/the guide!

@luisherranz
Copy link
Member Author

@briangrider, what I did for the create-block template was simply to opt-out from wp-scripts for now (remove the viewScript field) and don't bundle/transpile the view file at all:

This is a temporary solution because that file won't be minified and you cannot divide it into multiple files, but I hope it'd be enough for testing purposes until we figure out how to integrate it with wp-scripts (a viewModule field?).

Let me know if that works for you!

@briangrider
Copy link

@luisherranz Yep, this is what I was doing too and it works fine! Like you said, not having multiple files and no minification is where I was getting hung up but it doesn't seem like it's too big of a deal at this point because a standard viewScript (at least the ones I'm writing) tends to be around 100-200 lines or less. I think once this is in core, it'll be important to be able to use the standard wp-scripts package but in the interim, this is an easy enough way to be able to experiment with this awesome new functionality

derekblank pushed a commit that referenced this pull request Dec 7, 2023
…6143)

* Bundle `@wordpress/interactivity` as an ES module

* Add the class with a basic API

* Make it work with the Navigation block

* Add versions

* Register `@wordpress/interactivity` module

* Add query and image blocks

* Add id with module identifier to the enqueued modules

* Add requried $usage for admin/frontend

* Avoid the use of enqueue to register modules

* Refactor versions

* Switch from `both` to `['admin', 'frontend']`

* Improve comments

* Add an optional static dependency graph

* Add static and dynamic dependencies in the server

* Add the file and search blocks

* Move $version out of $args to match wp_register_script

* Improve version logic

* Add polyfill

* Fix $version using its own arg in register calls

* Add unit tests

* Refactor tests, add test get import map

* Cleaning data

* Use gutenberg_url()

* Use wp_print_script_tag

* Fix DocBlock on get_import_map()

* Load navigation module only on Gutenberg plugin

* Load query module only on Gutenberg plugin

* Load search module only on Gutenberg plugin

* Load file module only on Gutenberg plugin

* Load image module only on Gutenberg plugin

* Make registration optional

* Improve navigation logic

* Remove unnecessary check

* Fix missing view file

* Don't print the import map if it's empty

* Load the importmap polyfill locally

* Move the es-modules-shims package to the top

* Use the public functions in the tests

* Update test to be more output oriented

* Remove we from comments.

* Start using modules in the interactivity e2e tests

* Update package-lock.json

---------

Co-authored-by: Carlos Bravo <carlos.bravo@automattic.com>
@t-hamano
Copy link
Contributor

Loading modules does not seem to work with classic themes, as reported by #57370. In the classic theme, only polyfill JS is loaded.

Block Theme:

<!DOCTYPE html>
<html lang="ja">
<head>
	<!-- ... -->
	<script type="importmap">
	{"imports":{"@wordpress\/interactivity":"http:\/\/localhost:8888\/wp-content\/plugins\/gutenberg\/build\/interactivity\/index.min.js?ver=6.4.2"}}
	</script>
	<script type="module" src="http://localhost:8888/wp-content/plugins/gutenberg/build/interactivity/image.min.js?ver=6.4.2" id="@wordpress/block-library/image"></script>
	<script type="module" src="http://localhost:8888/wp-content/plugins/gutenberg/build/interactivity/navigation.min.js?ver=6.4.2" id="@wordpress/block-library/navigation-block"></script>
	<!-- ... -->
</head>
<body>
	<!-- ... -->
	<script src="http://localhost:8888/wp-content/plugins/gutenberg/build/modules/importmap-polyfill.min.js" defer=""></script>
</body>
</html>

Classic Theme:

<!DOCTYPE html>
<html lang="ja">
<head>
</head>
<body>
	<!-- ... -->
	<script src="http://localhost:8888/wp-content/plugins/gutenberg/build/modules/importmap-polyfill.min.js" defer=""></script>
</body>
</html>

My guess is that the reason why it doesn't work is as follows:

Each block's module JS is enqueued within the block's rendering callback function (Image block example). Based on the enqueued JS, it outputs the import map and module script via the wp_head hook.

For block themes, the entire HTML is pre-fetched and the entire page is displayed. Here, the block's rendering callback is called and the block's module JS is enqueued. This is before the wp_head action hook is executed.

But in the classic theme, the block is rendered when the content is rendered, i.e. when the_content is executed within the page. When the wp_head action hook is executed, the blocks have not yet been rendered, meaning the module JS for each block has not been enqueued, so the import map and module JS will not be output.

I don't have any good ideas at the moment, but I think some logic adjustments are needed.

@luisherranz
Copy link
Member Author

Thanks for the report @t-hamano, and thanks for the fix, @c4rl0sbr4v0:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Backported to WP Core Pull request that has been successfully merged into WP Core [Feature] Script Modules API Related to the Script Modules API that adds support for native ES modules and import maps [Type] Feature New feature to highlight in changelogs.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants