Skip to content
System nominalUTC --:--:--Articles 080
MODULE 04 Lesson 12 / 18 25 MIN

The public side

Conditional Assets and the Frontend Budget

The plugin ships 444 KB of frontend files and a nonce to every account page. How the conditional enqueue works, where it misses, and how to measure your own budget.

The file is includes/assets.php, it is 202 lines, and it is the only reason a store owner who installed a courier plugin has a slower homepage. Everything else in the plugin is dormant until a page asks for it. Assets are different: the browser downloads what you enqueue whether or not a tracking code is ever typed.

That makes this file the one place where a bug is paid for by people who never use the feature. It is also the file with the best intentions in the codebase and one honest gap in the middle, so it is worth reading line by line rather than skimming for function names.

The enqueue runs at priority 20 on every front-end request

Everything hangs off one line at the bottom of the file:

add_action( 'wp_enqueue_scripts', 'sfct_maybe_enqueue_frontend_assets', 20 );

Priority 20 matters more than it looks. The default is 10, and WordPress calls the whole wp_enqueue_scripts action before the theme prints anything in wp_head. Running late in that action means other plugins have already registered their handles, so this callback can read the final queue and decide. Running too late, inside wp_head itself, means the styles have already been printed and anything enqueued afterwards is dropped with a _doing_it_wrong() notice in debug mode.

Between those two, twenty is a reasonable choice. The file itself is registered separately from the decision to enqueue it:

function sfct_register_frontend_assets() {
	wp_register_style( 'sfct-frontend', SFCT_URL . 'assets/css/frontend-style.css', array(), SFCT_VERSION );
	wp_register_script( 'sfct-frontend', SFCT_URL . 'assets/js/frontend-app.js', array(), SFCT_VERSION, true );
}

wp_register_* puts the handle in the registry with its URL and version and loads nothing. wp_enqueue_* moves it into the queue. Keeping registration and enqueue separate is what lets the shortcode callbacks call sfct_enqueue_frontend_assets() late in the request and still get a valid handle, because the handle already exists. The last true on the script puts it in the footer, so a slow script cannot hold up the first paint.

Three contexts decide, and everything else gets nothing

sfct_maybe_enqueue_frontend_assets() sets a flag and tests it. The flag starts false. It becomes true if is_account_page() exists and returns true, or if is_order_received_page() exists and returns true, or if the current request is a singular post whose content contains the sfct_tracking shortcode.

The function_exists() guards are not paranoia. Both functions come from WooCommerce, and this plugin loads its includes before it is certain WooCommerce is active. Calling is_account_page() unguarded on a site where the other plugin was deactivated mid-request is a fatal error on the front page.

The third test is the interesting one:

if ( is_singular() ) {
	$post = get_post();
	if ( $post && has_shortcode( (string) $post->post_content, 'sfct_tracking' ) ) {
		$should_enqueue = true;
	}
}

has_shortcode() looks for the literal tag in the raw post content, which is cheap and precise. On a page built in the block editor with a Shortcode block, the tag is in the content and this finds it. On a page where the shortcode comes from a widget, a reusable block, a template part, or a page builder that stores layout in post meta, there is no shortcode in post_content and this test fails. The page still renders the tracking form, because the shortcode exists and runs. It renders without the stylesheet, which is how a plugin turns into a support ticket that says the tracking page looks broken on my site.

What I measured on a WordPress 7.0.7 install with this plugin active, walking wp_enqueue_scripts and reading the queue:

PagePlugin assetsBytes, uncompressed
Home, or any page with no shortcodenone0
Page containing [sfct_tracking]style + script443,612
WooCommerce My Accountstyle + script443,612
Checkoutnone0
Page containing [sfct_tracking_button]none0

So the plugin does not enqueue on every page. It enqueues on roughly the pages where someone might track an order, which is the design goal, and it costs 444 KB uncompressed on those pages. Gzipped, the same two files are 55,824 bytes: 52,696 for the stylesheet and 3,128 for the script. That is the number the user's connection actually pays, and it is the number to compare against the rest of the page when you decide whether it is acceptable.

Four shortcodes render without the assets

The button in the last row of that table is not an accident. The detection only looks for sfct_tracking, so a page whose only plugin content is [sfct_tracking_button] gets no stylesheet. That shortcode returns a single anchor tag with the classes sfct-external-button and sfct-shortcode-tracking-button, so the buttons exist and are unstyled by the plugin. On most themes that is a plain link in the middle of a paragraph, which is not what the shortcode's name or its admin card promises.

The other three shortcodes are not detected either. sfct_customer_orders, sfct_my_orders and sfct_order_tracking call sfct_enqueue_frontend_assets() at the top of the callback, and each buffers its markup with ob_start(). When the callback runs from the_content, the theme has already printed the header, so that call cannot add anything to the page. It is not wrong to have it, but it reads like a safety net and it only works in the unusual case where the shortcode runs before the header prints, which is why the pre-pass is the part doing the real work.

I measured all six shortcodes through do_shortcode() on the lab install. [sfct_order_tracking order_id="10"] returned 6,248 bytes of card markup and added nothing to the queue. [sfct_customer_orders] returned its notice and added nothing. The only callback that changed the queue was [sfct_tracking], on a page where the pre-pass had already scheduled it.

The fix is one line in the detection function: test all four tags that need the stylesheet, not only the public one. has_shortcode() takes one tag, so you loop over them. If you want the button styled too, add its tag to the list, or load a smaller stylesheet for it, or accept that it is a link and let the theme handle it. What you should not do is leave the detection looking like it covers the plugin when it covers one shortcode out of six.

