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:
| Page | Plugin assets | Bytes, uncompressed |
|---|---|---|
| Home, or any page with no shortcode | none | 0 |
Page containing [sfct_tracking] | style + script | 443,612 |
| WooCommerce My Account | style + script | 443,612 |
| Checkout | none | 0 |
Page containing [sfct_tracking_button] | none | 0 |
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.
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.