Skip to content

formatFields('option') renders heavy embeds on every request #92

Description

@parisek

Problem

Helpers::formatFields( 'option' ) eagerly formats every field on the options page. When one of them is a post_object pointing at a form (wpforms, contact-form-7), fieldFormatter() runs it through do_shortcode() and returns fully rendered HTML.

timber-kit.md § Override timber_context() documents Helpers::formatFields('option') as the canonical way to source global options. So the render happens on every single request, on every page — including pages that contain no form at all.

Measured on a production site (oekoplan, timber-kit v1.26.0):

$x = Helpers::formatFields("option");
gettype($x["career"]["form"])  →  string
strlen(...)                    →  6050
substr(..., 0, 60)             →  <div class="wpforms-container wpforms-render-modern" id="wpforms-1991…

Two consequences:

  1. 6 kB of form HTML is rendered per request just to build the global context, then thrown away on every page that doesn't use it.
  2. WPForms registers its CSS/JS globally. Rendering the shortcode triggers the plugin's asset enqueue, so wpforms-*.css, the layout stylesheets and jQuery land in <head> of every page on the site. Verified by diffing rendered HTML before/after the theme adopted StarterBase — 6 stylesheets + jQuery appeared on pages with no form.

There is also a correctness angle: a form rendered into the global context means duplicate id="wpforms-form-…" in the DOM if the same form is rendered again by the page itself.

Where it comes from

src/Helpers.php around line 1216:

} elseif ( $field['value']->post_type === 'wpforms' ) {
    if ( $is_preview ) {
        $field['value'] = '[wpforms id="' . $field['value']->ID . '"]';
    } else {
        $field['value'] = do_shortcode( '[wpforms id="' . $field['value']->ID . '"]' );
    }
}

The field_formatter_* filter at line ~1303 fires after this, so a downstream filter can replace the output but cannot prevent the render or the asset enqueue.

Current workaround (and why it isn't the fix)

Passing the existing second parameter:

$global_fields = Helpers::formatFields( 'option', true );  // $is_preview

This works — verified that nothing else changes:

links + footer + announcement with preview=true vs without  →  byte-identical
career.form with preview=true                               →  [wpforms id="1991"]

But $is_preview means "we are in the editor preview". Using it to mean "don't render heavy embeds" overloads a flag with unrelated semantics, and any future behaviour keyed on $is_preview would silently start applying to normal front-end requests.

Proposed solutions

Roughly in order of preference:

  1. Lazy post_object formatting. Return a small value object that renders the shortcode on string conversion. Twig calling {{ content.form }} renders it; a context build that never touches it pays nothing. No API break, fixes every call site at once.
  2. A dedicated parameter/flagformatFields( $post, $is_preview, $render_embeds = true ), or an options array. Explicit, but every caller has to know to pass it.
  3. A filter that fires before the render, e.g. field_formatter_pre_post_object, so a theme can opt out per field. Smallest change, but leaves the default behaviour surprising.

Happy to open a PR for whichever direction you prefer — (1) reads as the real fix to me.

Context

Found while migrating a theme to StarterBase. Reported from oekoplan (~/Sites/wordpress/oekoplan), timber-kit v1.26.0, ACF Pro 6.8.6, WPForms 2.0.0.2.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions