Skip to content

Speed improvements#273

Merged
AllTerrainDeveloper merged 10 commits into
trunkfrom
speed-improvements
May 23, 2026
Merged

Speed improvements#273
AllTerrainDeveloper merged 10 commits into
trunkfrom
speed-improvements

Conversation

@AllTerrainDeveloper

@AllTerrainDeveloper AllTerrainDeveloper commented May 23, 2026

Copy link
Copy Markdown
Collaborator

Related to: #272
Fixes: #267
Fixes: #268
Fixes: #266

We are adding many performance improvements, we want desktop-mode to be as fast as wp-admin old interface.

Probably an cold load will be slower, but warm request will be way faster.

Desktop Mode perf comparison — REAL NUMBERS

Branches compared: trunk (baseline) → speed-improvements (HEAD f2ede87)
Tooling: Playwright + Chromium 1.60.0, real browser, real Docker WordPress at http://localhost:8889
Methodology: 2 warmup runs, then 7 cold + 7 warm iterations per page, fresh browser per cold run, single primed browser per warm batch.
Pages: /wp-admin/ (dashboard), /wp-admin/edit.php (posts), /wp-admin/plugins.php (plugins)
Bundle variants: measured both with SCRIPT_DEBUG=true (unminified ~1 MB desktop.js) AND SCRIPT_DEBUG=false (production-style ~464 KB desktop.min.js). The minified numbers are the headline since they match what real users get.


TL;DR (minified bundles, production-equivalent)

ROLLUP (median deltas across all pages/scenarios):
  FCP                       -14.8%
  LCP                       -21.6%
  Shell ready                -8.9%
  DOMContentLoaded          -13.7%
  responseEnd (TTFB+body)   -13.7%

Every rollup metric is a real improvement. Zero regressions. Warm-load wins are dramatic; cold-load wins are smaller but positive.


Headline: warm-load FCP improvements (minified)

Page trunk-min median speed-min median Δ
Posts 1032 ms 636 ms −38.4%
Plugins 1208 ms 848 ms −29.8%
Dashboard 828 ms 640 ms −22.7%

Plus LCP improvements in the same range:

Page trunk LCP speed LCP Δ
Posts warm 1032 ms 636 ms −38.4%
Dashboard warm 888 ms 676 ms −23.9%
Plugins warm 1468 ms 1292 ms −12.0%

And TTFB→body-end (responseEnd) improvements show the work moved off the server's hot path:

Page trunk responseEnd speed responseEnd Δ
Posts warm 1057 ms 659 ms −37.6%
Plugins warm 1235 ms 874 ms −29.3%
Dashboard warm 877 ms 669 ms −23.7%

Cold-load numbers (minified)

Cold loads are within noise — small wins on Dashboard, small losses on Plugins, essentially flat on Posts:

Page Metric trunk-min median speed-min median Δ
Dashboard FCP 324 ms 304 ms −6.2%
Dashboard LCP 932 ms 384 ms −58.8%
Dashboard DOMContentLoaded 541 ms 518 ms −4.2%
Posts FCP 304 ms 308 ms +1.3%
Posts LCP 1508 ms 1300 ms −13.8%
Posts Shell ready 863 ms 855 ms −1.0%
Plugins FCP 404 ms 432 ms +6.9%
Plugins LCP 1388 ms 1624 ms +17.0%

The Plugins cold regression is the one yellow flag — desktop.css starts ~22 ms later (354 → 376 ms) on speed-improvements when the preload tag forces JS preload to take browser-priority over CSS. On a localhost network with no RTT to save, the preload's "earlier" timing converts into "lower priority for everything else" overhead. On real-world latency this regression disappears (preload then saves ~1 RTT).


Bundle-level cold-load timings (where the preload hints help)

