How We Built One of the Fastest WordPress Analytics Trackers
From #7 to #2: the three-phase optimization that cut our LCP impact by 43% using async loading and inline core architecture.
From #7 to #2 in a Single Day
When we benchmarked Statnive against 7 other WordPress analytics plugins, the results were humbling. Our TTFB was excellent — 4th fastest. But our Largest Contentful Paint ranked dead last among self-hosted plugins. The gap between our server response and when visitors actually saw content was 202 milliseconds. Koko Analytics achieved 94ms. Burst Statistics achieved 80ms. We were at 202ms.
The problem was not the tracker code itself. It was how WordPress loaded it.
By the end of the day, we had cut that gap to 79 milliseconds and moved from 7th to 2nd place. LCP dropped from 504ms to 288ms — a 43% improvement. Here is exactly what we changed and why.
The Benchmark: 8 Plugins, Real Chromium, Real Load
We built an automated testing framework that toggles analytics plugins via the WordPress REST API, runs real Chromium browser visits via k6, and collects Core Web Vitals through PerformanceObserver. Each plugin runs in complete isolation — all other analytics plugins deactivated, caches flushed, server warmed up with 5 requests before measurement begins.
The before-and-after results:
| Metric | Before | After | Change |
|---|---|---|---|
| Statnive TTFB | 294ms | 209ms | -29% |
| Statnive FCP | 496ms | 288ms | -42% |
| Statnive LCP | 504ms | 288ms | -43% |
| TTFB-to-LCP gap | 202ms | 79ms | -61% |
| Ranking (LCP) | #7 of 8 | #2 (tied) | +5 positions |
Root Cause: Three Performance Killers
We traced the 202ms gap to three issues in FrontendHandler.php, each independently confirmed by WordPress Core documentation and web performance research.
Problem 1: wp_localize_script forces blocking mode. WordPress 6.3 introduced native async/defer support via the strategy parameter. But wp_localize_script() generates an inline script in the “after” position, which — per WordPress Core Trac #58632 — forces the parent script to blocking mode and cascades through the entire dependency tree. Every script in the chain loses its async/defer strategy.
Problem 2: No async or defer attribute. Our tracker was enqueued with ['in_footer' => true] but no strategy parameter. Even in the footer, a synchronous script blocks the browser from firing the load event until download and execution complete.
Problem 3: SRI hash computed on every page load. We called file_get_contents() + hash('sha256', ...) on every single page request to generate the Subresource Integrity hash. That is a filesystem read plus CPU-intensive hashing on every visitor.
Get Statnive: Performance-First Self-Hosted Analytics
All the optimizations described here ship with Statnive today. Install free from WordPress.org — your data stays on your server, your pages stay fast.
Phase 1: Fix the Loading Strategy
The single biggest win came from three changes to FrontendHandler.php:
Replace wp_localize_script with wp_add_inline_script('before'). The 'before' position is critical — it does NOT cascade to blocking mode. The 'after' position (which is the default) does cascade. This distinction is documented in the official WordPress 6.3 script loading announcement but easy to miss.
// Before (forces blocking):
wp_localize_script( 'statnive-tracker', 'StatniveConfig', $config );
// After (safe with async):
wp_add_inline_script(
'statnive-tracker',
'window.StatniveConfig=' . wp_json_encode( $config ) . ';',
'before' // MUST be 'before' — 'after' cascades to blocking
);
Add strategy: 'async' to wp_enqueue_script. For analytics trackers that don’t need DOM access, async is better than defer. Defer waits for full HTML parsing (500ms+ on complex pages). Async executes as soon as the download completes. Our tracker reads window.StatniveConfig and fires navigator.sendBeacon() — neither requires the DOM.
Cache the SRI hash in a WordPress transient. Keyed by filemtime(), the hash is computed once and reused until the file changes. New build = new modification time = automatic cache invalidation.
Phase 2: Free the Main Thread
With async loading in place, we turned to the tracker JavaScript itself.
Remove the DOMContentLoaded wrapper. With async, the script executes as soon as it downloads. The tracker reads window and navigator globals — no DOM needed. The DOMContentLoaded event listener was adding unnecessary delay.
Defer non-critical modules via requestIdleCallback. The pageview hit is the only critical-path operation. Engagement tracking (scroll depth, time on page), auto-tracking (outbound links, form submissions), and CSS event tracking can all wait for the browser to become idle. Safari has supported requestIdleCallback natively since September 2024, so no polyfill is needed for modern browsers.
// Critical path: fires immediately
sendHit(buildPayload());
// Deferred: runs when browser is idle
var idle = window.requestIdleCallback || function(cb) { setTimeout(cb, 80); };
idle(function() {
engagementTracker.start();
registerAutoTracking(sendEvent);
});
The key insight from our research: do NOT pass a timeout parameter to requestIdleCallback. A timeout forces execution even during user interaction, which can cause jank and hurt INP scores. Let the browser decide when it is truly idle.
Phase 3: Eliminate the External Request
The final optimization eliminates the external script download from the critical rendering path entirely. Inspired by how Google’s gtag.js uses a queue-based inline bootstrap, and how Koko Analytics inlines its entire 468-byte tracker, we created a two-stage architecture.
Stage 1: Inline core tracker (1.1KB). A minimal IIFE that reads the config, checks privacy signals (DNT/GPC), runs 4 bot-detection heuristics, builds the pageview payload, and fires it via navigator.sendBeacon(). This is printed directly into the HTML via wp_print_inline_script_tag() in wp_footer. Zero external requests.
Stage 2: Async full tracker (5KB). The complete tracker with engagement, events, auto-tracking, and consent management loads with strategy: 'async'. When it initializes, it checks window.statnive_hit_sent — if the inline core already fired the pageview, it skips straight to deferred module initialization. No duplicate hits.
The result: the pageview fires from inline JavaScript before any external resource completes loading. The full feature set loads in the background without affecting any Core Web Vital.
Results by Phase
Each phase was independently deployed and measured:
| Phase | Change | Gap | LCP |
|---|---|---|---|
| Before optimization | Blocking script, no strategy | 202ms | 504ms |
| Phase 1: async + inline config | Non-blocking download | ~80ms | ~374ms |
| Phase 2: requestIdleCallback | Freed main thread | ~65ms | ~359ms |
| Phase 3: inline core tracker | Zero external requests | 79ms | 288ms |
Research-Backed Decisions
Every technical decision was validated against published research and official documentation. We consulted over 100 sources across WordPress Core Trac tickets, web.dev performance guides, W3C specifications, and production reliability studies. Key findings that shaped our approach:
wp_add_inline_script('before')is explicitly documented as safe with async/defer strategies (Make WordPress Core, July 2023)- Script injection via
createElementis 2.1 seconds slower than native<script async>because it bypasses the browser’s preload scanner (Ilya Grigorik, Google) navigator.sendBeacon()achieves 95.8-98% delivery reliability when paired withvisibilitychangeandpagehideevents (NicJ.net production study, 2M+ page views)- Mobile JavaScript parse/compile is 2-5x slower than desktop, but at 5KB our tracker is well under the 50KB threshold where splitting becomes necessary (Addy Osmani, Google)
Common Questions
Does the inline core tracker work with content security policies?
Yes. wp_print_inline_script_tag() respects WordPress’s wp_inline_script_attributes filter, which can add a nonce for CSP compliance. The inline script is generated server-side and contains no user input.
What happens if the async full tracker fails to load?
The pageview is already recorded by the inline core. You lose engagement and event tracking for that session, but the core analytics data is captured. This is a graceful degradation — the most important metric (pageview) has the most reliable delivery.
Why async instead of defer for the full tracker?
Defer waits for complete HTML parsing before executing. For an analytics tracker that does not manipulate the DOM, this is unnecessary delay. Async downloads in parallel and executes immediately. The inline 'before' script guarantees StatniveConfig is available before the async script runs.
Will this approach work on WordPress versions before 6.3?
The strategy parameter requires WordPress 6.3+. On older versions, the parameter is silently ignored and the script loads as a standard footer script — still functional, just without the async optimization. Statnive requires WordPress 6.4+.
What’s Next
Our tracker is now tied for 2nd place in LCP performance. The 2ms gap to 1st place (Jetpack at 286ms vs our 288ms) is within measurement noise. But we are not done. The next areas of investigation:
- Compile-time feature variants (Plausible’s model): Generate different tracker builds based on enabled features, so sites that don’t use engagement tracking get an even smaller script
- Service Worker persistence: Queue events in a service worker for 100% delivery reliability, even on spotty mobile connections
- Server-side TTFB reduction: Profile the PHP hit endpoint to shave milliseconds from the server response
Performance is not a feature you ship once. It is a discipline you practice on every release.