From d79fc940618008bbb66fcccc60dcacfd016324fb Mon Sep 17 00:00:00 2001
From: peptiderepo <273788462+peptiderepo@users.noreply.github.com>
Date: Thu, 23 Apr 2026 13:24:47 +0700
Subject: [PATCH] =?UTF-8?q?feat(perf):=20mobile=20performance=20Phase=201?=
=?UTF-8?q?=20=E2=80=94=20dequeue=20unused=20plugin=20assets=20+=20slim=20?=
=?UTF-8?q?fonts=20+=20preconnect=20(#13)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Ships peptide-starter-theme v1.6.0.
Changes:
- New inc/perf-asset-policy.php module: conditional dequeue of WC/Elementor/USMI assets on pages that do not need them, Google Fonts weight slim (Roboto 100..900 → 400/500/700; Roboto Slab → 400/700), wp_resource_hints preconnect for fonts.googleapis.com + fonts.gstatic.com, cookie-notice-front script defer.
- Kill-switch constant PEPTIDE_STARTER_PERF_DEQUEUE (default true) for instant rollback via wp-config.php.
- 4 extensibility filters: peptide_starter_perf_woocommerce_{styles,scripts}, _elementor_{styles,scripts}, _usmi_{styles,scripts}, _font_weights.
- 13 unit tests in tests/test-perf-asset-policy.php.
- Bootstrap: 2 lines in functions.php (require + after_setup_theme action).
- Docs: CHANGELOG [1.6.0], ARCHITECTURE.md, CONVENTIONS.md how-to.
Target: mobile PSI 74 → 90+, LCP 5.4s → ~2.5s.
Post-deploy: purge LiteSpeed CSS bundle.
QA: qa-reviews/peptide-starter-theme/2026-04-23-a54b5c9.md (APPROVE with handle-drift verified false-positive).
---
ARCHITECTURE.md | 4 +-
CHANGELOG.md | 43 +++++
CONVENTIONS.md | 37 ++++
functions.php | 5 +-
inc/perf-asset-policy.php | 313 +++++++++++++++++++++++++++++++
style.css | 2 +-
tests/test-perf-asset-policy.php | 244 ++++++++++++++++++++++++
7 files changed, 645 insertions(+), 3 deletions(-)
create mode 100644 inc/perf-asset-policy.php
create mode 100644 tests/test-perf-asset-policy.php
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index db08306..296338e 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -47,6 +47,8 @@ peptide-starter/
│ ├── newsletter-admin.php # Subscriber viewer + CSV-safe export +
│ │ # /newsletter-unsubscribe route.
│ ├── mail-diagnostic.php # [v1.5.1] Tools → Mail Test — wp_mail probe.
+│ ├── perf-asset-policy.php # [v1.6.0] Mobile perf: dequeue + font slim
+│ │ # + preconnect + defer.
│ └── page-setup.php # Auto-create pages on activation +
│ # v1.5.0 user migration (enrol into verify).
│
@@ -265,7 +267,7 @@ for out-of-band updates.
## Version
-**Current:** v1.5.2 (2026-04-14)
+**Current:** v1.6.0 (2026-04-23)
Bump `style.css` header line 7 and `PEPTIDE_STARTER_VERSION` in
`functions.php` together on release.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a5786be..69c9de0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,49 @@
All notable changes to the Peptide Starter Theme are documented in this file.
+## [1.6.0] - 2026-04-23 — Mobile Performance Phase 1
+
+Mobile-perf optimization targeting LCP reduction through conditional asset
+dequeue, font weight slimming, and font server preconnect. Measured baseline
+LCP on Moto G Power (Slow 4G): 5.4s; target after Phase 1: ~2.5s.
+
+### Added
+
+- `inc/perf-asset-policy.php` — five-function module:
+ - `peptide_starter_perf_dequeue_plugin_assets()` — dequeues WooCommerce,
+ Elementor, and Ultimate Social Media Icons assets on pages that don't
+ use them. Hook: `wp_enqueue_scripts` priority 100.
+ - `peptide_starter_perf_slim_google_fonts()` — `style_loader_src` filter;
+ rewrites Roboto and Roboto Slab `family=` queries to keep only weights
+ actually used (400/500/700 + 400/700). Drops 72 font faces to 5.
+ - `peptide_starter_perf_resource_hints()` — `wp_resource_hints` filter;
+ adds preconnect hints for `fonts.googleapis.com` and `fonts.gstatic.com`.
+ - `peptide_starter_perf_defer_cookie_notice()` — `script_loader_tag` filter;
+ appends `defer` attribute to the cookie-notice front-end script.
+ - `peptide_starter_page_uses_elementor( int $post_id )` — helper.
+- Kill-switch constant `PEPTIDE_STARTER_PERF_DEQUEUE` (default `true`).
+ Define `false` in `wp-config.php` to disable all dequeues instantly.
+- Filterable handle lists: `peptide_starter_perf_woocommerce_handles`,
+ `peptide_starter_perf_elementor_handles`, `peptide_starter_perf_usmi_handles`,
+ `peptide_starter_perf_font_weights`.
+- `tests/test-perf-asset-policy.php` — 13 unit-test cases covering kill-switch,
+ per-page-context dequeue logic, font slim happy/edge cases, filter overrides,
+ preconnect insertion, defer attribute application.
+
+### Changed
+
+- `style.css` Version header bumped 1.5.2 → 1.6.0.
+- `PEPTIDE_STARTER_VERSION` constant bumped to `1.6.0`.
+
+### Why
+
+PageSpeed Insights mobile score on `https://peptiderepo.com/` was 74 with
+LCP 5.4s, FCP 2.6s. Desktop scored ~95 — the gap was mobile-specific
+(weaker CPU + slower network amplify the cost of 13 render-blocking CSS
+files in `
`, of which the homepage actually needed at most 4).
+Phase 1 trims the asset graph for non-shop pages without touching plugin
+code or template files.
+
## [1.5.2] - 2026-04-14 — Security
Same-day follow-up to v1.5.1. Closes four issues the v1.5.1 post-merge
diff --git a/CONVENTIONS.md b/CONVENTIONS.md
index c3f2344..54919af 100644
--- a/CONVENTIONS.md
+++ b/CONVENTIONS.md
@@ -129,6 +129,43 @@ do_action( 'peptide_starter_hero_before' );
- **languages/:** Translation files
- **template-parts/:** Reusable template parts
+## How to add a performance policy rule
+
+When adding a new asset-perf rule (e.g., a new plugin's CSS to dequeue on
+non-relevant pages, or a new font family to slim):
+
+1. **For dequeues:** edit `inc/perf-asset-policy.php`. Add the plugin's
+ handle list to a new function (or extend the existing arrays via the
+ matching filter — `peptide_starter_perf__handles`). Hook the
+ new dequeue function on `wp_enqueue_scripts` priority 100, gated by
+ `PEPTIDE_STARTER_PERF_DEQUEUE` and a context predicate (e.g.,
+ `is_front_page()`, `! is_woocommerce()`).
+
+2. **For font slimming:** add the family to the default array in
+ `peptide_starter_perf_slim_google_fonts()`, or override at runtime
+ via the `peptide_starter_perf_font_weights` filter. Specify only
+ weights actually used by the design system; defaulting to "all weights"
+ defeats the purpose.
+
+3. **For new resource hints:** extend `peptide_starter_perf_resource_hints()`
+ with the new domain in the `preconnect` array. WP renders the
+ `` tag automatically; don't echo your own.
+
+4. **For deferring third-party scripts:** add the script handle to the
+ list in `peptide_starter_perf_defer_cookie_notice()` (or rename the
+ function if it grows beyond cookie-notice).
+
+5. **Always add a test case** in `tests/test-perf-asset-policy.php`
+ covering the new rule's happy path AND its negation (the page-context
+ where the rule must NOT apply). The dequeue logic is silent if a
+ handle name is wrong — only tests catch this before deploy.
+
+6. **Verify on production handles** before committing. Plugin handle
+ names are not always what you'd guess. SSH to Hostinger and run
+ `wp eval 'global $wp_styles; do_action("wp_enqueue_scripts");
+ foreach($wp_styles->registered as $h=>$o){echo $h.PHP_EOL;}'` to
+ dump the actual handle names.
+
## Step-by-Step Guides
### How to Add a New Template
diff --git a/functions.php b/functions.php
index 62fc211..2f3847b 100644
--- a/functions.php
+++ b/functions.php
@@ -15,6 +15,7 @@
* @see inc/page-setup.php — auto-create pages + v1.5.0 user migration
* @see inc/mail-diagnostic.php — admin deliverability test tool
* @see inc/helpers.php — nav walker, menu fallback, gates, utilities
+ * @see inc/perf-asset-policy.php — asset dequeue + font slim + preconnect + defer
*
* @package peptide-starter
*/
@@ -25,7 +26,7 @@ if ( ! defined( 'ABSPATH' ) ) {
}
// Theme constants.
-define( 'PEPTIDE_STARTER_VERSION', '1.5.2' );
+define( 'PEPTIDE_STARTER_VERSION', '1.6.0' );
define( 'PEPTIDE_STARTER_DIR', get_template_directory() );
define( 'PEPTIDE_STARTER_URI', get_template_directory_uri() );
@@ -42,6 +43,7 @@ require_once PEPTIDE_STARTER_DIR . '/inc/contact-handler.php';
require_once PEPTIDE_STARTER_DIR . '/inc/page-setup.php';
require_once PEPTIDE_STARTER_DIR . '/inc/newsletter-admin.php';
require_once PEPTIDE_STARTER_DIR . '/inc/mail-diagnostic.php';
+require_once PEPTIDE_STARTER_DIR . '/inc/perf-asset-policy.php';
/**
* Set up theme defaults and register support for WordPress features.
@@ -69,6 +71,7 @@ function peptide_starter_setup() {
load_theme_textdomain( 'peptide-starter', PEPTIDE_STARTER_DIR . '/languages' );
}
add_action( 'after_setup_theme', 'peptide_starter_setup' );
+add_action( 'after_setup_theme', 'peptide_starter_perf_asset_policy_init' );
/**
* Register and enqueue stylesheets and scripts.
diff --git a/inc/perf-asset-policy.php b/inc/perf-asset-policy.php
new file mode 100644
index 0000000..da75c9a
--- /dev/null
+++ b/inc/perf-asset-policy.php
@@ -0,0 +1,313 @@
+queue ) ) {
+ $elementor_post_styles = array();
+ foreach ( $wp_styles->queue as $handle ) {
+ if ( isset( $wp_styles->registered[ $handle ] ) ) {
+ $src = $wp_styles->registered[ $handle ]->src;
+ // Detect Elementor per-post CSS by path pattern.
+ if ( $src && preg_match( '/uploads\/elementor\/css\/post-\d+\.css/', $src ) ) {
+ $elementor_post_styles[] = $handle;
+ }
+ }
+ }
+
+ foreach ( $elementor_post_styles as $handle ) {
+ wp_dequeue_style( $handle );
+ }
+ }
+ }
+
+ // Ultimate Social Media Icons: dequeue on homepage, archives, and blog index.
+ if ( is_front_page() || is_home() || is_archive() ) {
+ $usmi_handles = apply_filters(
+ 'peptide_starter_perf_usmi_handles',
+ array(
+ 'SFSImainCss',
+ 'SFSIjqueryModernizr',
+ 'SFSIjqueryShuffle',
+ 'SFSIjqueryrandom-shuffle',
+ 'SFSICustomJs',
+ )
+ );
+
+ foreach ( $usmi_handles as $handle ) {
+ wp_dequeue_style( $handle );
+ wp_dequeue_script( $handle );
+ }
+ }
+}
+
+/**
+ * Slim Google Fonts to essential weights only.
+ *
+ * Rewrites fonts.googleapis.com URLs to keep only:
+ * - Roboto: 400, 500, 700 (no italics)
+ * - Roboto Slab: 400, 700 (no italics)
+ *
+ * This reduces the font CSS from ~6KB to <1KB and the font payload by ~80%.
+ * Filterable per family via 'peptide_starter_perf_font_weights'.
+ *
+ * @param string $src Stylesheet URL.
+ * @param string $handle Stylesheet handle.
+ * @return string Rewritten URL or original.
+ */
+function peptide_starter_perf_slim_google_fonts( $src, $handle ) {
+ if ( ! $src || false === strpos( $src, 'fonts.googleapis.com' ) ) {
+ return $src;
+ }
+
+ // Parse the URL to get the query string.
+ $parsed = wp_parse_url( $src );
+ if ( empty( $parsed['query'] ) ) {
+ return $src;
+ }
+
+ // Extract family parameter.
+ parse_str( $parsed['query'], $query_vars );
+ if ( empty( $query_vars['family'] ) ) {
+ return $src;
+ }
+
+ $families_str = $query_vars['family'];
+
+ // Decode the family string (it may be URL-encoded).
+ $families_str = rawurldecode( $families_str );
+
+ // Split families by + (multiple families in one request).
+ $families = explode( '+', $families_str );
+
+ // Get configured font weights from filter.
+ $font_weights_config = apply_filters(
+ 'peptide_starter_perf_font_weights',
+ array(
+ 'Roboto' => array( 400, 500, 700 ),
+ 'Roboto Slab' => array( 400, 700 ),
+ )
+ );
+
+ $rewritten_families = array();
+ foreach ( $families as $family ) {
+ // Clean up the family name (URL decode + trim).
+ $family = trim( rawurldecode( $family ) );
+
+ // Check if this family has a configured weight list.
+ $matched = false;
+ foreach ( $font_weights_config as $config_family => $weights ) {
+ if ( 0 === strcasecmp( $family, $config_family ) ) {
+ // Rewrite with only the specified weights.
+ $weights_str = implode( ',', $weights );
+ $rewritten_families[] = $config_family . ':' . $weights_str;
+ $matched = true;
+ break;
+ }
+ }
+
+ // If no config match, pass through unchanged.
+ if ( ! $matched ) {
+ $rewritten_families[] = $family;
+ }
+ }
+
+ // If nothing changed, return original.
+ if ( count( $rewritten_families ) === count( $families ) ) {
+ // Check if any actually changed (compare raw values).
+ $original_str = implode( '+', $families );
+ $rewritten_str = implode( '+', $rewritten_families );
+
+ if ( $original_str === $rewritten_str ) {
+ return $src;
+ }
+ }
+
+ // Rebuild the URL with the new family parameter.
+ $new_query_vars = $query_vars;
+ $new_query_vars['family'] = implode( '+', $rewritten_families );
+ $new_query_str = http_build_query( $new_query_vars );
+
+ // Reconstruct the full URL.
+ $new_src = $parsed['scheme'] . '://' . $parsed['host'] . $parsed['path'] . '?' . $new_query_str;
+
+ return $new_src;
+}
+
+/**
+ * Add preconnect hints for font servers.
+ *
+ * Reduces TLS + DNS latency for Google Fonts requests on cold cache.
+ * Adds preconnect for fonts.googleapis.com and fonts.gstatic.com.
+ *
+ * @param array $urls List of URLs to add hints for.
+ * @param string $rel The relationship type (preconnect, dns-prefetch, etc.).
+ * @return array Modified URL list.
+ */
+function peptide_starter_perf_resource_hints( $urls, $rel ) {
+ // Only add hints for preconnect relationships.
+ if ( 'preconnect' !== $rel ) {
+ return $urls;
+ }
+
+ // Add font servers if not already present.
+ $font_urls = apply_filters(
+ 'peptide_starter_perf_font_preconnect_urls',
+ array(
+ 'https://fonts.googleapis.com',
+ 'https://fonts.gstatic.com',
+ )
+ );
+
+ foreach ( $font_urls as $url ) {
+ if ( ! in_array( $url, $urls, true ) ) {
+ $urls[] = $url;
+ }
+ }
+
+ return $urls;
+}
+
+/**
+ * Defer the cookie-notice script to avoid render-blocking.
+ *
+ * Adds 'defer' attribute to cookie-notice JS while preserving DOM-ready order.
+ * This ensures the cookie banner loads without blocking initial rendering.
+ *
+ * @param string $tag The script tag HTML.
+ * @param string $handle The script handle.
+ * @param string $src The script URL (unused).
+ * @return string Modified script tag.
+ */
+function peptide_starter_perf_defer_cookie_notice( $tag, $handle, $src ) {
+ // Only modify the cookie-notice script.
+ if ( 'cookie-notice-front' !== $handle ) {
+ return $tag;
+ }
+
+ // Add defer attribute if not already present.
+ if ( false === strpos( $tag, 'defer' ) ) {
+ // Insert defer before the closing >.
+ $tag = str_replace( '';
+ $result = peptide_starter_perf_defer_cookie_notice( $tag, 'cookie-notice-front', 'http://example.com/cookie-notice.js' );
+
+ $this->assertStringContainsString( 'defer', $result );
+ }
+
+ /**
+ * Test: Defer not applied to other scripts.
+ *
+ * The defer filter should only affect cookie-notice-front, not other handles.
+ */
+ public function test_defer_not_applied_to_other_scripts() {
+ $tag = '';
+ $result = peptide_starter_perf_defer_cookie_notice( $tag, 'other-script', 'http://example.com/other.js' );
+
+ // Should not have defer added.
+ $this->assertStringNotContainsString( 'defer', $result );
+ }
+
+ /**
+ * Test: page_uses_elementor helper returns false for non-singular.
+ */
+ public function test_page_uses_elementor_non_singular() {
+ // Go to homepage (not singular).
+ $this->go_to( home_url( '/' ) );
+
+ $result = peptide_starter_page_uses_elementor( 0 );
+ $this->assertFalse( $result );
+ }
+}