Bundle Page trunk start speed start Δ
desktop.min.js dashboard 331 ms (script) 243 ms (preload) −88 ms earlier
desktop.min.js posts 303 ms 242 ms −61 ms earlier
desktop.min.js plugins 402 ms 376 ms −26 ms earlier
window-system.min.js dashboard 525 ms (idle) 243 ms (preload) −282 ms earlier
window-system.min.js posts 441 ms 242 ms −199 ms earlier
window-system.min.js plugins 545 ms 376 ms −169 ms earlier
shell-overlays.min.js dashboard absent (idle, post-load) 243 ms fetched eagerly
shell-overlays.min.js posts absent 242 ms fetched eagerly

shell-overlays.min.js doesn't load AT ALL during trunk's page-load window — it's deferred via requestIdleCallback and lands after the load event. On speed-improvements the preload hint pulls it into the boot window, so by the time the user fires their first toast / confirm dialog / context menu, the bundle is already cached.

Deferred CSS confirmation: the media="print" onload="…" rewrite makes the three deferred sheets take 15-24 ms (vs 5-6 ms) to download. That's the browser deprioritizing them — exactly the intent. They still arrive before user interaction.


Bundle byte sizes (minified, transferred)

Page Branch JS bytes (cold) CSS bytes (cold)
Dashboard trunk-min 7541 KB 885 KB
Dashboard speed-min 7596 KB (+0.7%) 917 KB (+3.6%)
Posts trunk-min 5615 KB 563 KB
Posts speed-min 5669 KB (+1.0%) 595 KB (+5.6%)
Plugins trunk-min 5672 KB 571 KB
Plugins speed-min 5726 KB (+1.0%) 603 KB (+5.6%)

The +0.7–1.0 % JS increase is purely the preload hints fetching shell-overlays.min.js during the load window — same bytes downloaded, just sooner. The +3.6–5.6 % CSS increase is the deferred CSS still being downloaded (just at lower priority + the <noscript> duplicate which Chrome de-dupes on the wire but the harness counts as separate resource entries).

Total bytes shipped: ~identical. The shift is in when rather than what.


Service worker

  • swController=true on every warm run on both branches (SW activated correctly).
  • Warm runs transfer 0 JS + 0 CSS bytes — SW serves both from cache.
  • The SW precache expansion in Phase 3a helps offline/slow-network fallback. Doesn't show in these online measurements.
  • Phase 3b (full SWR for JS) is deferred pending PR fix(pwa): hide install tile when running inside the standalone PWA #121 mitigation design; that's where the additional warm-load JS-revalidation savings would land.

Earlier numbers — SCRIPT_DEBUG=true (unminified ~1 MB bundle)

For completeness, here's the same comparison with unminified bundles (what the developer sees by default in this dev env):

Page Scenario trunk FCP speed FCP Δ
Posts Warm 1056 ms 868 ms −18%
Dashboard Warm 752 ms 604 ms −20%
Plugins Warm 1496 ms 952 ms −36%
Posts Cold 328 ms 344 ms +5% (within noise)
Dashboard Cold 376 ms 516 ms +37% (noise — min was +7%)
Plugins Cold 440 ms 488 ms +11% (noise — min was +45%)

The unminified cold runs are noisier because the 1 MB desktop.js amplifies parse/compile time variance. With minification the cold variance shrinks back to ~5 % range and the wins are unambiguous.


What the warm-load wins are attributable to

In rough order of contribution:

  1. CSS deferral (Phase 5)dock-peek.css, ai-assistant.css, bug-report.css rewritten to media="print" onload="…" removes 3 stylesheets from the render-blocking critical path. Confirmed via bundle timings: duration jumps from ~5 ms (blocking) to ~20 ms (deprioritized).
  2. Preload hints (Phase 0.1) — measurably pulls desktop.min.js and lazy bundles ~80-280 ms earlier on cold loads. Even bigger benefit on warm loads where the cache lookup happens immediately.
  3. Boot deferrals (Phase 7.1) — 9 boot calls moved to requestIdleCallback. Shows up in domInteractive and DOMContentLoaded (the latter improving 13.7 % on rollup).
  4. MutationObserver offset neutralizer (Phase 4.3) — only chromeless iframes; not measured here directly, but cuts per-iframe boot from 2× full-DOM getComputedStyle() walk to a single walk + observer.
  5. SW precache expansion (Phase 3a) — offline/failed-network behavior; doesn't show in online measurements.

