rocklobster-in / contact-form-7

Contact Form 7 - Just another contact form plugin for WordPress.
Other
289 stars 144 forks source link

Scripts and style are enqueued on pages lacking forms #1278

Open westonruter opened 1 year ago

westonruter commented 1 year ago

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:

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

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:

$ 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)      │ 73.1                    ║
╟───────────────────┼─────────────────────────╢
║ LCP (median)      │ 73.1                    ║
╟───────────────────┼─────────────────────────╢
║ TTFB (median)     │ 32.6                    ║
╟───────────────────┼─────────────────────────╢
║ LCP-TTFB (median) │ 40.1                    ║
╚═══════════════════╧═════════════════════════╝

And I ran it again with the plugin active:

$ 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)      │ 79                      ║
╟───────────────────┼─────────────────────────╢
║ LCP (median)      │ 79                      ║
╟───────────────────┼─────────────────────────╢
║ TTFB (median)     │ 33.75                   ║
╟───────────────────┼─────────────────────────╢
║ LCP-TTFB (median) │ 45.55                   ║
╚═══════════════════╧═════════════════════════╝

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.

westonruter commented 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&#038;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.

westonruter commented 1 year ago

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

BigQuery SQL ```sql # HTTP Archive query to get counts of theme/plugin scripts blocking in head. # # WPP Research, Copyright 2023 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. CREATE TEMP FUNCTION GET_BLOCKING_HEAD_SOURCES (custom_metrics STRING) RETURNS ARRAY LANGUAGE js AS ''' const sourceRegExp = new RegExp( '/wp-content/(?plugin|theme)s/(?[^/]+)/(?[^\?]+)' ); /** * Get slug of extension prefixed by theme/plugin from URL. * * @param {string} src Script URL. * @return {?{type: "plugin"|"theme", slug: string, path: string}} Source info if matched. */ function getSource(src) { const matches = src.match( sourceRegExp ); if (matches) { return matches.groups; } return null; } /** * Get script sources for scripts in the head which are blocking (not async nor defer). * * @param {object} data * @param {object} data.cms * @param {object} data.cms.wordpress * @param {Array<{src: string, intended_strategy: string, async: boolean, defer: boolean, in_footer: boolean}>} data.cms.wordpress.scripts * @return {Array} Sources. */ function getBlockingHeadScriptSources(data) { const sources = []; for ( const script of data.cms.wordpress.scripts ) { const source = getSource(script.src); if (!source) { continue; } sources.push( [ source.type, source.slug ].join( ':' ) ); sources.push( [ source.type, source.slug, source.path ].join( ':' ) ); } return sources; } const sources = []; try { const data = JSON.parse(custom_metrics); sources.push(...getBlockingHeadScriptSources(data)); } catch (e) {} return sources; '''; WITH all_sources AS ( SELECT GET_BLOCKING_HEAD_SOURCES(custom_metrics) AS sources, FROM `httparchive.all.pages`, UNNEST(technologies) AS technology WHERE date = CAST("2023-07-01" AS DATE) AND technology.technology = "WordPress" AND is_root_page = TRUE ) SELECT source, COUNT(source) AS source_count FROM all_sources, UNNEST(sources) AS source GROUP BY source HAVING source_count >= 10000 ORDER BY source_count DESC ```

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.

PhilMakower commented 1 year ago

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?

https://developers.google.com/recaptcha/docs/v3

westonruter commented 1 year ago

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.

westonruter commented 1 year ago

@PhilMakower I've reverted the change to reCAPTCHA in #1279 via 9d8fee27d8b6515b3c301a1966236b3a8a4744eb.

takayukister commented 1 year ago

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.

westonruter commented 1 year ago

@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?

takayukister commented 1 year ago

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?

westonruter commented 1 year ago

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.

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.

westonruter commented 1 year ago

OK, I reverted that change. See the latest diff from my branch (not visible in PR since closed).

westonruter commented 1 year ago

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.

takayukister commented 1 year ago

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.

westonruter commented 1 year ago

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?

Mte90 commented 11 months ago

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.

westonruter commented 8 months ago

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.

buzztone commented 8 months ago

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)?