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

Integrate with WooCommerce to offload Google Analytics to Web Worker #1563

Merged
merged 24 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
eb523a1
Add helper.php
westonruter Sep 27, 2024
f430916
Improve normalization in tests
westonruter Sep 27, 2024
84fc204
Offload inline scripts to web worker as well
westonruter Sep 27, 2024
796996e
Add WooCommerce integration with Google Analytics
westonruter Sep 27, 2024
ac4744f
Clean up wwo_update_script_type() logic
westonruter Sep 28, 2024
c2d73f0
Avoid prepending WWO script multiple times and only do so at end of p…
westonruter Sep 29, 2024
844884c
Update wwo_mark_script_for_offloading() to pass multiple script handles
westonruter Sep 29, 2024
fc70dec
Enable Partytown debug when WP_DEBUG and SCRIPT_DEBUG
westonruter Sep 29, 2024
5986738
Add WooCommerce-specific configuration
westonruter Sep 29, 2024
d188626
Add ga4w to mainWindowAccessors
westonruter Sep 29, 2024
805ec70
Use forked version of Partytown with wp.i18n workaround
westonruter Sep 29, 2024
efe3506
Add missing since tag
westonruter Sep 29, 2024
e9ac924
Update readme with FAQs and filter docs
westonruter Sep 29, 2024
5a7d421
Cast apply_filters() return value to array
westonruter Sep 29, 2024
ac9c55f
Add private access tags
westonruter Sep 29, 2024
a0af3d0
Note additional plugin support for the future
westonruter Sep 29, 2024
649e617
Explain that inline before/after scripts are also offloaded to worker
westonruter Sep 29, 2024
74b93c7
Use plugin active tests instead of looking for specific slugs
westonruter Sep 29, 2024
89ea382
Extend window.partytown if it already exists
westonruter Sep 29, 2024
2f38b13
Facilitate extending Partytown config with non-JSON serializable data
westonruter Sep 29, 2024
41434a2
Add analytics to list of tags
westonruter Sep 29, 2024
cfea71a
Use example URL
westonruter Sep 29, 2024
db9d10e
Differentiate before/after inline scripts in tests
westonruter Sep 29, 2024
998d19f
Revert "Remove web-worker-offloading from plugins.json for now since …
westonruter Sep 30, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"npm": ">=10.2.3"
},
"dependencies": {
"@builder.io/partytown": "^0.10.2",
"@builder.io/partytown": "github:westonruter/partytown#add/wp-i18n-workaround",
"web-vitals": "4.2.3"
},
"devDependencies": {
Expand Down
1 change: 1 addition & 0 deletions plugins.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"optimization-detective",
"performance-lab",
"speculation-rules",
"web-worker-offloading",
"webp-uploads"
]
}
70 changes: 70 additions & 0 deletions plugins/web-worker-offloading/helper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php
/**
* Helpers for Web Worker Offloading.
*
* @since 0.1.0
* @package web-worker-offloading
*/

if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}

