PluginProbe
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN / 1.3.3
xSpeed Cache: AI-Powered Performance Hub with MCP, Caching & CDN v1.3.3
1.3.3 1.3.2 1.3.1 1.3.0 1.2.4 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.1.8 All 29 releases
← All changes | includes/class-preloader.php +121 -0 1.1.81.3.3 View file →
@@ -39,8 +39,38 @@
39 39 public const USER_AGENT = 'xSpeed-Preloader/1.0 (+cache warmer; admin-initiated)';
40 40 public const REQUEST_TIMEOUT = 8;
41 41
42 42 /**
43 + * Default cap on NEW remote images resolved per warmed page.
44 + *
45 + * A crawl warms the cache; it is not a licence to hit third-party hosts
46 + * hundreds of times for one page.
47 + *
48 + * The cap counts only images whose dimensions are not already known, and
49 + * results persist between runs — so each crawl advances through a heavily
50 + * embedded page rather than re-picking the same first N. That is what
51 + * makes a cap safe here: without the skip it would strand everything past
52 + * the limit permanently, because images appear in the same DOM order
53 + * every time.
54 + *
55 + * 20 is a starting point, not a measurement. Sites that embed more can
56 + * raise it via `xspeed_preloader_remote_dimension_limit`.
57 + */
58 + private const REMOTE_DIMENSION_LIMIT = 20;
59 +
60 + /**
61 + * How many new remote images one warmed page may resolve.
62 + */
63 + private static function remote_dimension_limit(): int {
64 + /**
65 + * Filter the per-page cap on remote dimension lookups.
66 + *
67 + * @param int $limit Default 20. Values below 1 disable the lookup.
68 + */
69 + return (int) apply_filters( 'xspeed_preloader_remote_dimension_limit', self::REMOTE_DIMENSION_LIMIT );
70 + }
71 +
72 + /**
43 73 * Why the top-level sitemap fetch failed on this request, or '' when it
44 74 * succeeded. Set by fetch_sitemap_urls(), read by resolve_queue() — the
45 75 * reason has to survive the return of an empty array, which is exactly
46 76 * what it could not do before. Request-scoped; never persisted. (#142)
@@ -311,8 +341,99 @@
311 341 'error' => sprintf( 'HTTP %d', $code ),
312 342 'ts' => time(),
313 343 );
314 344 $state['errors'] = array_slice( $state['errors'], -20 );
345 + return;
346 + }
347 +
348 + self::warm_remote_dimensions( (string) wp_remote_retrieve_body( $response ) );
349 + }
350 +
351 + /**
352 + * Resolve dimensions for externally hosted images found on a warmed page.
353 + *
354 + * The crawl already has the HTML in hand, so harvesting image URLs from it
355 + * costs nothing extra — and this is the one place where paying for a
356 + * remote lookup is free of consequence, because no visitor is waiting.
357 + *
358 + * An image on another domain has no local file to measure, so the front
359 + * end skips it and the page ships without width/height — which is layout
360 + * shift, on precisely the sites least able to fix it by hand (a CDN, a
361 + * sister site, a shared asset host). Warming here means the NEXT render
362 + * finds the dimensions in cache and stamps them, with the visitor paying
363 + * nothing.
364 + *
365 + * Deliberately bounded per page: a crawl should not turn into a scraper
366 + * for a page embedding hundreds of third-party images.
367 + *
368 + * @param string $html The warmed page's HTML.
369 + */
370 + private static function warm_remote_dimensions( string $html ): void {
371 + if ( '' === $html || ! class_exists( '\XSpeed\Lazy_Loader' ) ) {
372 + return;
373 + }
374 +
375 + $opts = Settings_Manager::get( 'lazy' );
376 + if ( empty( $opts['add_missing_dimensions'] ) ) {
377 + return;
378 + }
379 +
380 + // Match any <img>, not only one carrying `src`. The URL worth warming
381 + // may live in a lazy attribute instead — which is the whole point of
382 + // #328 — and resolvable_image_url() below is what knows where to look.
383 + if ( ! preg_match_all( '#<img\b[^>]*>#i', $html, $m, PREG_SET_ORDER ) ) {
384 + return;
385 + }
386 +
387 + $home = wp_parse_url( home_url(), PHP_URL_HOST );
388 + $targets = array();
389 + foreach ( $m as $tag ) {
390 + // Only tags MISSING a dimension are worth resolving — one that
391 + // already declares both needs nothing.
392 + // Same lookbehind as Lazy_Loader::ensure_dimensions(): a bare
393 + // `\bwidth=` also matches `data-width=`, so a slider carrying its
394 + // own metadata looked already-sized and was skipped from warming.
395 + // The two must agree, or the collector skips exactly the tags the
396 + // renderer still needs measured. (#333 review round 3, issue 2)
397 + if ( preg_match( '#(?<![-\w])width\s*=#i', $tag[0] ) && preg_match( '#(?<![-\w])height\s*=#i', $tag[0] ) ) {
398 + continue;
399 + }
400 + // Ask the same resolver the render path uses, rather than reading
401 + // `src` directly. A slider parks a spacer in `src` and the real
402 + // URL in `data-lazy`/`data-src`/`data-original`, so a collector
403 + // looking only at `src` warmed the SPACER and never the image —
404 + // leaving remotely-hosted slider images unresolvable at render
405 + // time, the exact markup #328 is about. (#333 review round 2,
406 + // issue 3)
407 + // `false`: do not let the resolver settle a name-refused URL by
408 + // MEASURING it. That is circular here — remote measurement is
409 + // gated until warm_dimensions() sets $warming, and this collector
410 + // is what feeds warm_dimensions(). Take the URL the tag offers and
411 + // let the warm pass decide. (#333 review round 3, issue 3)
412 + $src = Lazy_Loader::resolvable_image_url( $tag[0], false );
413 + if ( '' === $src || ! preg_match( '#^https?://#i', $src ) ) {
414 + continue;
415 + }
416 + $host = wp_parse_url( $src, PHP_URL_HOST );
417 + if ( ! $host || $host === $home ) {
418 + continue; // Local images already resolve from disk.
419 + }
420 + // Already resolved (or already known unresolvable) — looking it up
421 + // again costs a request and teaches us nothing. Skipping it is
422 + // also what makes the cap below advance: images appear in the same
423 + // DOM order every crawl, so a collector that did not skip would
424 + // re-pick the same first N for ever and never reach the rest.
425 + if ( Lazy_Loader::dimensions_known( $src ) ) {
426 + continue;
427 + }
428 + $targets[ $src ] = true;
429 + if ( count( $targets ) >= self::remote_dimension_limit() ) {
430 + break;
431 + }
432 + }
433 +
434 + if ( $targets ) {
435 + Lazy_Loader::warm_dimensions( array_keys( $targets ) );
315 436 }
316 437 }
317 438
318 439 private static function mark_complete( array $state ): void {