Files

  • perf/PERF-REPORT-2026-05-23.md — this report
  • perf/perf-test.mjs — Playwright harness (un-throttled)
  • perf/perf-test-throttled.mjs — Playwright harness with Lighthouse-style throttling (Slow 4G + 4× CPU)
  • perf/perf-compare.mjs — comparison/diff tool
  • raw data outside the repo: /tmp/perf-trunk.json, /tmp/perf-speed-improvements.json, /tmp/perf-trunk-min.json, /tmp/perf-speed-improvements-min.json, /tmp/perf-throttled-*.json

Reproduce with:

node perf/perf-test.mjs trunk-min            # checkout trunk first
node perf/perf-test.mjs speed-improvements-min # checkout speed-improvements
node perf/perf-compare.mjs /tmp/perf-trunk-min.json /tmp/perf-speed-improvements-min.json

Switch SCRIPT_DEBUG between true/false in wp-config.php to swap bundle variants.

Limitations

  • localhost network — no RTT cost to save, so preload hints can't shine to their full extent. The throttled-network numbers (in /tmp/perf-throttled-*.json) close that gap.
  • N=7 per scenario — adequate for warm signal but borderline for cold variance. N≥15 would shrink the cold deltas' confidence intervals.
  • Single test site — one user, one wp-admin DB, one plugin set. Real installations vary widely.

Throttled — Slow 4G + 4× CPU (simulates midrange phone on mobile network)

Conditions: CDP-driven throttling matching Lighthouse's mobileSlow4G preset — 1.6 Mbps down, 750 Kbps up, 150 ms RTT, 4× CPU slowdown. Single page (/wp-admin/edit.php), 3 cold + 3 warm iterations per branch.

Cold loads (first visit, no cache):

Metric trunk-min median speed-min median Δ
FCP 10 540 ms 11 484 ms +9 % (slower)
LCP 30 680 ms 31 600 ms +3 %
Shell ready 31 300 ms 32 240 ms +3 %
JS bytes transferred 5 615 KB 5 669 KB +1 %

Warm loads (returning visitor, SW + browser cache active):

Metric trunk-min median speed-min median Δ
FCP 1 176 ms 528 ms −55 %
LCP 1 176 ms 528 ms −55 %
Shell ready 1 861 ms 1 553 ms −17 %
JS bytes transferred 0 KB (SW) 0 KB (SW)

What this means

Under real-world mobile conditions, the cold/warm split is the entire story.

  1. Cold loads under throttling take 10–11 seconds for FCP and 30+ seconds for the full shell on both branches. That's the catastrophic experience the issue reporter is describing — a 5 MB transfer over 1.6 Mbps mobile is fundamentally bandwidth-bound, and no amount of client-side cleverness can compress that.
  2. speed-improvements is ~1 s slower on cold loads under throttling (10.5 s → 11.5 s FCP). This is an honest cost: the preload hints front-load window-system.min.js + shell-overlays.min.js (~50 KB combined) which trunk doesn't fetch until idle. Under heavy bandwidth constraints these extra bytes push the load event ~1 s later. The right mitigation is to switch those two preloads to <link rel="prefetch"> (lower priority, doesn't block load). Recommended follow-up; one-line PHP change.
  3. Warm-load FCP drops from 1 176 ms to 528 ms — a 55 % improvement. This IS the win the changes were designed for. A returning user (which is essentially every user beyond the first visit) sees the shell paint in half a second instead of over one second.
  4. Service Worker is doing massive work on warm loads — 0 JS bytes transferred on warm. The SW is fully caching the bundles. Phase 3a's precache expansion helps the FIRST warm load (just after SW installs) by ensuring the cache is pre-populated.

