diff --git a/plugins/web-worker-offloading/hooks.php b/plugins/web-worker-offloading/hooks.php index 77989c00b4..d6ae5d9be4 100644 --- a/plugins/web-worker-offloading/hooks.php +++ b/plugins/web-worker-offloading/hooks.php @@ -36,17 +36,19 @@ function wwo_get_configuration(): array { } /** - * Initializes Web Worker Offloading. + * Registers defaults scripts for Web Worker Offloading. * * @since 0.1.0 + * + * @param WP_Scripts $scripts WP_Scripts instance. */ -function wwo_init(): void { +function wwo_register_default_scripts( WP_Scripts $scripts ): void { $partytown_js = file_get_contents( __DIR__ . '/build/partytown.js' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- It's a local filesystem path not a remote request. if ( false === $partytown_js ) { return; } - wp_register_script( + $scripts->add( 'web-worker-offloading', '', array(), @@ -54,7 +56,7 @@ function wwo_init(): void { array( 'in_footer' => false ) ); - wp_add_inline_script( + $scripts->add_inline_script( 'web-worker-offloading', sprintf( 'window.partytown = %s;', @@ -63,36 +65,40 @@ function wwo_init(): void { 'before' ); - wp_add_inline_script( 'web-worker-offloading', $partytown_js ); + $scripts->add_inline_script( 'web-worker-offloading', $partytown_js ); } -add_action( 'wp_enqueue_scripts', 'wwo_init' ); +add_action( 'wp_default_scripts', 'wwo_register_default_scripts' ); /** - * Marks scripts with `web-worker-offloading` dependency as async. + * Prepends web-worker-offloading to the list of scripts to print if one of the queued scripts is offloaded to a worker. * - * This is needed because scripts offloaded to a worker thread can be considered async. However, they may include `before` and `after` inline - * scripts that need sequential execution. Once marked as async, `filter_eligible_strategies()` determines if the - * script is eligible for async execution. If so, it will be offloaded to the worker thread. + * This also marks their strategy as `async`. This is needed because scripts offloaded to a worker thread can be + * considered async. However, they may include `before` and `after` inline scripts that need sequential execution. Once + * marked as async, `filter_eligible_strategies()` determines if the script is eligible for async execution. If so, it + * will be offloaded to the worker thread. * * @since 0.1.0 * - * @param string[]|mixed $script_handles Array of script handles. - * @return string[] Array of script handles. + * @param string[]|mixed $script_handles An array of enqueued script dependency handles. + * @return string[] Script handles. */ -function wwo_update_script_strategy( $script_handles ): array { - $script_handles = array_intersect( (array) $script_handles, array_keys( wp_scripts()->registered ) ); - foreach ( $script_handles as $handle ) { - if ( in_array( 'web-worker-offloading', wp_scripts()->registered[ $handle ]->deps, true ) ) { +function wwo_filter_print_scripts_array( $script_handles ): array { + $scripts = wp_scripts(); + foreach ( (array) $script_handles as $handle ) { + if ( true === (bool) $scripts->get_data( $handle, 'worker' ) ) { + $scripts->set_group( 'web-worker-offloading', false, 0 ); // Try to print in the head. + array_unshift( $script_handles, 'web-worker-offloading' ); + + // TODO: This should be reconsidered because scripts needing to be offloaded will often have after scripts. See . if ( false === wp_scripts()->get_data( $handle, 'strategy' ) ) { wp_script_add_data( $handle, 'strategy', 'async' ); // The 'defer' strategy would work as well. wp_script_add_data( $handle, 'wwo_strategy_added', true ); } } } - return $script_handles; } -add_filter( 'print_scripts_array', 'wwo_update_script_strategy' ); +add_filter( 'print_scripts_array', 'wwo_filter_print_scripts_array' ); /** * Updates script type for handles having `web-worker-offloading` as dependency. @@ -106,8 +112,7 @@ function wwo_update_script_strategy( $script_handles ): array { function wwo_update_script_type( $tag, string $handle ) { if ( is_string( $tag ) && - array_key_exists( $handle, wp_scripts()->registered ) && - in_array( 'web-worker-offloading', wp_scripts()->registered[ $handle ]->deps, true ) + (bool) wp_scripts()->get_data( $handle, 'worker' ) ) { $html_processor = new WP_HTML_Tag_Processor( $tag ); diff --git a/plugins/web-worker-offloading/readme.txt b/plugins/web-worker-offloading/readme.txt index e1fb6f3fe3..774ff9139d 100644 --- a/plugins/web-worker-offloading/readme.txt +++ b/plugins/web-worker-offloading/readme.txt @@ -13,4 +13,15 @@ Offload JavaScript execution to a Web Worker. This plugin offloads JavaScript execution to a Web Worker, improving performance by freeing up the main thread. -In order to opt-in a script to be loaded in a worker, simply add the `web-worker-offloading` script as a dependency. +In order to opt-in a script to be loaded in a worker, simply add `worker` script data to a registered script. For example, +if you have a script registered with the handle of `foo`, opt-in to offload it to a web worker by doing: + +` +wp_script_add_data( 'foo', 'worker', true ); +` + +== Frequently Asked Questions == + += Why are my offloaded scripts not working and I see a 404 error in the console for `partytown-sandbox-sw.html`? = + +If you find that your offloaded scripts aren't working while also seeing a 404 error in the console for a file at `/wp-content/plugins/web-worker-offloading/build/partytown-sandbox-sw.html?1727389399791` then it's likely you have Chrome DevTools open with the "Bypass for Network" toggle enabled in the Application panel. diff --git a/plugins/web-worker-offloading/tests/test-web-worker-offloading.php b/plugins/web-worker-offloading/tests/test-web-worker-offloading.php index f587a5495a..e13598d03b 100644 --- a/plugins/web-worker-offloading/tests/test-web-worker-offloading.php +++ b/plugins/web-worker-offloading/tests/test-web-worker-offloading.php @@ -60,13 +60,13 @@ static function ( $config ) { } /** - * @covers ::wwo_init + * @covers ::wwo_register_default_scripts */ - public function test_wwo_init(): void { - $this->assertEquals( 10, has_action( 'wp_enqueue_scripts', 'wwo_init' ) ); + public function test_wwo_register_default_scripts(): void { + $this->assertEquals( 10, has_action( 'wp_default_scripts', 'wwo_register_default_scripts' ) ); // Register scripts. - wwo_init(); + wp_scripts(); $wp_content_dir = WP_CONTENT_DIR; $partytown_config = wwo_get_configuration(); @@ -83,12 +83,6 @@ public function test_wwo_init(): void { ); $this->assertEquals( file_get_contents( $partytown_lib . 'partytown.js' ), $after_data ); $this->assertTrue( wp_script_is( 'web-worker-offloading', 'registered' ) ); - - // Ensure that Partytown is enqueued when a script depends on it. - wp_enqueue_script( 'partytown-test', 'https://example.com/test.js', array( 'web-worker-offloading' ) ); - - $this->assertTrue( wp_script_is( 'web-worker-offloading', 'enqueued' ) ); - $this->assertTrue( wp_script_is( 'partytown-test', 'enqueued' ) ); } /** @@ -107,64 +101,72 @@ public static function data_update_script_types(): array { ), 'add-script-for-web-worker-offloading' => array( 'set_up' => static function (): void { - wp_enqueue_script( 'foo', 'https://example.com/foo.js', array( 'web-worker-offloading' ), '1.0.0', true ); + wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), '1.0.0', true ); + wp_script_add_data( 'foo', 'worker', true ); }, 'expected' => '{{ wwo_config }}{{ wwo_inline_script }}', 'doing_it_wrong' => false, ), 'add-defer-script-for-web-worker-offloading' => array( 'set_up' => static function (): void { - wp_enqueue_script( 'foo', 'https://example.com/foo.js', array( 'web-worker-offloading' ), '1.0.0', array( 'strategy' => 'defer' ) ); + wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), '1.0.0', array( 'strategy' => 'defer' ) ); + wp_script_add_data( 'foo', 'worker', 1 ); }, 'expected' => '{{ wwo_config }}{{ wwo_inline_script }}', 'doing_it_wrong' => false, ), 'add-async-script-for-web-worker-offloading' => array( 'set_up' => static function (): void { - wp_enqueue_script( 'foo', 'https://example.com/foo.js', array( 'web-worker-offloading' ), '1.0.0', array( 'strategy' => 'async' ) ); + wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), '1.0.0', array( 'strategy' => 'async' ) ); + wp_script_add_data( 'foo', 'worker', true ); }, 'expected' => '{{ wwo_config }}{{ wwo_inline_script }}', 'doing_it_wrong' => false, ), 'add-script-for-web-worker-offloading-with-before-data' => array( 'set_up' => static function (): void { - wp_enqueue_script( 'foo', 'https://example.com/foo.js', array( 'web-worker-offloading' ), '1.0.0', true ); + wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), '1.0.0', true ); wp_add_inline_script( 'foo', 'console.log("Hello, World!");', 'before' ); + wp_script_add_data( 'foo', 'worker', true ); }, 'expected' => '{{ wwo_config }}{{ wwo_inline_script }}', 'doing_it_wrong' => false, ), 'add-script-for-web-worker-offloading-with-after-data' => array( 'set_up' => static function (): void { - wp_enqueue_script( 'foo', 'https://example.com/foo.js', array( 'web-worker-offloading' ), '1.0.0', true ); + wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), '1.0.0', true ); wp_add_inline_script( 'foo', 'console.log("Hello, World!");', 'after' ); + wp_script_add_data( 'foo', 'worker', true ); }, 'expected' => '{{ wwo_config }}{{ wwo_inline_script }}', 'doing_it_wrong' => true, ), 'add-script-for-web-worker-offloading-with-before-and-after-data' => array( 'set_up' => static function (): void { - wp_enqueue_script( 'foo', 'https://example.com/foo.js', array( 'web-worker-offloading' ), '1.0.0', true ); + wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), '1.0.0', true ); wp_add_inline_script( 'foo', 'console.log("Hello, World!");', 'before' ); wp_add_inline_script( 'foo', 'console.log("Hello, World!");', 'after' ); + wp_script_add_data( 'foo', 'worker', true ); }, 'expected' => '{{ wwo_config }}{{ wwo_inline_script }}', 'doing_it_wrong' => true, ), 'add-async-script-for-web-worker-offloading-with-before-and-after-data' => array( 'set_up' => static function (): void { - wp_enqueue_script( 'foo', 'https://example.com/foo.js', array( 'web-worker-offloading' ), '1.0.0', array( 'strategy' => 'async' ) ); + wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), '1.0.0', array( 'strategy' => 'async' ) ); wp_add_inline_script( 'foo', 'console.log("Hello, World!");', 'before' ); wp_add_inline_script( 'foo', 'console.log("Hello, World!");', 'after' ); + wp_script_add_data( 'foo', 'worker', true ); }, 'expected' => '{{ wwo_config }}{{ wwo_inline_script }}', 'doing_it_wrong' => true, ), 'add-defer-script-for-web-worker-offloading-with-before-and-after-data' => array( 'set_up' => static function (): void { - wp_enqueue_script( 'foo', 'https://example.com/foo.js', array( 'web-worker-offloading' ), '1.0.0', array( 'strategy' => 'defer' ) ); + wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), '1.0.0', array( 'strategy' => 'defer' ) ); wp_add_inline_script( 'foo', 'console.log("Hello, World!");', 'before' ); wp_add_inline_script( 'foo', 'console.log("Hello, World!");', 'after' ); + wp_script_add_data( 'foo', 'worker', true ); }, 'expected' => '{{ wwo_config }}{{ wwo_inline_script }}', 'doing_it_wrong' => true, @@ -176,7 +178,7 @@ public static function data_update_script_types(): array { * Test `wwo_update_script_type`. * * @covers ::wwo_update_script_type - * @covers ::wwo_update_script_strategy + * @covers ::wwo_filter_print_scripts_array * * @dataProvider data_update_script_types * @@ -185,22 +187,7 @@ public static function data_update_script_types(): array { * @param bool $doing_it_wrong Whether to expect a `_doing_it_wrong` notice. */ public function test_update_script_types( Closure $set_up, string $expected, bool $doing_it_wrong ): void { - // Setup. - wwo_init(); - - $wwo_config_data = wp_scripts()->get_inline_script_data( 'web-worker-offloading', 'before' ); - $wwo_inline_script_data = wp_scripts()->get_inline_script_data( 'web-worker-offloading', 'after' ); - - $expected = str_replace( - '{{ wwo_config }}', - wp_get_inline_script_tag( $wwo_config_data, array( 'id' => 'web-worker-offloading-js-before' ) ), - $expected - ); - $expected = str_replace( - '{{ wwo_inline_script }}', - wp_get_inline_script_tag( $wwo_inline_script_data, array( 'id' => 'web-worker-offloading-js-after' ) ), - $expected - ); + $expected = $this->replace_placeholders( $expected ); if ( $doing_it_wrong ) { $this->setExpectedIncorrectUsage( 'wwo_update_script_type' ); @@ -215,6 +202,98 @@ public function test_update_script_types( Closure $set_up, string $expected, boo $this->assertEquals( $expected, $actual ); } + /** + * Test head and footer scripts. + * + * @covers ::wwo_update_script_type + * @covers ::wwo_filter_print_scripts_array + */ + public function test_head_and_footer_scripts(): void { + wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), '1.0.0', false ); + wp_script_add_data( 'foo', 'worker', true ); + + $this->assertEquals( + $this->replace_placeholders( '{{ wwo_config }}{{ wwo_inline_script }}' ), + trim( get_echo( 'wp_print_head_scripts' ) ) + ); + + wp_enqueue_script( 'bar', 'https://example.com/bar.js', array(), '1.0.0', true ); + wp_script_add_data( 'bar', 'worker', true ); + + $this->assertEquals( + $this->replace_placeholders( '' ), + trim( get_echo( 'wp_print_footer_scripts' ) ) + ); + } + + /** + * Test only head script. + * + * @covers ::wwo_update_script_type + * @covers ::wwo_filter_print_scripts_array + */ + public function test_only_head_script(): void { + wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), '1.0.0', false ); + wp_script_add_data( 'foo', 'worker', true ); + + $this->assertEquals( + $this->replace_placeholders( '{{ wwo_config }}{{ wwo_inline_script }}' ), + trim( get_echo( 'wp_print_head_scripts' ) ) + ); + + wp_enqueue_script( 'bar', 'https://example.com/bar.js', array(), '1.0.0', true ); + + $this->assertEquals( + $this->replace_placeholders( '' ), + trim( get_echo( 'wp_print_footer_scripts' ) ) + ); + } + + /** + * Test only footer script. + * + * @covers ::wwo_update_script_type + * @covers ::wwo_filter_print_scripts_array + */ + public function test_only_footer_script(): void { + wp_enqueue_script( 'foo', 'https://example.com/foo.js', array(), '1.0.0', false ); + + $this->assertEquals( + $this->replace_placeholders( '' ), + trim( get_echo( 'wp_print_head_scripts' ) ) + ); + + wp_enqueue_script( 'bar', 'https://example.com/bar.js', array(), '1.0.0', true ); + wp_script_add_data( 'bar', 'worker', true ); + + $this->assertEquals( + $this->replace_placeholders( '{{ wwo_config }}{{ wwo_inline_script }}' ), + trim( get_echo( 'wp_print_footer_scripts' ) ) + ); + } + + /** + * Replace placeholders. + * + * @param string $template Template. + * @return string Template with placeholders replaced. + */ + private function replace_placeholders( string $template ): string { + $wwo_config_data = wp_scripts()->get_inline_script_data( 'web-worker-offloading', 'before' ); + $wwo_inline_script_data = wp_scripts()->get_inline_script_data( 'web-worker-offloading', 'after' ); + + $template = str_replace( + '{{ wwo_config }}', + wp_get_inline_script_tag( $wwo_config_data, array( 'id' => 'web-worker-offloading-js-before' ) ), + $template + ); + return str_replace( + '{{ wwo_inline_script }}', + wp_get_inline_script_tag( $wwo_inline_script_data, array( 'id' => 'web-worker-offloading-js-after' ) ), + $template + ); + } + /** * Reset WP_Scripts and WP_Styles. */