The localised payload carries a nonce, an AJAX URL and 5.9 KB of interface text

After enqueueing, the plugin calls wp_localize_script( 'sfct-frontend', 'SFCT_FRONTEND', … ) with three keys. The name is misleading: the function does not localise anything, it prints a JavaScript object assignment into the page. What lands in the footer on a tracking page is:

var SFCT_FRONTEND = {"ajaxUrl":"http://example.test/wp-admin/admin-ajax.php","nonce":"0019edbade","i18n":{ … }};

Measured on the lab install, that block is 5,951 bytes of inline HTML, and all but a few dozen characters are translation strings for the timeline interface. ajaxUrl is public by definition: admin-ajax.php is a URL anyone can read from any WordPress site, and its name has never been a security boundary. nonce is the interesting one, and it is worth being accurate about it. A WordPress nonce is not a secret and not a token. It is derived from the user session, the action name and a time window, and its job is to prove the request came from a page the site rendered, not from a third-party page that borrowed the user's cookies. It is printed into every page that needs it, so calling it a leak misunderstands what it protects.

The accuracy matters because there is still a reason to scope it. This nonce is generated for the action sfct_public_nonce and printed for anonymous visitors as well as logged-in ones. If a tracking page were ever rendered for a logged-in administrator, that administrator's session would now be carrying a nonce into a page whose main job is to accept a tracking code from the query string. The nonce is not privileged, so nothing breaks, and the scoping you would want is the ordinary one: create nonces for the actions the current page can actually perform, for the shortest lifetime the action allows. That is hygiene, not a vulnerability fix.

SFCT_VERSION is doing cache busting, not cache invalidation

Every wp_register_style() and wp_register_script() call in the plugin passes SFCT_VERSION as the version argument, which is '1.2.0' from the plugin header. WordPress appends it as a query string, so the URL the browser requests is frontend-style.css?ver=1.2.0. Change the version and the URL changes, and the browser treats it as a different resource. That is the whole mechanism, and it is why you should test a release after bumping the constant rather than only after editing the CSS.

What it does not do is guarantee freshness. If you edit the stylesheet without bumping the version, the URL stays the same and browsers keep the old file. The version string is also not a content hash, so two different builds sharing a version number are indistinguishable to the cache. If you want the stronger guarantee, use filemtime() as the version argument during development and a hash in production. Both are compatible with this pattern; the plugin chose the constant, which is the right choice when the file ships inside a versioned plugin.

Lab 30 minutes

Measure the plugin on your own install instead of trusting the table above. Put this in a must-use plugin, load a page that does not use the plugin, and read the output:

add_action( 'wp_print_styles', function () {
	foreach ( wp_styles()->queue as $handle ) {
		$src = wp_styles()->registered[ $handle ]->src ?? '';
		if ( false !== strpos( (string) $src, 'sifero-courier-timeline' ) ) {
			error_log( 'style ' . $handle . ' ' . $src );
		}
	}
} );

Then do the same for scripts on wp_print_footer_scripts, and check the same three page types: the home page, a page with [sfct_tracking], and a page with [sfct_customer_orders] while logged in as a customer with at least one order.

Now add sfct_customer_orders, sfct_my_orders and sfct_order_tracking to the detection in sfct_maybe_enqueue_frontend_assets() and measure again. Finally, answer: the stylesheet is 430 KB uncompressed on an upload. Is that a number you would accept in a plugin you shipped, and what would you do about it if the answer is no?

Show the solution and the reasoning

The measurement output should match the table in the lesson: nothing on the home page, both the style and the script on a page containing [sfct_tracking], and nothing at all for [sfct_customer_orders] before your change, even while logged in as a customer with an order. That last result is the one to sit with. The shortcode renders a card with a company logo, a progress bar and a timeline, and it does so with no stylesheet from the plugin.

After adding the three tags to the detection, the customer page loads both files as well. The function grows from one has_shortcode() call to four, and you could write it as a loop over an array to keep it readable:

$sfct_tags = array( 'sfct_tracking', 'sfct_customer_orders', 'sfct_my_orders', 'sfct_order_tracking' );

if ( is_singular() ) {
	$post = get_post();
	foreach ( $sfct_tags as $sfct_tag ) {
		if ( $post && has_shortcode( (string) $post->post_content, $sfct_tag ) ) {
			$should_enqueue = true;
			break;
		}
	}
}

That fixes the common case and does not fix the page builder case, because has_shortcode() only reads post_content. There is no reliable way to know in advance whether a shortcode will render when the content lives outside the post. The honest options are to enqueue on more page types than strictly needed, to enqueue from inside the shortcode and accept that the stylesheet arrives in the footer where WordPress will still print it, or to expose a filter so a site can force the assets on specific URLs. Pick one and document it; the silent unstyle is the only genuinely bad outcome.

On the 430 KB: no, I would not ship it as one file. Size on disk and size over the wire are different questions, and the wire number is 52 KB gzipped, which is large for a stylesheet but not absurd for a component with a logo card, a progress bar, a timeline and a search form. The reason to care is that the file is one handle, so a page that needs the search form downloads the styles for the timeline too. Splitting it into a base stylesheet and an optional one is the work. What I would not do is leave 430 KB of CSS in the plugin repository and call the gzip number the answer, because the parser still has to read the whole file and 430 KB of selectors is a real cost on a phone.

Official reference

The WordPress developer reference for what this lesson uses.

Progress

Mark this lesson done and it stays marked in this browser.