/**
* Gets configuration for Web Worker Offloading.
*
* @since 0.1.0
* @link https://partytown.builder.io/configuration
* @link https://github.com/BuilderIO/partytown/blob/b292a14047a0c12ca05ba97df1833935d42fdb66/src/lib/types.ts#L393-L548
*
* @return array<string, mixed> Configuration for Partytown.
*/
function wwo_get_configuration(): array {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just moved this function from hooks.php to helpers.php

$config = array(
'lib' => wp_parse_url( plugin_dir_url( __FILE__ ), PHP_URL_PATH ) . 'build/',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There used to be forward in here, can you clarify why you removed it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, because it was empty so it wasn't having any purpose.

);

if ( WP_DEBUG && SCRIPT_DEBUG ) {
$config['debug'] = true;
}
Comment on lines +27 to +29
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about always having this key set, and making the conditional only around whether it's true or false?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, because it defaults to false anyway, and by not setting it we avoid sending (a tiny amount) of bytes down the wire.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's probably negligible though for performance. I'd say initializing properties here makes it easier to understand what's available, also since this PHP layer is entirely separate from the actual JS API where this is consumed.

Copy link
Member Author

@westonruter westonruter Sep 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. If someone wants to know what is available they should look at the docs which are linked in the source code:

* @link https://partytown.builder.io/configuration
* @link https://github.com/BuilderIO/partytown/blob/b292a14047a0c12ca05ba97df1833935d42fdb66/src/lib/types.ts#L393-L548

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't want to have to supply the default values for very single configuration option (many of which are under-documented in Partytown).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair. Let's leave this as is then.


/**
* Add configuration for Web Worker Offloading.
*
* Many of the configuration options are not documented publicly, so refer to the TypeScript definitions.
* Additionally, not all of the configuration options (e.g. functions) can be serialized as JSON and must instead be
* defined in JavaScript instead. To do so, use the following PHP code instead of filtering `wwo_configuration`:
*
* add_action(
* 'wp_enqueue_scripts',
* function () {
* wp_add_inline_script(
* 'web-worker-offloading',
* <<<JS
* window.partytown = {
* ...(window.partytown || {}),
* resolveUrl: (url, location, type) => {
* if (type === 'script') {
* const proxyUrl = new URL('https://my-reverse-proxy.example.com/');
* proxyUrl.searchParams.append('url', url.href);
* return proxyUrl;
* }
* return url;
* },
* };
* JS,
* 'before'
* );
* }
* );
*
* There are also many configuration options which are not documented, so refer to the TypeScript definitions.
*
* @since 0.1.0
* @link https://partytown.builder.io/configuration
* @link https://github.com/BuilderIO/partytown/blob/b292a14047a0c12ca05ba97df1833935d42fdb66/src/lib/types.ts#L393-L548
*
* @param array<string, mixed> $config Configuration for Partytown.
*/
return (array) apply_filters( 'wwo_configuration', $config );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be good to add some sanitization here.

For example, since several supported properties/keys are arrays and several plugins/sources may add similar entries to them, we could sanitize these arrays using array_unique(). Just thinking about what this PR implements for WooCommerce for example allowlisting wp, I'm sure lots of scripts would need that.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would agree with you, but I think sanitizing for this initial version could go down a rabbit hole.

The number of possible configuration options is large and the value types can be complex: https://github.com/BuilderIO/partytown/blob/main/src/lib/types.ts

We could implement a JSON Schema to mirror the TypeScript types and use this to provide usage warnings. However, I think this validation would be better done inside Partytown itself where the types are being defined.

In the case of mainWindowAccessors, there's no need for array_unique() because Partytown is using the value like this:

              } else if (webWorkerCtx.$config$.mainWindowAccessors?.includes(propName)) {
                return getter(this, [propName]);

So I think we should consider what validation and sanitization we do as part of this plugin for a subsequent release.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it depends also on how we want to use their configuration options. We don't need to holistically capture everything Partytown supports, that would indeed be a rabbit hole. But for instance, it seems that mainWindowAccessors will be relevant in how we would use Partytown for quite a few integrations, so we could do some sanitization there where it makes sense. In other words, I think we should do this based on demand.

As long as all of these 4 arguments only support an array of strings, and as long as duplicate values are not a problem, it's probably okay not to sanitize anything. So we can move ahead with this as is, but I think we should look out for when things are more complicated to use, or could be of more than one type, and especially then we should do some sanitization for how we want those arguments to be used.

}
97 changes: 35 additions & 62 deletions plugins/web-worker-offloading/hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,35 +10,11 @@
exit; // Exit if accessed directly.
}

/**
* Gets configuration for Web Worker Offloading.
*
* @since 0.1.0
* @link https://partytown.builder.io/configuration
*
* @return array{ debug?: bool, forward?: non-empty-string[], lib: non-empty-string, loadScriptsOnMainThread?: non-empty-string[], nonce?: non-empty-string } Configuration for Partytown.
*/
function wwo_get_configuration(): array {
$config = array(
'lib' => wp_parse_url( plugin_dir_url( __FILE__ ), PHP_URL_PATH ) . 'build/',
'forward' => array(),
);

/**
* Add configuration for Web Worker Offloading.
*
* @since 0.1.0
* @link https://partytown.builder.io/configuration
*
* @param array{ debug?: bool, forward?: non-empty-string[], lib: non-empty-string, loadScriptsOnMainThread?: non-empty-string[], nonce?: non-empty-string } $config Configuration for Partytown.
*/
return apply_filters( 'wwo_configuration', $config );
}

