Open westonruter opened 1 year ago
Actually, what I tested was the best case because this is without any of the integrations enabled (reCAPTCHA and Stripe). When those are also active, then the homepage includes yet more styles and scripts. In total:
<link rel='stylesheet' id='wpcf7-stripe-css' href='http://localhost:10033/wp-content/plugins/contact-form-7/modules/stripe/style.css?ver=5.8' media='all' />
<link rel='stylesheet' id='contact-form-7-css' href='http://localhost:10033/wp-content/plugins/contact-form-7/includes/css/styles.css?ver=5.8' media='all' />
...
<script src='http://localhost:10033/wp-content/plugins/contact-form-7/includes/swv/js/index.js?ver=5.8' id='swv-js'></script>
<script id='contact-form-7-js-extra'>
var wpcf7 = {"api":{"root":"http:\/\/localhost:10033\/wp-json\/","namespace":"contact-form-7\/v1"}};
</script>
<script src='http://localhost:10033/wp-content/plugins/contact-form-7/includes/js/index.js?ver=5.8' id='contact-form-7-js'></script>
<script src='http://localhost:10033/wp-includes/js/dist/vendor/wp-polyfill-inert.min.js?ver=3.1.2' id='wp-polyfill-inert-js'></script>
<script src='http://localhost:10033/wp-includes/js/dist/vendor/regenerator-runtime.min.js?ver=0.13.11' id='regenerator-runtime-js'></script>
<script src='http://localhost:10033/wp-includes/js/dist/vendor/wp-polyfill.min.js?ver=3.15.0' id='wp-polyfill-js'></script>
<script id='wpcf7-stripe-js-extra'>
var wpcf7_stripe = {"publishable_key":"foo"};
</script>
<script src='http://localhost:10033/wp-content/plugins/contact-form-7/modules/stripe/index.js?ver=5.8' id='wpcf7-stripe-js'></script>
<script src='https://www.google.com/recaptcha/api.js?render=foo&ver=3.0' id='google-recaptcha-js'></script>
<script id='wpcf7-recaptcha-js-extra'>
var wpcf7_recaptcha = {"sitekey":"foo","actions":{"homepage":"homepage","contactform":"contactform"}};
</script>
<script src='http://localhost:10033/wp-content/plugins/contact-form-7/modules/recaptcha/index.js?ver=5.8' id='wpcf7-recaptcha-js'></script>
None of these scripts seem to be needed as, again, there are no contact forms on the page.
And now re-running with the plugin active and the integrations enabled:
$ npm run research -- benchmark-web-vitals -u http://localhost:10033/ -n 100
> wpp-research@ research /home/westonruter/repos/wpp-research
> ./cli/run.mjs "benchmark-web-vitals" "-u" "http://localhost:10033/" "-n" "100"
╔═══════════════════╤═════════════════════════╗
║ URL │ http://localhost:10033/ ║
╟───────────────────┼─────────────────────────╢
║ Success Rate │ 100% ║
╟───────────────────┼─────────────────────────╢
║ FCP (median) │ 112.2 ║
╟───────────────────┼─────────────────────────╢
║ LCP (median) │ 112.2 ║
╟───────────────────┼─────────────────────────╢
║ TTFB (median) │ 34.7 ║
╟───────────────────┼─────────────────────────╢
║ LCP-TTFB (median) │ 76.7 ║
╚═══════════════════╧═════════════════════════╝
Here now the LCP-TTFB goes up to 76.7ms compared with the above results of 36.4ms when the plugin is deactivated. So the plugin increases TTFB-LCP from 40.1ms to 76.7ms, an increase of 36.4ms or ~2x.
Out of curiosity, I modified an HTTP Archive query I had previously written which listed out all blocking head
scripts on WordPress sites to instead list all scripts on WordPress sites, whether in the head
or the footer (and disregarding async
/defer
).
I found that scripts from CF7 are the 4th most commonly-found in HTTP Archive, and that the specific scripts includes/js/index.js
and includes/swv/js/index.js
are the 6th and 10th most commonly occurring scripts, respectively.
index | source | source_count |
---|---|---|
1 | plugin:elementor | 10,313,055 |
2 | plugin:woocommerce | 7,672,985 |
3 | theme:Avada | 5,538,430 |
4 | plugin:contact-form-7 | 5,300,922 |
5 | plugin:elementor-pro | 4,456,216 |
6 | plugin:contact-form-7:includes/js/index.js | 2,436,062 |
7 | theme:Divi | 2,272,539 |
8 | plugin:revslider | 2,114,175 |
9 | plugin:js_composer | 1,986,907 |
10 | plugin:contact-form-7:includes/swv/js/index.js | 1,893,741 |
11 | theme:bridge | 1,652,177 |
12 | plugin:elementor:assets/js/frontend.min.js | 1,618,387 |
13 | plugin:elementor:assets/js/frontend-modules.min.js | 1,609,515 |
14 | plugin:elementor:assets/lib/waypoints/waypoints.min.js | 1,605,195 |
15 | plugin:elementor:assets/js/webpack.runtime.min.js | 1,577,560 |
16 | theme:enfold | 1,544,323 |
17 | plugin:fusion-builder | 1,344,788 |
18 | plugin:woocommerce:assets/js/jquery-blockui/jquery.blockUI.min.js | 1,288,881 |
19 | plugin:woocommerce:assets/js/frontend/woocommerce.min.js | 1,280,037 |
20 | plugin:woocommerce:assets/js/js-cookie/js.cookie.min.js | 1,275,146 |
21 | theme:woodmart | 1,232,548 |
22 | plugin:woocommerce:assets/js/frontend/add-to-cart.min.js | 1,143,326 |
23 | plugin:gravityforms | 1,111,725 |
24 | plugin:elementor-pro:assets/js/frontend.min.js | 1,039,935 |
25 | theme:oceanwp | 1,003,417 |
26 | plugin:elementor-pro:assets/js/webpack-pro.runtime.min.js | 998,216 |
27 | plugin:woocommerce:assets/js/frontend/cart-fragments.min.js | 890,766 |
28 | plugin:revslider:public/assets/js/rs6.min.js | 833,623 |
29 | plugin:ultimate-member | 794,052 |
30 | plugin:elementor:assets/lib/swiper/swiper.min.js | 779,418 |
31 | plugin:revslider:public/assets/js/rbtools.min.js | 772,622 |
32 | plugin:elementor:assets/lib/dialog/dialog.min.js | 755,919 |
33 | plugin:elementor:assets/lib/share-link/share-link.min.js | 751,734 |
34 | plugin:js_composer:assets/js/dist/js_composer_front.min.js | 750,164 |
35 | theme:salient | 749,404 |
36 | plugin:elementor-pro:assets/lib/sticky/jquery.sticky.min.js | 701,773 |
37 | plugin:elementor:assets/js/preloaded-modules.min.js | 690,017 |
38 | plugin:contact-form-7:modules/recaptcha/index.js | 671,401 |
39 | plugin:jetpack | 665,232 |
40 | plugin:elementor-pro:assets/lib/smartmenus/jquery.smartmenus.min.js | 624,706 |
41 | theme:betheme | 600,101 |
42 | plugin:wpforms-lite | 583,171 |
43 | plugin:elementskit-lite | 565,598 |
44 | plugin:elementor:assets/lib/font-awesome/js/v4-shims.min.js | 539,512 |
45 | plugin:elementor-pro:assets/js/elements-handlers.min.js | 539,328 |
46 | theme:flatsome | 537,902 |
47 | theme:Divi:core/admin/js/common.js | 496,193 |
48 | plugin:elementor-pro:assets/js/preloaded-elements-handlers.min.js | 456,208 |
49 | plugin:thrive-visual-editor | 455,100 |
50 | plugin:LayerSlider | 450,092 |
Conditionally enqueueing these scripts based on whether a contact form is on the page should drastically reduce the impact that CF7 scripts have on JavaScript downloads across the web.
Don't the Google recaptcha v3 scripts need to load on all pages for it to work properly? Isn't that how Google tells what is usual behaviour, by running on all pages for a site?
Humm, apparently so:
Placement on your website
reCAPTCHA v3 will never interrupt your users, so you can run it whenever you like without affecting conversion. reCAPTCHA works best when it has the most context about interactions with your site, which comes from seeing both legitimate and abusive behavior. For this reason, we recommend including reCAPTCHA verification on forms or actions as well as in the background of pages for analytics.
I'm going to inquire further.
@PhilMakower I've reverted the change to reCAPTCHA in #1279 via 9d8fee27d8b6515b3c301a1966236b3a8a4744eb.
Site owners should know which pages on their site have a contact form, so we recommend that site owners themselves decide on which pages Contact Form 7's scripts are necessary. This is the surest way to control script loading. Your approach is indeed elegant and maybe works nicely on a vanilla WordPress install, but in reality, a plugin have to live in a complicated world with thousand of different plugins and themes. I think it would be difficult to work without causing conflicts.
@takayukister I hope you might reconsider. Excessive JavaScript is one of the worst performance problems on the web today, as you also affirm it is wasteful for your plugin to add its JS and CSS to every page in your blog post. My concern about the instructions in your post is that most users are not developers: they won’t realize the negative performance impact of the extra scripts and they certainly won’t feel comfortable adding PHP code to improve their site’s performance. You have such a popular plugin that you have a unique opportunity to make a big impact on the health of the web.
Your post says:
there is a technical difficulty for a plugin in knowing whether the page contains contact forms or not at the start of loading. [...] Note that wpcf7_enqueue_scripts() and wpcf7_enqueue_styles() must be called before wp_head() is called.
However, your scripts are already being printed in the footer. Therefore, is there actually a difficulty here? Also, whenever a script or stylesheet is enqueued after wp_head
it will get printed at wp_footer
: so why must they get called before wp_head
?
I can see an argument for not changing the behavior for enqueueing the stylesheet as I did in my PR, since moving a stylesheet to the footer could indeed cause compatibility problems with themes/plugins in regards to the CSS cascade. Otherwise, do you have any specific plugins and themes in which you anticipate there being a conflict?
What if I reverted the changes to the stylesheet so that it continues printing in the head
, but to conditionally enqueue scripts if a form is printed?
My biggest concern about your PR is the use of the wp_default_scripts
hook for registering of the plugin scripts. I think the fact you have to use wp_default_scripts
for an unintended purpose implies unsureness about how it will affect in a real user environment.
Do you think applying the defer
strategy like you did for bundled themes is insufficient?
My biggest concern about your PR is the use of the
wp_default_scripts
hook for registering of the plugin scripts. I think the fact you have to usewp_default_scripts
for an unintended purpose implies unsureness about how it will affect in a real user environment.
How is my PR using the wp_default_scripts
action for an unintended purpose? The hook documentation just says it "Fires when the WP_Scripts instance is initialized." WordPress uses this hook to register the script library which can be enqueued either on the frontend or admin. The user contributed note also indicates as such: "Add a script where you can refer to from anywhere in your WordPress installation using wp_enqueue_script
or admin_enqueue_script
." I've always used the hook for this purpose to register scripts for use later.
Nevertheless, it doesn't seem the change I made to wp_default_scripts
is actually necessary. The scripts can continue to be registered during wp_enqueue_scripts
. The main change would be to not call wpcf7_enqueue_scripts()
at the wp_enqueue_scripts
action, but rather to call it in WPCF7_ContactForm::form_html()
so it is conditionally enqueued only if necessary. I'll revert that change.
OK, I reverted that change. See the latest diff from my branch (not visible in PR since closed).
Do you think applying the
defer
strategy like you did for bundled themes is insufficient?
Since your plugin is already enqueueing your scripts in the footer (which is great) then this means adding defer
to them will have almost no effect, as they are already loading and executing at the end of the DOM loading. The use of defer
makes a big difference when scripts are in the head
, as it allows them to start loading early while not blocking rendering so that in the end they can run sooner when the page HTML finishes loading.
Thank you for the clarification. I reopened this issue since the discussion is valuable.
I'll add an action hook in the shortcode callback function or somewhere else to make it easy for add-on plugins to implement the same script loading control as you did in the PR. If there are real needs, someone will soon develop and release such an add-on.
I'm confused. Your blog post shares a sentiment that the plugin's current behavior of adding its scripts to every page is "redundant or wasteful". Therefore, if there isn't actually a technical difficulty to conditionally enqueue the scripts, why not have it be done by default in the plugin as my PR implements?
So at the end we will still need to customize this behaviour to load the assets manually in the various pages when needed instead of something from above that automatically inject the assets only when need it? I think that if every plugin will follow this behaviour that every website owner or developer has to customize this behaviour with the amount of wordpress plugins around it is will a huge job.
I think that is more healthy if the website itself, with the various plugins, load the assets only when need it without any action by the user or the website developer.
This problem just got a writeup in Just one of us after all? A closer look at Taylor Swift‘s new website because her site is using Contact Form 7:
In addition, an old culprit loads its assets: Contact Form 7 – even though there is no contact form on the website.
Your approach is indeed elegant and maybe works nicely on a vanilla WordPress install, but in reality, a plugin have to live in a complicated world with thousand of different plugins and themes. I think it would be difficult to work without causing conflicts.
I agree this a very important issue that needs to be considered in any change to Contact Form 7 (CF7).
I recall clearly the 100's of support questions that happened when Contact Form 7 added reCAPTCHA v3. A small percentage of CF7 users immediately had new issues with form submission & this created an avalanche of support questions (with other users then piling on and adding to the avalanche).
So how do you ensure this change does not cause new form submission issues in a complicated world with thousand of different plugins and themes (many of them badly written)?
I noticed that as soon as I activate the plugin, scripts are added to every single page on the frontend, even on pages that lack any contact form. This is a performance problem. These are the scripts and styles in particular:
I used the benchmark-web-vitals command on a vanilla WordPress install (WP 6.3 with Twenty Twenty-Three active) without the plugin active and I got the following:
And I ran it again with the plugin active:
So the plugin activation on the homepage increases LCP-TTFB from 40.1 to 45.55, so a ~5ms increase or ~9%.
I suggest that the scripts and stylesheet only be enqueued when a form is actually printed to the page.