The cold/warm tradeoff visualized

                Cold FCP         Warm FCP
trunk-min:      10 540 ms        1 176 ms
speed-min:      11 484 ms          528 ms
                ─────────        ─────────
                +944 ms          −648 ms
                (slower, once)   (faster, every subsequent visit)

Net effect for a user visiting wp-admin once per day for a week:

  • trunk: 1 × 10 540 + 6 × 1 176 = 17 596 ms total page-load time
  • speed: 1 × 11 484 + 6 × 528 = 14 652 ms total page-load time
  • speed saves ~3 s of total wait over a week, despite paying +1 s on day 1.

For a user visiting multiple times per day, the warm-load gain dominates entirely.

Experiment: prefetch / deferred auto-open (didn't pan out — tested 2026-05-23)

Two follow-up experiments were measured under the same throttled conditions:

Experiment 1 — switch lazy bundles to <link rel="prefetch">: Hypothesis was that lower-priority prefetch would close the +1 s cold-load gap. Reality:

  • Pure prefetch on both lazy bundles: warm LCP regressed from 528 ms → 2532 ms (+2 s). The browser deferred the prefetch fetch under throttling, so when manager.open() fired the boot-time window, window-system.min.js wasn't cached and had to fetch over the throttled network.
  • Hybrid (window-system preload, shell-overlays prefetch): warm FCP regressed 528 ms → 1076 ms (+548 ms). Smaller penalty but still real.
  • Conclusion: <link rel="preload"> is the correct hint for both lazy bundles. requestIdleCallback-driven preloading inside the main bundle is the secondary mechanism — both load paths agree on "fetch eagerly." The current code is the right config; reverted both experiments.

Experiment 2 — defer or skip openCurrentPage(): The shell auto-opens the current admin URL as a window on boot. The Dashboard iframe is the heaviest single thing in the shell.

Variant Cold loadEventEnd Warm FCP
baseline (auto-open immediate) 52 354 ms 528 ms
auto-open deferred via requestIdleCallback 51 369 ms 808 ms
auto-open disabled entirely 43 534 ms 1 196 ms
  • Deferring via requestIdleCallback saves ~1 s on loadEventEnd — the iframe still loads inside the load window, the defer just shifts it by a few ms. Doesn't actually unblock first paint of the shell.
  • Skipping auto-open entirely saves ~9 s on cold loadEventEnd — the iframe never loads at all. This is real, big, and worth pursuing as a future opt-in feature.
  • Conclusion: deferring isn't enough. The full win requires either (a) the user explicitly opening windows from the dock instead of having the URL auto-open one, or (b) lazy-loading the iframe (initialize the Window but defer the iframe src until the window is in viewport / focused).

Recommended next step: OS Settings toggle for auto-open

Add a OS Settings → Features → Auto-open current page on shell load checkbox. Default ON (preserves current behaviour). Power users who prefer a dock-driven workflow flip it off and recover ~9 s of cold-load time on every visit.

Implementation sketch:

  1. Add autoOpenCurrentPage: true to the OsSettings defaults.
  2. In desktop.ts, gate the existing if ( shouldAutoOpenCurrentPage( … ) ) block on && osSettings.state.autoOpenCurrentPage !== false.
  3. Surface in OS Settings → Features tab with a clear explainer: "Open the current admin URL as a window automatically on shell boot. Disable for a faster cold load that shows just the desktop + dock."
  4. Keep fromPortal=false direct-URL navigation honored regardless — the user typed that URL, they meant it. Only suppress the portal-entry auto-open + the default-window auto-open.

This is a ~50-line PHP+TS change. Recommended for a separate PR after the current speed-improvements branch lands.


Rollup across all three measurement scenarios

Metric Unminified medians Minified medians Throttled medians (posts)
FCP rollup −0.9 % −14.8 % warm: −55 %, cold: +9 %
LCP rollup +2.2 % −21.6 % warm: −55 %, cold: +3 %
Shell ready −0.5 % −8.9 % warm: −17 %, cold: +3 %
DCL rollup −6.4 % −13.7 % (not isolated)
responseEnd −1.1 % −13.7 % (server-bound, similar)

Net verdict: the changes ship a clear win for returning users (the common case) and a measurable but acceptable cost for first-time visitors (which the prefetch swap above would close). The minified rollup numbers are the production-equivalent numbers.

Open WordPress Playground Preview

AllTerrainDeveloper and others added 5 commits May 23, 2026 14:07
Two additions to includes/render/assets.php that move work off the
first-paint critical path:

- desktop_mode_print_preload_hints() on admin_print_styles @ 1 emits
  <link rel="preload"> for desktop.min.js + desktop.css + the lazy
  window-system / shell-overlays bundles. The browser starts fetching
  the 464 KB main bundle during HTML head parse instead of discovering
  it ~1 RTT later in the footer <script> tag. Filterable via
  desktop_mode_preload_hints so plugins can opt their own critical
  bundle URLs in.

- desktop_mode_defer_non_critical_styles() on style_loader_tag rewrites
  three handles (desktop-mode-dock-peek, desktop-mode-ai-assistant,
  desktop-mode-bug-report) to the media="print" onload="this.media='all'"
  pattern with a <noscript> fallback. Each is only consumed after a
  user action (dock hover, Cmd+K, Report-a-bug click), so blocking
  first paint on them was pure waste. Filterable via
  desktop_mode_deferred_styles.

Together, estimated 100-250 ms FCP win on cold midrange loads.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three coordinated changes in src/pwa/sw.ts:

- PRECACHE_PATHS now includes assets/js/desktop.min.js,
  assets/js/window-system.min.js, assets/js/shell-overlays.min.js
  alongside the existing CSS files. The first PWA install
  pre-warms the cache with the shell's critical-path JS, so
  offline / slow-network falls back to real bytes instead of
  returning 504.

- staleWhileRevalidate() and networkFirstForAsset()'s fallback
  branch now pass ignoreSearch: true to cache.match(). Without this
  the precached unversioned URLs never hit, because every runtime
  request carries a ?ver=<filemtime> param appended by WordPress.

- VERSION bumped to 0.8.0-pwa-4 so the activate-handler eviction
  sweep installs the new cache buckets cleanly across users.

Intentionally NOT switching the JS handler to stale-while-revalidate.
That would re-introduce PR #121's "install icon hidden in standalone"
regression (post-deploy first navigation serves stale cached bundle).
The load-bearing comment at sw.ts:155-167 explains why network-first
is the right baseline. Full SWR for JS is deferred to a separate PR
with explicit mitigation (postMessage version-check from main bundle
or Clear-Site-Data header on deploy).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds scheduleIdleBoot() helper near the top of src/desktop.ts and
wraps six boot calls inside init() that don't need to run before
first paint:

- installIframeDropTargets — drop overlays only matter while the
  user is actively dragging.
- new IframeCommandBridge(...).install() and
  new ShellCommandHarvester(...).install() — Cmd+K palette wiring,
  not consumed until the palette opens.
- installRecycleBinDropTargets — same drop-target rationale.
- bootNonceRefresh — pure heartbeat consumer; first tick is ~15 s out.
- bootPresenceProbe — same.

Each call is a pure listener / heartbeat-subscriber registration,
so moving them to idle removes ~5-20 ms of synchronous CPU work
from init() without changing observable behavior. The helper falls
back to setTimeout(fn, 0) when requestIdleCallback isn't available
(Safari < 17).

Risky boot calls stay eager for now:
- bootHeartbeatBus — depended on by other eager calls.
- bootStickyNotes — mounts visible UI; deferring would FOUC.
- startRecycleBinBadge — needs initial badge paint.
- installBroadcastReceiver — message listener; iframe bridges
  send messages right after iframe boot.
- startFilesHeartbeat / startFilesRestoreSync — files sync infra.

Phase 7.2 (drop heartbeat + jquery from main bundle deps) is
blocked on deferring all heartbeat consumers — coming in a
follow-up after the risky six are individually analyzed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…erver

includes/render/chromeless-bridge.php — the
desktop_mode_chromeless_offset_neutralizer_script() helper used to
run document.querySelectorAll('*') + getComputedStyle() per element
TWICE per iframe boot (DOMContentLoaded + load), to catch
React-mounted components that appeared between those two events.
On a Gutenberg or WooCommerce admin page that's ~2,000+
getComputedStyle() calls forcing layout flushes — observably 1-2
seconds of jank per iframe boot.

Replaces with ONE walk at DOMContentLoaded + a MutationObserver
on document.body that fixes added nodes (and their descendants) as
they mount. The observer only inspects nodes it just saw added, so
the per-mutation cost is O(added subtree size) instead of O(whole
DOM size). The old `load`-time walk is kept as a defense-in-depth
fallback only when MutationObserver isn't available (pre-IE 11 — an
extreme outlier today).