/**
* Registers defaults scripts for Web Worker Offloading.
*
* @since 0.1.0
* @access private
*
* @param WP_Scripts $scripts WP_Scripts instance.
*/
Expand All @@ -59,7 +35,7 @@ function wwo_register_default_scripts( WP_Scripts $scripts ): void {
$scripts->add_inline_script(
'web-worker-offloading',
sprintf(
'window.partytown = %s;',
'window.partytown = {...(window.partytown || {}), ...%s};',
wp_json_encode( wwo_get_configuration() )
),
'before'
Expand All @@ -72,12 +48,8 @@ function wwo_register_default_scripts( WP_Scripts $scripts ): void {
/**
* Prepends web-worker-offloading to the list of scripts to print if one of the queued scripts is offloaded to a worker.
*
* 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
* @access private
*
* @param string[]|mixed $script_handles An array of enqueued script dependency handles.
* @return string[] Script handles.
Expand All @@ -88,61 +60,62 @@ function wwo_filter_print_scripts_array( $script_handles ): array {
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 <https://github.com/WordPress/performance/pull/1497/files#r1733538721>.
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 );
}
Comment on lines -92 to -96
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline after scripts are often used with analytics, and they need to be offloaded to a worker (maybe always?).

break;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We break because we only need to prepend the WWO script when encountering the first script that is to be offloaded to a worker.

}
}
return $script_handles;
}
add_filter( 'print_scripts_array', 'wwo_filter_print_scripts_array' );
add_filter( 'print_scripts_array', 'wwo_filter_print_scripts_array', PHP_INT_MAX );
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The priority is increased to PHP_INT_MAX so that functions like wwo_mark_scripts_for_offloading() can add print_scripts_array filters to mark scripts for worker offloading easily at earlier priorities.


/**
* Updates script type for handles having `web-worker-offloading` as dependency.
*
* @since 0.1.0
* @access private
*
* @param string|mixed $tag Script tag.
* @param string $handle Script handle.
* @return string|mixed Script tag with type="text/partytown" for eligible scripts.
*/
function wwo_update_script_type( $tag, string $handle ) {
if (
is_string( $tag ) &&
is_string( $tag )
&&
(bool) wp_scripts()->get_data( $handle, 'worker' )
) {
$html_processor = new WP_HTML_Tag_Processor( $tag );

while ( $html_processor->next_tag( array( 'tag_name' => 'SCRIPT' ) ) ) {
if ( $html_processor->get_attribute( 'id' ) !== "{$handle}-js" ) {
continue;
}
if ( null === $html_processor->get_attribute( 'async' ) && null === $html_processor->get_attribute( 'defer' ) ) {
_doing_it_wrong(
'wwo_update_script_type',
esc_html(
sprintf(
/* translators: %s: script handle */
__( 'Unable to offload "%s" script to a worker. Script will continue to load in the main thread.', 'web-worker-offloading' ),
$handle
)
),
'Web Worker Offloading 0.1.0'
);
} else {
if ( $html_processor->get_attribute( 'id' ) === "{$handle}-js" ) {
$html_processor->set_attribute( 'type', 'text/partytown' );
$tag = $html_processor->get_updated_html();
break;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only one tag should have a given ID, so once we found it, we can bail.

}
if ( true === wp_scripts()->get_data( $handle, 'wwo_strategy_added' ) ) {
$html_processor->remove_attribute( 'async' );
$html_processor->remove_attribute( 'data-wp-strategy' );
}
$tag = $html_processor->get_updated_html();
}
}

return $tag;
}
add_filter( 'script_loader_tag', 'wwo_update_script_type', 10, 2 );

/**
* Filters inline script attributes to offload to a worker if the script has been opted-in.
*
* @since 0.1.0
* @access private
*
* @param array<string, mixed>|mixed $attributes Attributes.
* @return array<string, mixed> Attributes.
*/
function wwo_filter_inline_script_attributes( $attributes ): array {
$attributes = (array) $attributes;
if (
isset( $attributes['id'] )
&&
1 === preg_match( '/^(?P<handle>.+)-js-(?:before|after)$/', $attributes['id'], $matches )
&&
(bool) wp_scripts()->get_data( $matches['handle'], 'worker' )
) {
$attributes['type'] = 'text/partytown';
}
return $attributes;
}
add_filter( 'wp_inline_script_attributes', 'wwo_filter_inline_script_attributes' );
3 changes: 2 additions & 1 deletion plugins/web-worker-offloading/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,6 @@

define( 'WEB_WORKER_OFFLOADING_VERSION', '0.1.0' );

// Load the hooks.
require_once __DIR__ . '/helper.php';
require_once __DIR__ . '/hooks.php';
require_once __DIR__ . '/third-party.php';
78 changes: 75 additions & 3 deletions plugins/web-worker-offloading/readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,95 @@ Tested up to: 6.6
Stable tag: 0.1.0
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Tags: performance, JavaScript, web worker, partytown
Tags: performance, JavaScript, web worker, partytown, analytics

Offload JavaScript execution to a Web Worker.

== Description ==

This plugin offloads JavaScript execution to a Web Worker, improving performance by freeing up the main thread.
This plugin offloads JavaScript execution to a Web Worker, improving performance by freeing up the main thread. This should translate into improved [Interaction to Next Paint](https://web.dev/articles/inp) (INP) scores.

In order to opt-in a script to be loaded in a worker, simply add `worker` script data to a registered script. For example,
⚠ _This functionality is experimental._ ⚠

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 );
`

Unlike with the script loading strategies (async/defer), any inline before/after scripts associated with the worker-offloaded registered script will also be offloaded to the worker, whereas with the script strategies an inline after script would block the script from being delayed.

Otherwise, the plugin currently ships with built-in integrations to offload Google Analytics to a web worker for the following plugin:

* [WooCommerce](https://wordpress.org/plugins/woocommerce/)

Support for [Site Kit by Google](https://wordpress.org/plugins/google-site-kit/) and [Rank Math SEO](https://wordpress.org/plugins/seo-by-rank-math/) are [planned](https://github.com/WordPress/performance/issues/1455).

Please monitor your analytics once activating to ensure all the expected events are being logged. At the same time, monitor your INP scores to check for improvement.

This plugin relies on the [Partytown 🎉](https://partytown.builder.io/) library by Builder.io, released under the MIT license. This library is in beta and there are quite a few [open bugs](https://github.com/BuilderIO/partytown/issues?q=is%3Aopen+is%3Aissue+label%3Abug).

The [Partytown configuration](https://partytown.builder.io/configuration) can be modified via the `wwo_configuration` filter. For example:

`
<?php
add_filter( 'wwo_configuration', function ( $config ) {
$config['mainWindowAccessors'][] = 'wp'; // Make the wp global available in the worker (e.g. wp.i18n and wp.hooks).
return $config;
} );
`

However, not all of the configuration options can be serialized to JSON in this way, for example the `resolveUrl` configuration is a function. To specify this, you can add an inline script as follows.

`
<?php
add_action(
'wp_enqueue_scripts',
function () {
wp_add_inline_script(
'web-worker-offloading',
<<<JS
window.partytown = {
...(window.partytown || {}),
resolveUrl: (url, location, type) => {
if (type === 'script') {
const proxyUrl = new URL('https://my-reverse-proxy.example.com/');
proxyUrl.searchParams.append('url', url.href);
return proxyUrl;
}
return url;
},
};
JS,
'before'
);
}
);
`

There are also many configuration options which are not documented, so refer to the [TypeScript definitions](https://github.com/BuilderIO/partytown/blob/b292a14047a0c12ca05ba97df1833935d42fdb66/src/lib/types.ts#L393-L548).

== 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.

= Where can I report security bugs? =

The Performance team and WordPress community take security bugs seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions.

To report a security issue, please visit the [WordPress HackerOne](https://hackerone.com/wordpress) program.

= How can I contribute to the plugin? =

Contributions are always welcome! Learn more about how to get involved in the [Core Performance Team Handbook](https://make.wordpress.org/performance/handbook/get-involved/).

The [plugin source code](https://github.com/WordPress/performance/tree/trunk/plugins/web-worker-offloading) is located in the [WordPress/performance](https://github.com/WordPress/performance) repo on GitHub.

== Changelog ==

= 0.1.0 =

* Initial release.
Loading