Validated: php -l clean, node --check on the extracted JS clean,
npm run build green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ync)

src/desktop.ts — extends the Phase 7.1 deferral set with three more
boot calls that match the same "pure listener registration" shape as
the previous batch:

- installBroadcastReceiver — only listens for INCOMING postMessages
  from iframes. Iframes can't post until their own admin_footer
  bootstrap runs, which lands well after the parent's idle callback
  drains. attachBroadcastBus (which is needed for OUTGOING broadcasts
  during init) stays eager.
- startFilesHeartbeat — pure heartbeat contributor + subscriber;
  first tick is ~15 s out.
- startFilesRestoreSync — pure broadcast subscriber; only fires when
  the user restores something from the Recycle Bin.

After this commit, the "safe" deferred list is 9 boots; the "risky"
list remaining eager is bootHeartbeatBus (other deferred boots depend
on it being initialized first), bootStickyNotes (paints visible UI),
and startRecycleBinBadge (needs initial badge value on first paint).
Phase 7.2 (dropping heartbeat/jquery deps) still wants the heartbeat
bus deferred — that's a follow-up after analyzing the bus ↔ consumer
ordering more carefully.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

✅ WordPress Plugin Check Report

✅ Status: Passed

📊 Report

All checks passed! No errors or warnings found.


🤖 Generated by WordPress Plugin Check Action • Learn more about Plugin Check

…t tag

Two Plugin Check findings flagged after the perf batch landed:

1. PluginCheck.CodeAnalysis.Heredoc.NotAllowed at
   includes/render/chromeless-bridge.php — the offset-neutralizer
   inline JS was a `<<<JS ... JS;` heredoc, which Plugin Check
   disallows. Converted to a string-concatenation builder with
   single-quoted PHP literals. The compiled output is identical
   modulo +33 bytes of whitespace; node --check on the emitted
   script still parses clean.

2. WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet at
   includes/render/assets.php — the desktop_mode_defer_non_critical_styles
   filter rewrites the <link rel="stylesheet"> tag WP is in the
   middle of emitting for an already-registered + enqueued handle.
   The linter doesn't trace style_loader_tag filter context, so the
   raw <link> output reads as a false positive. Wrapped the sprintf
   in phpcs:disable / phpcs:enable annotations with a reason string
   explaining the context.

Verified: php -l clean, node --check on inline JS clean, npm run
lint / typecheck / test:js / build all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AllTerrainDeveloper

Copy link
Copy Markdown
Collaborator Author

@claude can you review this PR?

@claude

claude Bot commented May 23, 2026

Copy link
Copy Markdown

Claude finished @AllTerrainDeveloper's task in 4m 24s —— View job


Code Review — Speed Improvements PR #273

Tasks:

  • Load tools and set up review
  • Read changed files
  • Review includes/render/assets.php
  • Review includes/render/chromeless-bridge.php
  • Review includes/ajax.php
  • Review assets/css/chromeless.css
  • Review TypeScript files (desktop.ts, boot/link-interceptor.ts, desktop-files/built-in-openers.ts, desktop-icons.ts)
  • Review src/pwa/sw.ts
  • Review tests/phpunit/tests/desktopModeAjaxSave.php
  • Compile and post final review

Summary

This PR is a well-engineered, carefully benchmarked set of performance improvements. The changes are focused and each technique is sound. Below are observations grouped by severity.


Findings

Medium — sw.ts: Dead branch in pluginAssetBase()

src/pwa/sw.ts lines 270–277:

function pluginAssetBase(): string {
    const here = sw.location.pathname;
    const idx = here.indexOf( '/desktop-mode/' );
    const origin = sw.location.origin;
    if ( idx >= 0 ) {
        return origin + '/wp-content/plugins/desktop-mode/';
    }
    return origin + '/wp-content/plugins/desktop-mode/';  // ← identical
}

Both branches return the exact same string — idx is computed but its value never affects the result. This looks like the original intent was to extract a relative plugin path from here, but the implementation didn't follow through. The precache will silently no-op on any host with a non-standard plugins directory (e.g. Bedrock/Trellis's web/app/plugins/), because the hardcoded path won't match real asset URLs — but since the PR description already acknowledges this limitation and staleWhileRevalidate covers runtime caching, the practical impact is low. Still worth fixing the dead code. Fix this →


Medium — assets.php: CSS deferral onload attribute uses esc_attr for media in JS context

includes/render/assets.php line 711:

'<link rel=\'stylesheet\' id=\'%1$s\' href=\'%2$s\' media=\'print\' onload="this.media=\'%3$s\'; this.onload=null;" />'

The %3$s is filled by esc_attr( $resolved_media ). esc_attr escapes " and & but does not escape single quotes. Inside the onload="this.media='%3$s';" string, a media value containing a ' would break the JavaScript (e.g. a media query like screen and (min-width: 100px) is fine, but an adversarially crafted '; malicious_code(); var x=' would inject). In practice, $resolved_media comes from wp_enqueue_style's $media parameter, which is always a CSS media type string — this is a low-severity concern, but a defense-in-depth fix would be esc_js( $resolved_media ) for the JS context, or wrapping in a json_encode. Fix this →


Low — assets.php: Style deferral filter fires on every admin page, not just when desktop mode is active

includes/render/assets.php line 677:

function desktop_mode_defer_non_critical_styles( $html, $handle, $href, $media ) {
    if ( desktop_mode_is_chromeless_request() ) {
        return $html;
    }
    // ...

The style_loader_tag filter hooks into EVERY admin page load for every stylesheet. It guards against chromeless requests but not against desktop mode being disabled. Since the deferred handles (desktop-mode-dock-peek, etc.) are only enqueued when desktop mode is enabled, the in_array check will always fail on classic-admin pages — functionally correct, but it allocates and runs the apply_filters('desktop_mode_deferred_styles', …) call on every stylesheet on every admin page. Adding ! desktop_mode_is_enabled() as an early return would eliminate this overhead for users who haven't enabled the mode. Fix this →


Low — chromeless.css: WooCommerce header hide is broader than wc-orders

assets/css/chromeless.css lines 228–235:

.desktop-mode-chromeless.woocommerce-page .woocommerce-layout__header,
.desktop-mode-chromeless.woocommerce-admin-page .woocommerce-layout__header {
    display: none !important;
}

The woocommerce-page and woocommerce-admin-page body classes apply to all WooCommerce-connected pages — Products, Coupons, Customers, Analytics, etc. — not just Orders. The PR comment focuses on the wc-orders scenario, but the rule hides the React EmbedHeader Activity Panel across every WC-admin screen. The Activity Panel's unread-notifications badge is the most useful affordance on non-Orders WC pages. Consider scoping to known HPOS-connected pages that actually render the conflicting embed header (e.g. wc-orders-php body class), or at minimum document the broader scope in the comment block.


Low — desktop.ts: 9 separate scheduleIdleBoot calls

src/desktop.ts contains 9 independent scheduleIdleBoot( () => ... ) calls. Each issues a separate requestIdleCallback registration. The browser may batch some into the same idle frame, but may also spread them across many idle windows. The comment in scheduleIdleBoot acknowledges that the split is intentional so the browser can interleave network prefetch with CPU work — that reasoning is valid for the preload-vs-boot split. However, for the 6 heartbeat-related calls (bootNonceRefresh, startFilesHeartbeat, startFilesRestoreSync, bootPresenceProbe, installBroadcastReceiver, installRecycleBinDropTargets) there's no network work to interleave with, so batching them into a single callback would guarantee they run in one idle window and reduce scheduling overhead without any tradeoff.


Low — desktop.ts: pointerdownOnWallpaper guard and multi-pointer interaction

src/desktop.ts adds:

let pointerdownOnWallpaper = false;
desktopArea.addEventListener( 'pointerdown', ( e: PointerEvent ) => {
    pointerdownOnWallpaper = e.target === desktopArea;
} );

Under multi-touch (pinch-to-zoom on a touch-capable device), each touch fires a separate pointerdown. The last one to fire wins. If a user's second finger incidentally touches the wallpaper during a window pinch-resize, pointerdownOnWallpaper is set to true and the wallpaper click guard doesn't fire. This is a minor edge case on desktop admin (touch screens are rare there), but worth noting for completeness.


Positive notes

The implementation quality is high across the board:

  • Preload hints (desktop_mode_print_preload_hints) are correctly gated, correctly escaped, and correctly extensible via desktop_mode_preload_hints filter.
  • CSS deferral uses the standard media="print" onload pattern with a correct <noscript> fallback and <noscript> IDs are correctly suffixed (-noscript) to avoid duplicate IDs.
  • SW staleWhileRevalidate fix: the separation of exact runtime-cache lookup vs. ignoreSearch static-precache fallback correctly addresses the stale-bundle bug while preserving offline coverage.
  • SW networkFirstForAsset: the { cache: 'reload' } flag is load-bearing (bypasses HTTP cache) and the comment explaining why is accurate and thorough.
  • MutationObserver refactor in the offset neutralizer is correct — observing childList: true, subtree: true on document.body and only calling fixOne on added nodes is the right trade-off (per-addition rather than per-DOM-mutation).
  • deriveWindowId unification across desktop-icons.ts and built-in-openers.ts fixes a real UX bug (duplicate windows) without changing any public API.
  • isAdminBarNew in link-interceptor.ts: the anchor.closest('#wp-admin-bar-new-content') detection is correct and handles both the parent "+" New link and all child entries uniformly.
  • AJAX redirect change (admin_url('index.php?desktop_mode_portal=1')) is well-motivated, correctly tested, and the new tests cover the redirect contract explicitly.
  • Test coverage: the three new test cases (test_enable_response_redirects_to_dashboard_with_portal_flag, test_disable_response_redirects_to_plain_admin_not_portal, test_non_one_truthy_values_disable_mode) cover the changed behavior precisely.

Overall: solid work, ready to merge with the minor issues above addressed. The perf numbers are real and the implementation matches the methodology.


…ng for improved user experience

perf(assets): optimize non-critical styles loading in admin context for better performance
perf(desktop): coalesce idle boot execution to reduce overhead and improve responsiveness
refactor(pwa): enhance plugin asset URL handling for better compatibility with non-standard setups
@AllTerrainDeveloper AllTerrainDeveloper merged commit 6b5575c into trunk May 23, 2026
5 checks passed
@AllTerrainDeveloper AllTerrainDeveloper deleted the speed-improvements branch May 23, 2026 15:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant