get('http_home', []); $headers = isset($home['headers']) && is_array($home['headers']) ? $home['headers'] : []; $status = (int) ($home['status_code'] ?? 0); // Normalize header keys. $h = []; foreach ($headers as $k => $v) { $key = strtolower((string) $k); $h[$key] = $v; } // Cache detection (heuristics). $cache_control = isset($h['cache-control']) ? (is_array($h['cache-control']) ? implode(',', $h['cache-control']) : (string) $h['cache-control']) : ''; $age = isset($h['age']) ? (string) $h['age'] : ''; $x_cache = ''; foreach (['x-cache','x-cache-status','cf-cache-status','x-litespeed-cache','x-rocket','x-proxy-cache','x-fastcgi-cache','x-varnish'] as $k) { if (isset($h[$k])) { $x_cache .= $k . '=' . (is_array($h[$k]) ? implode(',', $h[$k]) : (string) $h[$k]) . ' ; '; } } $has_cache = false; if ($cache_control !== '' && preg_match('/max-age\s*=\s*\d+/i', $cache_control) && !preg_match('/no-cache|no-store|must-revalidate/i', $cache_control)) { $has_cache = true; } if ($age !== '' && is_numeric($age) && (int) $age > 0) { $has_cache = true; } if ($x_cache !== '') { $has_cache = true; } if (!$has_cache) { $findings[] = $this->finding( 'perf_cache_headers_missing', I18n::tr('No cache headers seen on the homepage', 'لا تظهر ترويسات تخزين مؤقت على الصفحة الرئيسية'), 'Performance', // 4.0.18: one request without cache headers is a hint, not proof. // Many hosts and CDNs cache without advertising it, so this is a // low-severity check-this item instead of a high -10 penalty. 'low', -3, [ 'source' => 'http', 'details' => sprintf( I18n::tr('Homepage answered %1$s with Cache-Control: %2$s, Age: %3$s and no cache status header (x-cache, cf-cache-status and similar).', 'أعادت الصفحة الرئيسية رمز الحالة %1$s، وترويسة التخزين (Cache-Control): %2$s، وعمر النسخة (Age): %3$s، ولا توجد ترويسة تبيّن حالة التخزين المؤقت.'), (string) $status, $cache_control !== '' ? $cache_control : I18n::tr('none', 'لا يوجد'), $age !== '' ? $age : I18n::tr('none', 'لا يوجد') ), 'samples' => [ 'cache-control' => Redactor::redact($cache_control, 400), 'age' => Redactor::redact($age, 80), 'cache_signals' => Redactor::redact($x_cache, 400), ], ], I18n::tr('A page cache serves visitors a ready copy instead of building the page on every visit, which makes the site faster under load. Some hosts cache without sending these headers, so confirm with your host before changing anything.', 'التخزين المؤقت للصفحات يقدّم للزائر نسخة جاهزة بدلًا من بناء الصفحة في كل زيارة، فيبقى الموقع سريعًا وقت الضغط. بعض الاستضافات تخزّن الصفحات دون إرسال هذه الترويسات، لذلك تحقّق من الاستضافة قبل أي تغيير.'), [ I18n::tr('Ask your host whether page caching is on, or turn it on in your cache plugin.', 'اسأل الاستضافة عن تفعيل التخزين المؤقت للصفحات، أو فعّله من إضافة التخزين المؤقت لديك.'), I18n::tr('Open the homepage in a private window twice and check the response headers for max-age, age or a cache status.', 'افتح الصفحة الرئيسية مرتين في نافذة خاصة، وابحث في ترويسات الاستجابة عن مدة التخزين (max-age) أو عمر النسخة (age) أو حالة التخزين المؤقت.'), ], [ 'how' => I18n::tr('Run a new audit after turning caching on.', 'شغّل فحصًا جديدًا بعد تفعيل التخزين المؤقت.'), 'pass_condition' => I18n::tr('The homepage sends a cache max-age, an age above zero or a cache status header.', 'ترسل الصفحة الرئيسية مدة تخزين أو عمر نسخة أكبر من صفر أو ترويسة لحالة التخزين المؤقت.'), ], $this->id() ); } // Compression check. $content_encoding = isset($h['content-encoding']) ? (is_array($h['content-encoding']) ? implode(',', $h['content-encoding']) : (string) $h['content-encoding']) : ''; $vary = isset($h['vary']) ? (is_array($h['vary']) ? implode(',', $h['vary']) : (string) $h['vary']) : ''; $has_compression = (bool) preg_match('/\b(gzip|br|deflate)\b/i', $content_encoding); if (!$has_compression) { $findings[] = $this->finding( 'perf_compression_missing', I18n::tr('Pages are sent without compression', 'الصفحات تُرسل دون ضغط'), 'Performance', 'med', -6, [ 'source' => 'http', 'details' => sprintf(I18n::tr('Homepage compression (content-encoding): %1$s. Vary: %2$s.', 'ضغط الصفحة الرئيسية (content-encoding): %1$s. ترويسة Vary: %2$s.'), $content_encoding !== '' ? $content_encoding : I18n::tr('none', 'لا يوجد'), $vary !== '' ? $vary : I18n::tr('none', 'لا يوجد')), 'samples' => [ 'content-encoding' => Redactor::redact($content_encoding, 100), 'vary' => Redactor::redact($vary, 120), ], ], I18n::tr('Compression makes pages smaller so they reach visitors faster, especially on mobile networks.', 'ضغط الصفحات يقلل حجمها فتصل للزائر أسرع، خاصة على شبكات الجوال.'), [ I18n::tr('Turn on gzip or Brotli compression on the server or in your caching plugin.', 'فعّل ضغط الصفحات (gzip أو Brotli) على الخادم أو من إضافة التخزين المؤقت.'), I18n::tr('Run a new audit and check that compression is reported.', 'شغّل فحصًا جديدًا وتأكد أن الضغط أصبح ظاهرًا.'), ], [ 'how' => I18n::tr('Run a new audit, then open this finding again.', 'شغّل فحصًا جديدًا ثم افتح هذه النتيجة مرة أخرى.'), 'pass_condition' => I18n::tr('The homepage is sent with gzip or Brotli compression.', 'تُرسل الصفحة الرئيسية مضغوطة بطريقة gzip أو Brotli.'), ], $this->id() ); } // Sample pages heuristics. // v3.3.52 P2.4 — asset-weight / blocking-JS / lazyload verdicts only on // real indexable parsed pages (an error/noindex page's asset profile is // not representative of the live content the user actually serves). $pages = $this->indexable_parsed_sample_pages((array) $ctx->get('sample_pages', [])); $too_many_assets = []; $too_many_css = []; // legacy combined check $too_many_external_css = []; // v1.0.64: external CSS requests $excessive_inline_css = []; // v1.0.64: inline style-block bloat $duplicate_assets_pages = []; // v1.0.65: same base URL loaded 2+ times $too_many_js = []; $blocking_js = []; $lazy_found = false; $img_total = 0; $webp_total = 0; $font_swap_detected_anywhere = false; $pages_with_fonts = 0; foreach ($pages as $p) { if (!is_array($p)) { continue; } $url = (string) ($p['url'] ?? ''); $assets = isset($p['asset_counts']) && is_array($p['asset_counts']) ? $p['asset_counts'] : []; $assetDetails = isset($p['asset_details']) && is_array($p['asset_details']) ? $p['asset_details'] : []; $css = (int) ($assets['css_count'] ?? 0); // v1.0.64: split CSS counters (external link + inline style) $external_css = (int) ($assets['external_css_count'] ?? 0); $inline_css = (int) ($assets['inline_css_count'] ?? 0); $inline_css_bytes = (int) ($assets['inline_css_bytes'] ?? 0); $largest_inline_css = (int) ($assets['largest_inline_css_bytes'] ?? 0); // Legacy fallback: if collector hasn't run with the new fields, // approximate from the combined css_count. if ($external_css === 0 && $inline_css === 0 && $css > 0) { $external_css = $css; } $js = (int) ($assets['js_count'] ?? 0); $blocking = (int) ($assets['js_blocking_head'] ?? 0); $lazy = !empty($assets['lazyload_detected']); $imgs = (int) ($assets['images_total'] ?? 0); $webp = (int) ($assets['webp_images'] ?? 0); $img_total += $imgs; $webp_total += $webp; if ($lazy) { $lazy_found = true; } // Font-display heuristic: detect any font usage at all from CSS origins $css_origins = (array) ($assetDetails['css_origins'] ?? []); $has_font_css = false; foreach ($css_origins as $orig) { if (!is_array($orig)) { continue; } $label = strtolower((string) ($orig['label'] ?? '')); if (strpos($label, 'font') !== false || strpos($label, 'google') !== false || strpos($label, 'awesome') !== false) { $has_font_css = true; break; } } if ($has_font_css) { $pages_with_fonts++; } if (!empty($p['font_display_swap_detected'])) { $font_swap_detected_anywhere = true; } // v1.0.58 + v1.0.64: split CSS detection. The original combined // counter was misleading on Elementor/Astra sites that have only // 2 external CSS files but 30+ inline style blocks. External // requests and inline weight are different problems with different // fixes — they need different findings. // // Legacy combined check (kept for backward compat — fires the old // generic finding only if BOTH are notable, so it doesn't fire on // pure-inline sites). if ($css >= 25 && $external_css >= 10) { $too_many_css[] = [ 'url' => $url, 'css' => $css, 'js' => $js, 'css_origins' => $this->compact_asset_origins((array) ($assetDetails['css_origins'] ?? []), 6), ]; } // NEW: external CSS requests heavy (real round-trip cost) if ($external_css >= 15) { $too_many_external_css[] = [ 'url' => $url, 'external_css_count' => $external_css, 'inline_css_count' => $inline_css, 'css_origins' => $this->compact_asset_origins((array) ($assetDetails['css_origins'] ?? []), 6), ]; } // NEW: inline CSS bloat (HTML weight cost) // Threshold: > 80 KB total inline CSS, or a single inline style block > 50 KB if ($inline_css_bytes >= 80 * 1024 || $largest_inline_css >= 50 * 1024) { $excessive_inline_css[] = [ 'url' => $url, 'inline_css_count' => $inline_css, 'inline_css_bytes' => $inline_css_bytes, 'inline_css_kb' => (int) round($inline_css_bytes / 1024), 'largest_inline_block_kb' => (int) round($largest_inline_css / 1024), 'external_css_count' => $external_css, ]; } // v1.0.65: same base URL loaded 2+ times (render-blocking dup). $dups = isset($assetDetails['duplicate_assets']) && is_array($assetDetails['duplicate_assets']) ? $assetDetails['duplicate_assets'] : []; if (!empty($dups)) { $duplicate_assets_pages[] = [ 'url' => $url, 'duplicates' => array_slice($dups, 0, 8), 'duplicate_count' => count($dups), ]; } if ($js >= 20) { $too_many_js[] = [ 'url' => $url, 'css' => $css, 'js' => $js, 'js_origins' => $this->compact_asset_origins((array) ($assetDetails['js_origins'] ?? []), 4), ]; } // v1.0.78: use external_css only (not inline style-block count) to avoid // inflating the "total requests" metric on Elementor sites that have // 30+ inline styles but only 2 actual HTTP requests. $real_requests = $external_css + $js; if ($real_requests >= 25) { $too_many_assets[] = [ 'url' => $url, 'css' => $external_css, 'js' => $js, 'total' => $real_requests, 'note' => 'external CSS + JS requests only (inline excluded)', 'css_origins' => $this->compact_asset_origins((array) ($assetDetails['css_origins'] ?? []), 6), 'js_origins' => $this->compact_asset_origins((array) ($assetDetails['js_origins'] ?? []), 4), ]; } if ($blocking >= 5) { $blocking_js[] = ['url' => $url, 'blocking_js_in_head' => $blocking]; } } // v2.0.1: dynamic severity for external-CSS bloat. On a fully-tuned // stack (WP Rocket RUCSS active, HTTP/3 server, optimized origin), // 15-25 small CSS files load in parallel via multiplexing without // measurable LCP impact, and "Combine CSS" can actively HURT performance // (one big render-blocking file vs many parallel small ones). Only // raise to high severity when the count is genuinely excessive AND no // optimizer is mitigating it. Otherwise downgrade to low/info so the // finding doesn't recommend a fix that might regress real-world CWV. // v2.8.1: hoist $wp_ctx out of the CSS-only branch. Previously this // was assigned only when $too_many_external_css was non-empty, but // the JS branch below at line ~504 also uses $wp_ctx — sites with no // CSS findings but plenty of JS findings hit a TypeError on // is_delay_js_active(null). Triggered on wsend.pro run #9. $wp_ctx = (array) $ctx->get('wp', []); $http3_active = $this->is_http3_or_h2_active($home); if (!empty($too_many_external_css)) { $rucss_active = $this->is_rucss_active($wp_ctx); // Worst-case external CSS count across sampled pages. $worst_count = 0; foreach ($too_many_external_css as $row) { $worst_count = max($worst_count, (int) ($row['external_css_count'] ?? 0)); } $css_ladder = ['high' => -7, 'med' => -4, 'low' => -2, 'info' => -1]; if ($worst_count >= 40) { $sev = 'high'; } elseif ($worst_count >= 25) { $sev = 'med'; } elseif ($worst_count >= 15) { $sev = 'low'; } else { $sev = 'info'; } // Mitigation downgrade: RUCSS already inlines used-CSS, so the // remaining external requests are smaller deferred non-render- // blocking sheets — drop one rung. HTTP/3 multiplexing removes the // round-trip cost — drop another. Floor at info. $sev_order = ['info', 'low', 'med', 'high']; $idx = (int) array_search($sev, $sev_order, true); if ($rucss_active && $idx > 0) { $idx--; } if ($http3_active && $idx > 0) { $idx--; } $sev = $sev_order[$idx]; $impact = $css_ladder[$sev]; $mitigation_notes = []; if ($rucss_active) { $mitigation_notes[] = 'WP Rocket RUCSS detected — external sheets are deferred'; } if ($http3_active) { $mitigation_notes[] = 'HTTP/2/3 multiplexing detected — parallel-load mitigates round-trips'; } $findings[] = $this->finding( 'perf_many_external_css_requests', I18n::tr('Many external CSS files', 'عدد كبير من ملفات CSS الخارجية'), 'Performance', $sev, $impact, [ 'source' => 'crawl', 'details' => sprintf( 'worst page = %d external stylesheet link tags. %s', $worst_count, empty($mitigation_notes) ? 'No mitigation detected.' : 'Mitigation: ' . implode('; ', $mitigation_notes) ), 'samples' => array_slice($too_many_external_css, 0, 10), 'css_origin_summary' => $this->aggregate_asset_origins($too_many_external_css, 'css_origins', 8), 'thresholds' => ['low_min'=>15, 'med_min'=>25, 'high_min'=>40], 'rucss_active' => $rucss_active, 'http3_active' => $http3_active, ], I18n::tr('Many CSS files mean round-trips, but the real impact depends on the stack: RUCSS + HTTP/3 reduce it greatly. Rely on CrUX/PSI field data before any Combine CSS change.', 'كثرة ملفات التنسيق (CSS) تعني طلبات أكثر للمتصفح، لكن أثرها الفعلي يعتمد على إعدادات الموقع. تحقق أولًا من بيانات السرعة الحقيقية لزوار موقعك قبل دمج الملفات.'), [ I18n::tr('If CrUX FAILED on LCP, consider Combine CSS.', 'إذا كانت سرعة ظهور أكبر عنصر في الصفحة (LCP) ضعيفة لدى الزوار ففكّر في دمج ملفات التنسيق.'), I18n::tr('If CrUX PASSED, many-external-CSS is not a real problem — leave the setting as is.', 'إذا كانت بيانات السرعة الحقيقية جيدة فكثرة ملفات التنسيق ليست مشكلة فعلية، واترك الإعداد كما هو.'), I18n::tr('Combine CSS on an RUCSS-active site can cause a regression — test before applying.', 'دمج ملفات التنسيق مع ميزة إزالة التنسيق غير المستخدم قد يبطئ الموقع، فاختبر قبل التطبيق.'), ], [ 'how' => I18n::tr('Check performance with an external tool of your choice; if real-user data PASSes, ignore the finding.', 'افحص السرعة بأداة خارجية من اختيارك، وإذا كانت بيانات الزوار الحقيقية جيدة فتجاهل هذه النتيجة.'), 'pass_condition' => I18n::tr('Fewer than 15 CSS files per page, or real-visitor speed data is good.', 'أقل من 15 ملف تنسيق في الصفحة، أو بيانات السرعة الحقيقية للزوار جيدة.'), ], $this->id() ); } // Excessive-inline-CSS finding. Dynamic severity ladder + a "used-CSS // bundle" detector that recognises WP Rocket RUCSS / Autoptimize CCSS / // theme critical-CSS — a single dominant inline block is *the* render- // blocking-CSS solution, not the problem, so it gets one severity step // off the ladder. if (!empty($excessive_inline_css)) { $worst = $excessive_inline_css[0]; foreach ($excessive_inline_css as $row) { if ((int) ($row['inline_css_bytes'] ?? 0) > (int) ($worst['inline_css_bytes'] ?? 0)) { $worst = $row; } } $worst_kb = (int) ($worst['inline_css_kb'] ?? 0); $largest_block_kb = (int) ($worst['largest_inline_block_kb'] ?? 0); $is_used_css_bundle = ($worst_kb > 0 && $largest_block_kb >= 50 && ($largest_block_kb / max(1, $worst_kb)) >= 0.75); $ladder = ['high' => -7, 'med' => -4, 'low' => -2, 'info' => -1]; $sev_order = ['info', 'low', 'med', 'high']; if ($worst_kb >= 350) { $sev = 'high'; } elseif ($worst_kb >= 220) { $sev = 'med'; } elseif ($worst_kb >= 120) { $sev = 'low'; } else { $sev = 'info'; } // Step the bundle pattern down one rung — but never below info, // since info already reflects "this is fine" and zero-impact // findings only add noise. if ($is_used_css_bundle) { $idx = array_search($sev, $sev_order, true); if ($idx > 0) { $sev = $sev_order[$idx - 1]; } } $impact = $ladder[$sev]; $findings[] = $this->finding( 'perf_excessive_inline_css', sprintf(I18n::tr('High inline CSS size on %d pages (warning — check CrUX before changing)', 'تنسيق كبير مكتوب داخل الصفحات (%d) تحقق من بيانات السرعة قبل التعديل'), count($excessive_inline_css)), 'Performance', $sev, $impact, [ 'source' => 'crawl', 'details' => sprintf( I18n::tr('The worst page has %1$d KB of inline CSS (inline style blocks: %2$d, largest: %3$d KB). Inline CSS is not a request, so this size may be genuine "used CSS" from Elementor — not necessarily a problem.%4$s', 'في أكبر صفحة تنسيق مكتوب داخلها حجمه %1$d كيلوبايت (عدد الكتل: %2$d، وأكبرها %3$d كيلوبايت). هذا التنسيق لا يضيف طلبًا جديدًا، وقد يكون تنسيقًا مستخدمًا فعلًا من أداة بناء الصفحات، فليس بالضرورة مشكلة.%4$s'), $worst_kb, (int) ($worst['inline_css_count'] ?? 0), $largest_block_kb, $is_used_css_bundle ? I18n::tr(' The pattern here (a single block >= 75% of total inline) indicates a used-CSS bundle (WP Rocket RUCSS / Autoptimize CCSS), which is the solution, not the problem — severity lowered one level.', ' كتلة واحدة تمثل 75% أو أكثر من التنسيق الداخلي تعني غالبًا أن إضافة الأداء تجمع التنسيق المستخدم فقط، وهذا حل لا مشكلة، لذلك خُفّضت درجة الأهمية.') : '' ), 'samples' => array_slice($excessive_inline_css, 0, 10), 'thresholds' => [ 'info_max_kb' => 119, 'low_min_kb' => 120, 'med_min_kb' => 220, 'high_min_kb' => 350, ], 'used_css_bundle_detected' => $is_used_css_bundle, 'severity_basis' => sprintf( 'worst page = %d KB, largest block = %d KB (%s pattern) → %s', $worst_kb, $largest_block_kb, $is_used_css_bundle ? 'used-CSS bundle' : 'fragmented', $sev ), ], I18n::tr('Large inline CSS can hurt LCP, but only if your site\'s CrUX FAILED. If Core Web Vitals PASSED in PSI, this is usually genuine "used CSS" from Elementor and safe — changing it can break the layout for no gain. Confirm before changing.', 'قد يؤخر التنسيق الداخلي الكبير ظهور المحتوى، لكن ذلك يهم فقط إذا كانت بيانات السرعة الحقيقية لموقعك ضعيفة. إذا كانت جيدة فهذا تنسيق مستخدم فعلًا، وتعديله قد يكسر شكل الصفحة بلا فائدة.'), [ I18n::tr('1) Run an external performance tool of your choice on the slowest page and review real-user data if available.', '1) شغّل أداة قياس أداء خارجية من اختيارك على أبطأ صفحة وراجع بيانات المستخدمين الحقيقية إن وُجدت.'), I18n::tr('2) If PASSED → leave the setting as is; the site is doing fine and this inline CSS is "used".', '2) إذا كانت النتيجة جيدة فاترك الإعداد كما هو، فالتنسيق مستخدم فعلًا والموقع يعمل جيدًا.'), I18n::tr('3) If FAILED or the page size > 200 KB: in WP Rocket → File Optimization → CSS Files → enable "Remove Unused CSS" (not Async CSS).', '3) إذا كانت النتيجة ضعيفة أو الصفحة أكبر من 200 كيلوبايت ففعّل ميزة إزالة التنسيق غير المستخدم (Remove Unused CSS) في إضافة الأداء لديك.'), I18n::tr('4) If RUCSS is enabled and the size is still large: WP Rocket → "Clear Used CSS" → wait for regeneration, then measure again.', '4) إذا كانت الميزة مفعلة والحجم ما زال كبيرًا فامسح التنسيق المحفوظ من إضافة الأداء، وانتظر إعادة إنشائه ثم قس مرة أخرى.'), I18n::tr('5) In Elementor → Settings → Performance → enable "Improved CSS Loading".', '5) إذا كنت تستخدم أداة بناء صفحات ففعّل خيار تحميل التنسيق المحسّن في إعدادات الأداء الخاصة بها.'), I18n::tr('6) Use Perfmatters → Script Manager to disable widgets not used on the page.', '6) عطّل مكوّنات أداة البناء غير المستخدمة في الصفحة إن كانت أداتك تتيح ذلك.'), ], [ 'how' => I18n::tr('Run a new audit and compare real-visitor speed before and after.', 'شغّل فحصًا جديدًا، وقارن بيانات السرعة الحقيقية قبل التعديل وبعده.'), 'pass_condition' => I18n::tr('Real-visitor speed data is good, or inline CSS is under 80 KB.', 'بيانات السرعة الحقيقية جيدة، أو التنسيق الداخلي أقل من 80 كيلوبايت.'), ], $this->id() ); } // v1.0.65: NEW — duplicate asset detection (same base URL loaded 2+ // times, ignoring ?ver= cache busters). ده render-blocker حقيقي، // مثلاً Custom Fonts plugin بيـ enqueue نفس الـ local-fonts.css مرتين. if (!empty($duplicate_assets_pages)) { $all_dup_urls = []; foreach ($duplicate_assets_pages as $row) { foreach ((array) ($row['duplicates'] ?? []) as $d) { $u = (string) ($d['url'] ?? ''); if ($u === '') { continue; } if (!isset($all_dup_urls[$u])) { $all_dup_urls[$u] = ['url' => $u, 'type' => (string) ($d['type'] ?? ''), 'pages' => 0, 'max_count' => 0]; } $all_dup_urls[$u]['pages']++; $all_dup_urls[$u]['max_count'] = max((int) $all_dup_urls[$u]['max_count'], (int) ($d['count'] ?? 0)); } } $top = array_values($all_dup_urls); usort($top, static function ($a, $b) { return (int) $b['pages'] <=> (int) $a['pages']; }); $findings[] = $this->finding( 'perf_duplicate_assets', I18n::tr('The same file (CSS/JS) loads more than once on the same page', 'الملف نفسه (CSS/JS) يُحمَّل أكثر من مرة في الصفحة نفسها'), 'Performance', 'high', -6, [ 'source' => 'crawl', 'details' => sprintf(I18n::tr('Unique files repeated across pages: %1$d (pages affected: %2$d). This is real render-blocking.', 'عدد الملفات الفريدة المتكررة: %1$d (الصفحات المتأثرة: %2$d)، وهذا حجب حقيقي للعرض.'), count($all_dup_urls), count($duplicate_assets_pages)), 'samples' => array_slice($duplicate_assets_pages, 0, 10), 'top_duplicate_urls' => array_slice($top, 0, 10), ], I18n::tr('When the same CSS or JS file is enqueued twice with a different ?ver=, the browser downloads both as render-blocking. Common cause: font plugins or third-party widgets that enqueue manually without wp_enqueue_style.', 'عندما يُحمَّل ملف CSS أو JS نفسه مرتين برقم إصدار مختلف ينزّل المتصفح النسختين معًا ويحجبان العرض. والسبب الشائع إضافات الخطوط أو الأدوات الخارجية التي تحمّل الملفات يدويًا بدل wp_enqueue_style.'), [ I18n::tr('1) Review the repeated files listed in the details, and for each one find the plugin that loads it (usually a font, icon or widget plugin).', '1) راجع قائمة الروابط المكررة وابحث لكل رابط عن الإضافة التي تحمّله وغالبًا تكون إضافة خطوط أو أيقونات أو أداة جانبية.'), I18n::tr('2) In WP Rocket → File Optimization → "Excluded CSS/JS Files" — if there are exclusions for this file, clean them up.', '2) في إعدادات إضافة الأداء احذف أي استثناء يخص هذا الملف من قائمة الملفات المستثناة.'), I18n::tr('3) If the duplicate comes from a Custom Fonts plugin (Brainstorm Force) or similar: disable the plugin temporarily and see if the fonts still work from the theme.', '3) إذا كان التكرار من إضافة خطوط مخصصة فعطّلها مؤقتًا وتأكد أن الخطوط ما زالت تعمل من القالب.'), I18n::tr('4) In Perfmatters → Script Manager → Disable the duplicate copy on the page.', '4) أوقف النسخة المكررة من الملف في هذه الصفحة من إعدادات إضافة الأداء أو مدير السكربتات.'), I18n::tr('5) If you cannot tell who enqueues it: use the Query Monitor plugin → Scripts/Styles → it shows you the exact source.', '5) إذا لم تعرف مصدر الملف فاستخدم أداة تشخيص تعرض السكربتات والتنسيقات ومصدر كل منها.'), ], [ 'how' => I18n::tr('View Source on the page + search for the file name. If it appears twice → it is still there.', 'افتح مصدر الصفحة وابحث عن اسم الملف. إذا ظهر مرتين فالمشكلة ما زالت قائمة.'), 'pass_condition' => I18n::tr('Each CSS/JS file appears only once.', 'كل ملف CSS/JS يظهر مرة واحدة فقط.'), ], $this->id() ); } // v3.3.47 — Round-2 false-positive fix. The legacy CSS rollup was // double-counting the score (med/-3) on top of the two dedicated // info-severity successors (perf_many_external_css_requests + // perf_excessive_inline_css). Removed entirely — its successors // cover the same diagnosis with finer-grained samples and the right // severity. if (!empty($too_many_js)) { // v2.1.3 — JS mitigation gating. The pre-2.1.3 logic always // emitted high/-7 even when WP Rocket Delay JS / Defer JS / // Perfmatters Script Manager were active and HTTP/2/3 was in // use. Apply the same // mitigation ladder we already use for CSS in v2.0.1. $delay_js_active = $this->is_delay_js_active($wp_ctx); $defer_js_active = $this->is_defer_js_active($wp_ctx); $script_mgr_active = $this->is_script_manager_active($wp_ctx); $js_sev = 'high'; $js_impact = -7; // Each mitigation downgrades by one severity step (high → med → low → info). $sev_ladder = ['high' => -7, 'med' => -4, 'low' => -2, 'info' => -1]; $sev_order = ['high', 'med', 'low', 'info']; $sev_idx = 0; if ($delay_js_active) { $sev_idx++; } if ($http3_active) { $sev_idx++; } if ($script_mgr_active) { $sev_idx++; } // Defer JS alone is a softer mitigation than Delay JS. if ($defer_js_active && !$delay_js_active && $sev_idx < 3) { $sev_idx++; } $sev_idx = min(count($sev_order) - 1, $sev_idx); $js_sev = $sev_order[$sev_idx]; $js_impact = $sev_ladder[$js_sev]; $mitigation_notes = array_values(array_filter([ $delay_js_active ? 'WP Rocket Delay JS active' : null, $defer_js_active ? 'Defer JS active' : null, $script_mgr_active ? 'Perfmatters Script Manager active' : null, $http3_active ? 'HTTP/2 or HTTP/3 active (multiplexed requests)' : null, ])); $title_suffix = !empty($mitigation_notes) ? ' (mitigated)' : ''; $findings[] = $this->finding( 'perf_many_js_requests', I18n::tr('Many JavaScript files on checked pages', 'عدد كبير من ملفات JavaScript في صفحات العينة') . $title_suffix, 'Performance', $js_sev, $js_impact, [ 'source' => 'crawl', 'details' => I18n::tr('Threshold: 20 JavaScript files on one page.', 'الحد: 20 ملف JavaScript في الصفحة الواحدة.'), 'samples' => array_slice($too_many_js, 0, 10), 'js_origin_summary' => $this->aggregate_asset_origins($too_many_js, 'js_origins', 8), 'mitigation_notes' => $mitigation_notes, 'mitigation_severity_steps_applied' => $sev_idx, ], I18n::tr('Many JS files delay INP and TBT. With delay/defer + a script manager + HTTP/2/3 the impact drops noticeably — but reducing them is still recommended where possible.', 'كثرة ملفات JavaScript تؤخر استجابة الصفحة للمس والنقر. تأجيل السكربتات وإيقاف غير الضروري منها يقللان الأثر كثيرًا، ومع ذلك يبقى تقليل عددها مفيدًا.') . (!empty($mitigation_notes) ? I18n::tr(' ✓ Mitigations in place: ', ' ✓ الإجراءات المخففة الموجودة: ') . implode(' / ', $mitigation_notes) . '.' : ''), [ !$delay_js_active ? I18n::tr('Enable "Delay JavaScript Execution" in WP Rocket → File Optimization → JS.', 'فعّل تأخير تشغيل JavaScript حتى أول تفاعل من الزائر في إضافة الأداء لديك.') : I18n::tr('✓ Delay JS enabled (mitigated).', '✓ تأخير تشغيل JavaScript مفعّل.'), !$defer_js_active ? I18n::tr('Enable "Defer JavaScript" in the same place (complements Delay JS).', 'فعّل تأجيل JavaScript (Defer) أيضًا، فهو يكمّل خيار التأخير.') : I18n::tr('✓ Defer JS enabled (mitigated).', '✓ تأجيل JavaScript مفعّل.'), !$script_mgr_active ? I18n::tr('Enable Perfmatters Script Manager + turn off unnecessary JS per page (e.g. the WhatsApp button only on the contact page).', 'أوقف ملفات JavaScript غير الضرورية في كل صفحة، مثل إبقاء زر واتساب في صفحة التواصل وحدها.') : I18n::tr('✓ Perfmatters Script Manager enabled (mitigated).', '✓ مدير السكربتات مفعّل.'), I18n::tr('Check the script sources in the list: any source that adds 5 or more JavaScript files needs a closer look.', 'راجع مصادر السكربتات في القائمة، فأي مصدر يضيف 5 ملفات JavaScript أو أكثر يحتاج إلى مراجعة.'), I18n::tr('Before any global Combine JS, verify in CrUX/Lighthouse field data that INP is actually affected.', 'قبل دمج كل ملفات JavaScript تأكد من بيانات السرعة الحقيقية أن استجابة الصفحة متأثرة فعلًا.'), ], [ 'how' => I18n::tr('Run a new audit after the change and compare real-visitor responsiveness.', 'شغّل فحصًا جديدًا بعد التعديل وقارن سرعة استجابة الصفحات لدى الزوار.'), 'pass_condition' => I18n::tr('Fewer than 20 JavaScript files per page, or good real-visitor responsiveness.', 'أقل من 20 ملف JavaScript في الصفحة، أو استجابة جيدة لدى الزوار الحقيقيين.'), ], $this->id() ); } if (!empty($too_many_assets)) { // v2.1.5 — same mitigation ladder as perf_many_js_requests. // High asset count is much less impactful when: // - RUCSS is on (CSS rendered inline + external deferred) // - Delay JS is on (non-critical JS held until interaction) // - HTTP/2/3 is active (multiplexed requests = no head-of-line) $rucss_active2 = $this->is_rucss_active($wp_ctx); $delay_js_active2 = $this->is_delay_js_active($wp_ctx); $http3_active2 = $this->is_http3_or_h2_active($home); $sev_ladder = ['high' => -7, 'med' => -4, 'low' => -2, 'info' => -1]; $sev_order = ['high', 'med', 'low', 'info']; $sev_idx = 1; // start at med (was hard-coded -4) if ($rucss_active2) { $sev_idx++; } if ($delay_js_active2) { $sev_idx++; } if ($http3_active2 && $sev_idx < 3) { $sev_idx++; } $sev_idx = min(count($sev_order) - 1, $sev_idx); $combo_sev = $sev_order[$sev_idx]; $combo_impact = $sev_ladder[$combo_sev]; $mitigations2 = array_values(array_filter([ $rucss_active2 ? 'WP Rocket RUCSS active' : null, $delay_js_active2 ? 'WP Rocket Delay JS active' : null, $http3_active2 ? 'HTTP/2 or HTTP/3 active' : null, ])); $title_suffix2 = !empty($mitigations2) ? ' (mitigated)' : ''; $findings[] = $this->finding( 'perf_many_css_js_requests', I18n::tr('Many CSS and JavaScript requests on the checked pages', 'عدد كبير من طلبات ملفات التنسيق وJavaScript في الصفحات المفحوصة') . $title_suffix2, 'Performance', $combo_sev, $combo_impact, [ 'source' => 'crawl', 'details' => I18n::tr('Threshold: 25 external CSS and JavaScript requests on one page. Styles written inside the page are not counted.', 'الحد: 25 طلبًا خارجيًا لملفات CSS وJavaScript في الصفحة الواحدة، ولا تُحسب الأنماط المكتوبة داخل الصفحة.'), 'samples' => array_slice($too_many_assets, 0, 10), 'css_origin_summary' => $this->aggregate_asset_origins($too_many_assets, 'css_origins', 8), 'js_origin_summary' => $this->aggregate_asset_origins($too_many_assets, 'js_origins', 8), 'mitigation_notes' => $mitigations2, ], I18n::tr('Many external HTTP requests delay LCP/INP — but with RUCSS + Delay JS + HTTP/2/3 the impact drops noticeably.', 'كثرة الطلبات الخارجية تؤخر ظهور الصفحة واستجابتها، ويقل الأثر كثيرًا مع إزالة التنسيق غير المستخدم وتأخير السكربتات.') . (!empty($mitigations2) ? I18n::tr(' Already in place: ', ' إجراءات مخففة موجودة: ') . implode(' / ', $mitigations2) . '.' : ''), [ I18n::tr('Check the more specific CSS and JavaScript findings first.', 'راجع أولًا النتائج الأدق الخاصة بملفات التنسيق وملفات JavaScript.'), I18n::tr('Avoid global Combine CSS/JS with RUCSS active (it can break).', 'تجنب دمج كل ملفات التنسيق والسكربتات مع ميزة إزالة التنسيق غير المستخدم، لأن ذلك قد يكسر الصفحات.'), I18n::tr('If CrUX/PSI field data says LCP < 2.5s, ignore this finding.', 'إذا كان أكبر عنصر في الصفحة يظهر لدى الزوار خلال أقل من 2.5 ثانية فتجاهل هذه النتيجة.'), ], [ 'how' => I18n::tr('Run a new audit and compare real-visitor load speed.', 'شغّل فحصًا جديدًا وقارن سرعة التحميل لدى الزوار.'), 'pass_condition' => I18n::tr('Fewer than 25 requests per page, or the main content shows in under 2.5 seconds for visitors.', 'أقل من 25 طلبًا في الصفحة، أو يظهر المحتوى الرئيسي للزوار في أقل من 2.5 ثانية.'), ], $this->id() ); } if (!empty($blocking_js)) { $findings[] = $this->finding( 'perf_render_blocking_js', I18n::tr('Scripts that block the page in the head of the checked pages', 'سكربتات توقف عرض الصفحة في رأس الصفحات المفحوصة'), 'Performance', 'med', -5, [ 'source' => 'html', 'details' => I18n::tr('Some checked pages load 5 or more render-blocking scripts in the head.', 'بعض الصفحات تحمّل في رأسها 5 سكربتات فأكثر تؤخر ظهور الصفحة.'), 'samples' => array_slice($blocking_js, 0, 10), ], I18n::tr('Blocking JS in the head can delay first paint and increase time to interactive.', 'السكربت الذي يُحمَّل في رأس الصفحة دون تأجيل يؤخر ظهور المحتوى ويؤخر التفاعل.'), [ I18n::tr('Add defer/async to non-critical scripts.', 'أضف defer/async للسكربتات غير الحرجة.'), I18n::tr('Reduce third-party scripts as much as possible.', 'قلّل سكربتات الطرف الثالث قدر الإمكان.'), ], [ 'how' => I18n::tr('Run a new audit, then open this finding again.', 'شغّل فحصًا جديدًا ثم افتح هذه النتيجة مرة أخرى.'), 'pass_condition' => I18n::tr('Most checked pages load fewer than 5 blocking scripts in the head.', 'blocking_js_in_head < 5 لمعظم صفحات العينة.'), ], $this->id() ); } if (!$lazy_found && count($pages) > 0) { $findings[] = $this->finding( 'perf_lazyload_missing', I18n::tr('No loading="lazy" detected on images of the checked pages (estimate)', 'لم يظهر التحميل المؤجل للصور في الصفحات المفحوصة (تقدير)'), 'Performance', 'low', -3, [ 'source' => 'html', 'details' => I18n::tr('No loading="lazy" detected on the checked pages', 'لم يظهر التحميل المؤجل للصور في الصفحات المفحوصة'), 'samples' => array_map(static function ($p) { return ['url' => (string) ($p['url'] ?? '')]; }, array_slice($pages, 0, 5)), ], I18n::tr('Lazy-loading helps reduce the initial load, especially for long pages.', 'تأجيل تحميل الصور حتى يقترب منها الزائر يقلل التحميل الأول، خاصة في الصفحات الطويلة.'), [ I18n::tr('Enable lazy-load from your image/performance plugin or via theme settings.', 'فعّل التحميل المؤجل للصور من إضافة الصور أو الأداء أو من إعدادات القالب.'), I18n::tr('Make sure below-the-fold images use loading="lazy".', 'تأكد أن الصور التي لا تظهر في أول الشاشة تستخدم التحميل المؤجل (loading="lazy").'), ], [ 'how' => I18n::tr('Run a new audit, then open this finding again.', 'شغّل فحصًا جديدًا ثم افتح هذه النتيجة مرة أخرى.'), 'pass_condition' => I18n::tr('At least one checked page uses lazy loading for images.', 'صفحة واحدة على الأقل من الصفحات المفحوصة تستخدم التحميل المؤجل للصور.'), ], $this->id() ); } if ($img_total >= 10) { $ratio = $img_total > 0 ? ($webp_total / $img_total) : 0; if ($ratio < 0.30) { $findings[] = $this->finding( 'perf_low_webp_coverage', I18n::tr('Few images use the modern WebP format (estimate)', 'قليل من الصور بصيغة WebP الحديثة (تقدير)'), 'Performance', 'low', -3, [ 'source' => 'crawl', 'details' => sprintf(I18n::tr('WebP images: %1$d of %2$d (%3$s%%).', 'الصور بصيغة WebP: %1$d من %2$d (%3$s%%).'), (int) $webp_total, (int) $img_total, (string) round($ratio * 100, 1)), 'samples' => [ 'images_total' => $img_total, 'webp_images' => $webp_total, 'ratio' => round($ratio, 3), ], ], I18n::tr('Using WebP/AVIF reduces image size and improves LCP.', 'صيغتا WebP وAVIF تقللان حجم الصور فتظهر الصفحة أسرع.'), [ I18n::tr('Enable image conversion to WebP/AVIF via an image plugin or CDN.', 'فعّل تحويل الصور إلى WebP/AVIF عبر إضافة صور أو CDN.'), I18n::tr('Make sure srcset is served in modern formats for supporting browsers.', 'تأكد أن الصور تُقدَّم بصيغ حديثة للمتصفحات التي تدعمها.'), ], [ 'how' => I18n::tr('Run a new audit, then open this finding again.', 'شغّل فحصًا جديدًا ثم افتح هذه النتيجة مرة أخرى.'), 'pass_condition' => I18n::tr('At least 30% of images use WebP.', '30% على الأقل من الصور بصيغة WebP.'), ], $this->id() ); } } // Autoloaded options size. $db = (array) $ctx->get('db', []); $autoload_bytes = (int) ($db['autoload_bytes'] ?? 0); if ($autoload_bytes >= 1024 * 1024) { $top = isset($db['top_autoload_options']) && is_array($db['top_autoload_options']) ? $db['top_autoload_options'] : []; $findings[] = $this->finding( 'db_autoload_options_bloat', I18n::tr('Large settings data loaded on every page (autoloaded options)', 'بيانات إعدادات كبيرة تُحمَّل مع كل صفحة (autoload)'), 'Database', 'high', -8, [ 'source' => 'db', 'details' => 'SUM(LENGTH(option_value)) WHERE autoload=yes = ' . $autoload_bytes . I18n::tr(' bytes', ' بايت'), 'samples' => array_slice($top, 0, 10), ], I18n::tr('Autoloaded options load on every request and can increase TTFB and consume memory.', 'هذه الإعدادات تُقرأ من قاعدة البيانات مع كل زيارة، وحجمها الكبير يبطئ استجابة الخادم ويستهلك الذاكرة.'), [ I18n::tr('Review the largest options (by name and size) and identify their source (plugin/theme).', 'راجع أكبر الإعدادات بالاسم والحجم، واعرف الإضافة أو القالب الذي أنشأها.'), I18n::tr('Clean up old transients or unused options (manually/via a trusted plugin).', 'احذف البيانات المؤقتة القديمة والإعدادات غير المستخدمة يدويًا أو بإضافة تنظيف موثوقة.'), I18n::tr('Avoid storing huge data in autoload.', 'تجنب تخزين بيانات ضخمة في autoload.'), ], [ 'how' => I18n::tr('Run a new audit, then open this finding again.', 'شغّل فحصًا جديدًا ثم افتح هذه النتيجة مرة أخرى.'), 'pass_condition' => 'autoload_bytes < 1MB (Heuristic).', ], $this->id() ); } // Cron heuristics. $cron_events = (int) ($db['cron_events_count'] ?? 0); if ($cron_events > 200) { $findings[] = $this->finding( 'perf_cron_overload', I18n::tr('Many WP-Cron events (estimate)', 'عدد كبير من المهام المجدولة في ووردبريس (تقدير)'), 'Performance', 'low', -2, [ 'source' => 'db', 'details' => sprintf(I18n::tr('Scheduled tasks registered (WP-Cron events): %d.', 'عدد المهام المجدولة المسجلة (WP-Cron): %d.'), (int) $cron_events), ], I18n::tr('Many cron events may mean many tasks running automatically and affecting performance.', 'كثرة المهام المجدولة تعني عمليات كثيرة تعمل تلقائيًا في الخلفية وقد تبطئ الموقع.'), [ I18n::tr('Review the plugins that add many cron tasks.', 'راجع الإضافات التي تضيف مهام مجدولة كثيرة.'), I18n::tr('If the site is high-traffic: use a real cron job and disable WP cron (carefully).', 'إذا كانت زيارات الموقع كثيرة فاطلب من الاستضافة تشغيل المهام المجدولة من الخادم بدلًا من ووردبريس، بحذر.'), ], [ 'how' => I18n::tr('Run a new audit, then open this finding again.', 'شغّل فحصًا جديدًا ثم افتح هذه النتيجة مرة أخرى.'), 'pass_condition' => 'cron_events_count <= 200 (Heuristic).', ], $this->id() ); } // Conflict matrix: multiple cache/optimizer plugins can overlap and hurt stability/performance. $wp = (array) $ctx->get('wp', []); $plugins = isset($wp['plugins']) && is_array($wp['plugins']) ? $wp['plugins'] : []; $theme = isset($wp['theme']) && is_array($wp['theme']) ? $wp['theme'] : []; $adapters = (array) $ctx->get('adapters', []); $active_files = []; $active_meta = []; foreach ($plugins as $pl) { if (!is_array($pl)) { continue; } $file = strtolower((string) ($pl['file'] ?? '')); if ($file === '') { continue; } $active_files[$file] = true; $active_meta[$file] = [ 'plugin_file' => (string) ($pl['file'] ?? ''), 'name' => (string) ($pl['name'] ?? $file), 'version' => (string) ($pl['version'] ?? ''), ]; } $page_cache_candidates = [ 'wp-rocket/wp-rocket.php' => 'WP Rocket', 'sg-cachepress/sg-cachepress.php' => 'Speed Optimizer', 'litespeed-cache/litespeed-cache.php' => 'LiteSpeed Cache', 'w3-total-cache/w3-total-cache.php' => 'W3 Total Cache', 'wp-super-cache/wp-cache.php' => 'WP Super Cache', 'wp-fastest-cache/wpFastestCache.php' => 'WP Fastest Cache', 'breeze/breeze.php' => 'Breeze', ]; $asset_optimizer_candidates = [ 'wp-rocket/wp-rocket.php' => 'WP Rocket', 'perfmatters/perfmatters.php' => 'Perfmatters', 'autoptimize/autoptimize.php' => 'Autoptimize', 'sg-cachepress/sg-cachepress.php' => 'Speed Optimizer', 'wp-asset-clean-up/wpacu.php' => 'Asset CleanUp', 'swift-performance-lite/swift-performance-lite.php' => 'Swift Performance', ]; $active_page_cache = []; foreach ($page_cache_candidates as $file => $name) { if (isset($active_files[strtolower($file)])) { $active_page_cache[] = ['name' => $name, 'plugin_file' => $file]; } } $active_asset_optimizers = []; foreach ($asset_optimizer_candidates as $file => $name) { if (isset($active_files[strtolower($file)])) { $active_asset_optimizers[] = ['name' => $name, 'plugin_file' => $file]; } } if (count($active_page_cache) >= 2) { $findings[] = $this->finding( 'perf_multiple_page_cache_plugins', I18n::tr('More than one page-cache engine running together (high conflict risk)', 'أكثر من إضافة لتخزين الصفحات تعمل معًا (خطر تعارض مرتفع)'), 'Performance', 'high', -10, [ 'source' => 'options', 'details' => I18n::tr('More than one page-cache plugin detected.', 'رُصدت أكثر من إضافة لتخزين الصفحات مؤقتًا.'), // Plugin-level finding (not URL-based): set the count explicitly // so the UI shows "N affected" instead of a misleading 0. 'affected_count' => count($active_page_cache), 'samples' => [ 'active_page_cache_plugins' => $active_page_cache, ], ], I18n::tr('Running more than one page-cache engine together can duplicate rules, corrupt cache logic, and cause unstable performance.', 'تشغيل أكثر من إضافة لتخزين الصفحات معًا قد يكرر القواعد ويخلط النسخ المحفوظة ويجعل سرعة الموقع غير مستقرة.'), [ I18n::tr('Keep only one page-cache engine running.', 'أبقِ محرك تخزين صفحات واحدًا فقط يعمل.'), I18n::tr('Disable the overlapping page-cache features in the other plugin(s).', 'عطّل تخزين الصفحات في الإضافات الأخرى واترك إضافة واحدة مسؤولة عنه.'), I18n::tr('Clear all caches after the change and re-test the same pages.', 'امسح كل الملفات المخزنة مؤقتًا بعد التعديل، وأعد اختبار الصفحات نفسها.'), ], [ 'how' => I18n::tr('Run a new audit and confirm this finding no longer appears.', 'شغّل فحصًا جديدًا وتأكد أن هذه النتيجة لم تعد تظهر.'), 'pass_condition' => I18n::tr('Only one page-cache plugin remains active and no cache-overlap finding is open.', 'بقيت إضافة تخزين صفحات واحدة تعمل ولا توجد ملاحظة تداخل كاش مفتوحة.'), ], $this->id() ); } // v1.0.65: WP Rocket plugin-conflict warnings, from a LOCAL matrix of // known conflicts only. 2026-06-12 wp.org review: we previously also // read WP Rocket's own compatibility filter, but calling a foreign // (non-Aetos-prefixed) hook was flagged, so detection is now fully // local. كل tuple من (file, reason) → finding. $wp_rocket_active = isset($active_files['wp-rocket/wp-rocket.php']); if ($wp_rocket_active) { $known_conflicts = [ 'custom-fonts/custom-fonts.php' => I18n::tr('Custom Fonts (Brainstorm Force) can interfere with Remove Unused CSS and caching through its local-fonts.css delivery.', 'قد تتعارض إضافة Custom Fonts مع إزالة التنسيق غير المستخدم ومع تخزين الصفحات بسبب طريقة تحميل ملف الخطوط.' ), 'bsf-custom-fonts/bsf-custom-fonts.php' => I18n::tr('Custom Fonts (Brainstorm Force, legacy slug) can interfere with Remove Unused CSS and caching.', 'قد تتعارض إضافة Custom Fonts (الإصدار القديم) مع إزالة التنسيق غير المستخدم ومع تخزين الصفحات.' ), 'autoptimize/autoptimize.php' => I18n::tr('Autoptimize can overlap with WP Rocket CSS and JavaScript minification or combination.', 'قد تتعارض ميزات تصغير ودمج الملفات في Autoptimize مع WP Rocket.' ), 'wp-super-cache/wp-cache.php' => I18n::tr('WP Super Cache adds a second page-cache engine beside WP Rocket.', 'تضيف WP Super Cache تخزينًا ثانيًا للصفحات بجانب WP Rocket.' ), 'w3-total-cache/w3-total-cache.php' => I18n::tr('W3 Total Cache adds overlapping page-cache behavior beside WP Rocket.', 'تضيف W3 Total Cache تخزينًا للصفحات يتداخل مع WP Rocket.' ), 'wp-fastest-cache/wpFastestCache.php' => I18n::tr('WP Fastest Cache adds a second page-cache engine beside WP Rocket.', 'تضيف WP Fastest Cache تخزينًا ثانيًا للصفحات بجانب WP Rocket.' ), 'breeze/breeze.php' => I18n::tr('Breeze can duplicate page-cache behavior already managed by WP Rocket.', 'قد تكرر Breeze تخزين الصفحات الذي يتولاه WP Rocket.' ), 'litespeed-cache/litespeed-cache.php' => I18n::tr('LiteSpeed Cache can duplicate page-cache behavior already managed by WP Rocket.', 'قد تكرر LiteSpeed Cache تخزين الصفحات الذي يتولاه WP Rocket.' ), 'sg-cachepress/sg-cachepress.php' => I18n::tr('Disable overlapping frontend caching in SiteGround Optimizer when WP Rocket owns page caching.', 'عطّل تخزين الصفحات في SiteGround Optimizer عندما يتولاه WP Rocket.' ), 'hummingbird-performance/wp-hummingbird.php' => I18n::tr('Hummingbird minification or caching can overlap with WP Rocket.', 'قد تتعارض ميزات التصغير أو التخزين في Hummingbird مع WP Rocket.' ), 'cache-enabler/cache-enabler.php' => I18n::tr('Cache Enabler adds a second page-cache engine beside WP Rocket.', 'تضيف Cache Enabler تخزينًا ثانيًا للصفحات بجانب WP Rocket.' ), ]; $detected_conflicts = []; foreach ($known_conflicts as $file => $reason) { if (isset($active_files[strtolower($file)])) { $detected_conflicts[] = ['plugin_file' => $file, 'reason' => $reason]; } } if (!empty($detected_conflicts)) { $findings[] = $this->finding( 'perf_wp_rocket_plugin_conflicts', sprintf(I18n::tr('Plugins that really conflict with WP Rocket (%d)', 'إضافات تتعارض فعليًا مع WP Rocket (%d)'), count($detected_conflicts)), 'Performance', 'high', -8, [ 'source' => 'options', 'details' => I18n::tr('WP Rocket has a known list of plugins that break its optimization features.', 'لدى WP Rocket قائمة معروفة بالإضافات التي تعطّل ميزاته.'), // Plugin-level finding: count the conflicting plugins so the // UI shows the real number instead of 0. 'affected_count' => count($detected_conflicts), 'samples' => $detected_conflicts, ], I18n::tr('These plugins silently break WP Rocket features (RUCSS, Combine CSS, Cache) — meaning WP Rocket is running but the real result is not what its settings show. This is the common cause of "WP Rocket is enabled but the site is still slow".', 'هذه الإضافات تعطّل ميزات WP Rocket مثل إزالة التنسيق غير المستخدم والدمج والتخزين دون أي تنبيه. لذلك يبدو WP Rocket مفعّلًا بينما النتيجة الفعلية مختلفة، وهذا سبب شائع لبقاء الموقع بطيئًا.'), [ I18n::tr('1) For each plugin on the checked pages → open the plugin page and see if there is an alternative or if you can disable it.', '1) لكل إضافة في العينات افتح صفحتها وابحث عن بديل أو عن إمكانية تعطيلها.'), I18n::tr('2) Custom Fonts (Brainstorm Force) especially: the Astra/Elementor theme usually offers built-in alternatives — try disabling it and see if the fonts still work.', '2) إضافة Custom Fonts تحديدًا لها غالبًا بديل مدمج في القالب أو أداة البناء. جرّب تعطيلها وتأكد أن الخطوط ما زالت تعمل.'), I18n::tr('3) Other page-cache plugins: remove them and keep WP Rocket alone.', '3) إضافات تخزين الصفحات الأخرى: احذفها واترك WP Rocket وحده.'), I18n::tr('4) Autoptimize: turn it off, WP Rocket already covers its features.', '4) Autoptimize: عطّلها، فميزاتها موجودة في WP Rocket.'), I18n::tr('5) After turning them off, clear WP Rocket\'s cache and used CSS, then run a new audit.', '5) بعد التعطيل امسح الملفات المحفوظة في WP Rocket، ثم شغّل فحصًا جديدًا.'), ], [ 'how' => I18n::tr('Run a new audit after turning them off and check the related speed findings.', 'شغّل فحصًا جديدًا بعد التعطيل وراجع نتائج السرعة المرتبطة.'), 'pass_condition' => I18n::tr('This list becomes empty and the repeated files disappear.', 'تصبح هذه القائمة فارغة وتقل الملفات المكررة.'), ], $this->id() ); } } // v1.0.65: RUCSS-aware overlap. لو واحد فقط من الـ optimizers هو اللي // فعّل "Remove Unused CSS"، ده عادي ومش مشكلة → info. لو الاتنين فعّلوه // → conflict حقيقي high. لو ولا واحد → med (RUCSS مش شغّال خالص). $rocket_ctx = isset($adapters['wp_rocket']['context']['wp_rocket_settings']) && is_array($adapters['wp_rocket']['context']['wp_rocket_settings']) ? $adapters['wp_rocket']['context']['wp_rocket_settings'] : []; $perf_ctx = isset($adapters['perfmatters']['context']['perfmatters_options']) && is_array($adapters['perfmatters']['context']['perfmatters_options']) ? $adapters['perfmatters']['context']['perfmatters_options'] : []; $rocket_rucss = !empty($rocket_ctx['remove_unused_css']); $perf_rucss = !empty($perf_ctx['remove_unused_css']) || !empty($perf_ctx['unused_css']); $rucss_owners = []; if ($rocket_rucss) { $rucss_owners[] = 'WP Rocket'; } if ($perf_rucss) { $rucss_owners[] = 'Perfmatters'; } if (count($active_asset_optimizers) >= 2) { if (count($rucss_owners) >= 2) { $overlap_sev = 'high'; $overlap_impact = -8; $overlap_note = sprintf( I18n::tr('Both %s have Remove Unused CSS enabled; their RUCSS layers can conflict.', 'كل من %s فعّل ميزة إزالة التنسيق غير المستخدم، وقد تتعارض الميزتان.' ), implode(' + ', $rucss_owners) ); } elseif (count($rucss_owners) === 1) { $overlap_sev = 'info'; $overlap_impact = 0; $overlap_note = sprintf( I18n::tr('%s owns RUCSS; the other optimizer handles caching or script management, which is a valid split.', '%s يتولى إزالة التنسيق غير المستخدم، والإضافة الأخرى تتولى التخزين أو السكربتات، وهذا توزيع صحيح.' ), $rucss_owners[0] ); } else { $overlap_sev = 'med'; $overlap_impact = -4; $overlap_note = I18n::tr('Neither optimizer has Remove Unused CSS enabled; this is a missed optimization opportunity.', 'لم تُفعَّل ميزة إزالة التنسيق غير المستخدم في أي من الإضافتين، وهذه فرصة تحسين غير مستغلة.' ); } $findings[] = $this->finding( 'perf_multiple_asset_optimizers', I18n::tr('More than one CSS/JS optimization plugin detected (', 'رُصدت أكثر من إضافة لتحسين CSS/JS (') . ($overlap_sev === 'info' ? I18n::tr('not a conflict', 'لا يوجد تعارض') : I18n::tr('verify', 'تحقق')) . ')', 'Performance', $overlap_sev, $overlap_impact, [ 'source' => 'options', 'details' => $overlap_note, 'samples' => [ 'active_asset_optimizers' => $active_asset_optimizers, 'rucss_owners' => $rucss_owners, 'wp_rocket_remove_unused_css' => $rocket_rucss, 'perfmatters_remove_unused_css' => $perf_rucss, ], ], I18n::tr('If two optimizers manage the same feature (RUCSS/minify/defer), the results conflict. But if each manages something different, that is normal.', 'إذا تولت إضافتان الميزة نفسها، مثل إزالة التنسيق غير المستخدم أو التصغير أو التأجيل، فستتعارض النتائج. أما إذا تولت كل إضافة شيئًا مختلفًا فهذا وضع طبيعي.'), [ I18n::tr('Choose one plugin as the primary CSS/JS optimizer.', 'اختر إضافة واحدة لتكون المحسّن الأساسي لـ CSS/JS.'), I18n::tr('In the other plugins, turn off the overlapping minify/defer/delay features.', 'في الإضافات التانية، اقفل ميزات minify/defer/delay المتداخلة.'), I18n::tr('Keep per-page asset unloading in one place only (like Script Manager), not duplicated.', 'اجعل إلغاء تحميل الأصول لكل صفحة في مكان واحد فقط مثل مدير السكربتات بلا تكرار.'), ], [ 'how' => I18n::tr('Run a new audit and compare the CSS and JavaScript counts on the same pages.', 'شغّل فحصًا جديدًا وقارن عدد ملفات التنسيق وJavaScript في الصفحات نفسها.'), 'pass_condition' => I18n::tr('The overlap finding is closed and most checked pages trend toward lower CSS/JS counts.', 'أُغلقت ملاحظة التداخل وتتجه أغلب صفحات العينة إلى أعداد أقل من CSS/JS.'), ], $this->id() ); } $builder_hits = []; foreach ($plugins as $pl) { if (!is_array($pl)) { continue; } $file = strtolower((string) ($pl['file'] ?? '')); if (strpos($file, 'elementor') !== false || strpos($file, 'js_composer') !== false || strpos($file, 'wpbakery') !== false || strpos($file, 'divi') !== false || strpos($file, 'fl-builder') !== false || strpos($file, 'oxygen') !== false) { $builder_hits[] = ['plugin_file' => (string) ($pl['file'] ?? ''), 'version' => (string) ($pl['version'] ?? '')]; } } $theme_name = strtolower((string) ($theme['name'] ?? '')); if (strpos($theme_name, 'bricks') !== false) { $builder_hits[] = ['theme' => (string) ($theme['name'] ?? ''), 'version' => (string) ($theme['version'] ?? '')]; } if (!empty($builder_hits) && count($active_asset_optimizers) >= 2) { // v2.1.5 — soften when ownership is clearly split. The // recommended stack is WP Rocket (page cache + RUCSS + // Delay JS) + Perfmatters (Script Manager + DNS prefetch + // DB cleanup). Each plugin owns a different concern, so // "overlap" is the architecture, not a defect. // // Detection: WP Rocket active AND Perfmatters active AND // RUCSS is owned by WP Rocket (Perfmatters' Remove Unused // CSS is OFF). That's the canonical clean ownership pattern. $optimizer_names = strtolower(implode(' ', array_map(static function ($o) { return (string) ($o['name'] ?? '') . ' ' . (string) ($o['plugin_file'] ?? ''); }, $active_asset_optimizers))); $has_rocket = strpos($optimizer_names, 'rocket') !== false; $has_perfmatters = strpos($optimizer_names, 'perfmatters') !== false; $clear_ownership = false; if ($has_rocket && $has_perfmatters && function_exists('get_option')) { $rocket = get_option('wp_rocket_settings'); $perfmatters_opts = get_option('perfmatters_options'); $rocket_owns_rucss = is_array($rocket) && !empty($rocket['remove_unused_css']); $perfmatters_assets = (is_array($perfmatters_opts) && isset($perfmatters_opts['assets']) && is_array($perfmatters_opts['assets'])) ? $perfmatters_opts['assets'] : (is_array($perfmatters_opts) ? $perfmatters_opts : []); $perfmatters_owns_rucss = !empty($perfmatters_assets['remove_unused_css']) || !empty($perfmatters_assets['rucss']) || !empty($perfmatters_assets['unused_css']); $perfmatters_owns_script_mgr = !empty($perfmatters_assets['script_manager']) || !empty($perfmatters_assets['script_manager_enabled']) || !empty($perfmatters_opts['enable_script_manager']) || !empty($perfmatters_assets['enable_script_manager']); if ($rocket_owns_rucss && !$perfmatters_owns_rucss && $perfmatters_owns_script_mgr) { $clear_ownership = true; } // v3.3.48 — soften: when Rocket cleanly owns RUCSS AND // Perfmatters explicitly DOESN'T also own RUCSS, that alone // resolves the main "double-processing" concern, even if // we can't positively confirm Script Manager from the // option keys (Perfmatters changes them between major // versions). Round-3 review flagged that the strict // detection produced a harsh med/-5 finding on the // intentional clean-ownership stack we recommend. if ($rocket_owns_rucss && !$perfmatters_owns_rucss) { $clear_ownership = true; } } if ($clear_ownership) { // Managed overlap, not conflict — info/0. $findings[] = $this->finding( 'perf_builder_optimizer_managed_overlap', I18n::tr('Builder + WP Rocket + Perfmatters with clear ownership (managed)', 'أداة بناء الصفحات وWP Rocket وPerfmatters بأدوار واضحة'), 'Performance', 'info', 0, [ 'source' => 'options', 'details' => I18n::tr('WP Rocket manages page cache + RUCSS + Delay JS. Perfmatters manages Script Manager + DNS prefetch + DB cleanup. RUCSS is off in Perfmatters (no double processing). This is the recommended stack, not a conflict.', 'يتولى WP Rocket تخزين الصفحات وإزالة التنسيق غير المستخدم وتأخير السكربتات، ويتولى Perfmatters إدارة السكربتات وتنظيف قاعدة البيانات، والميزة نفسها غير مكررة بينهما. هذا توزيع سليم ولا تعارض فيه.'), 'tier' => 'parked_recommendation', 'samples' => [ 'builders' => $builder_hits, 'active_asset_optimizers' => $active_asset_optimizers, 'rocket_owns_rucss' => true, 'perfmatters_owns_script_manager' => true, 'perfmatters_rucss_off' => true, ], ], I18n::tr('Not a conflict. The roles are clearly split: WP Rocket handles caching, unused CSS and delayed scripts, and Perfmatters handles the script manager and database cleanup. This is the recommended setup for page-builder sites.', 'لا تعارض هنا. الأدوار مقسمة بوضوح بين الإضافتين، وهذا هو التوزيع الموصى به مع أدوات بناء الصفحات.'), [ I18n::tr('No action needed.', 'لا يلزم أي إجراء.'), I18n::tr('If you want to be sure: check Perfmatters → Assets → Remove Unused CSS = OFF (✓ correct).', 'للاطمئنان تأكد أن إزالة التنسيق غير المستخدم مغلقة في Perfmatters، فهذا هو الوضع الصحيح.'), ], [ 'how' => I18n::tr('For your information only.', 'للعلم فقط.'), 'pass_condition' => I18n::tr('Nothing to check.', 'لا يوجد ما يُفحص.'), ], $this->id() ); } else { // Real overlap risk — keep the original med/-5 finding. $findings[] = $this->finding( 'perf_builder_optimizer_overlap', I18n::tr('Overlap between a builder and more than one optimizer detected', 'أداة بناء الصفحات تعمل مع أكثر من إضافة أداء دون توزيع واضح للأدوار'), 'Performance', 'med', -5, [ 'source' => 'options', 'details' => I18n::tr('A page builder is running with more than one optimizer plugin, and ownership is not clearly split. Risk of double-processing CSS/JS.', 'تعمل أداة بناء الصفحات مع أكثر من إضافة أداء، والأدوار غير مقسمة بوضوح، فقد تُعالَج ملفات التنسيق والسكربتات مرتين.'), 'samples' => [ 'builders' => $builder_hits, 'active_asset_optimizers' => $active_asset_optimizers, 'theme' => [ 'name' => (string) ($theme['name'] ?? ''), 'version' => (string) ($theme['version'] ?? ''), ], ], ], I18n::tr('Page builders already load a heavy CSS/JS layer. Combining them with more than one optimizer (without a clear ownership split) risks breakage and inconsistent performance.', 'أدوات بناء الصفحات تحمّل كمية كبيرة من التنسيق والسكربتات، وجمعها مع أكثر من إضافة أداء دون توزيع واضح للأدوار يسبب أعطالًا وسرعة غير مستقرة.'), [ I18n::tr('Make one optimizer the primary source for CSS/JS transformations.', 'اجعل محسّنًا واحدًا هو المصدر الأساسي لتحويلات CSS/JS.'), I18n::tr('Or split ownership clearly: WP Rocket (cache+RUCSS+Delay JS) + Perfmatters (Script Manager only, with RUCSS off in Perfmatters).', 'أو وزّع الأدوار بوضوح: إضافة واحدة للتخزين وإزالة التنسيق غير المستخدم وتأخير السكربتات، وإضافة أخرى لإدارة السكربتات فقط.'), I18n::tr('Apply per-page exclusions on builder pages instead of aggressive global activation.', 'طبّق الاستثناءات على صفحات أداة البناء كل صفحة وحدها بدلًا من تفعيل عام على كل الموقع.'), ], [ 'how' => I18n::tr('Run a new audit and compare the request and blocking-script findings on the same pages.', 'شغّل فحصًا جديدًا وقارن نتائج عدد الطلبات والسكربتات الموقفة للعرض في الصفحات نفسها.'), 'pass_condition' => I18n::tr('Either only one optimizer is active, or ownership is clearly split with no double-processing.', 'إما أن يعمل محسّن واحد فقط وإما أن تكون المسؤوليات مقسّمة بوضوح بلا معالجة مزدوجة.'), ], $this->id() ); } } // ── font-display: swap missing ── if ($pages_with_fonts > 0 && !$font_swap_detected_anywhere) { $findings[] = $this->finding( 'perf_font_display_swap_missing', I18n::tr('Custom fonts do not use font-display: swap', 'الخطوط المخصصة لا تعرض النص قبل تحميل الخط (font-display: swap)'), 'Performance', 'med', -4, [ 'source' => 'html', 'details' => sprintf(I18n::tr('Custom font CSS detected on %d pages but no font-display:swap rules found', 'عدد الصفحات التي فيها خطوط مخصصة دون قاعدة font-display: swap: %d'), (int) $pages_with_fonts), 'samples' => [['note' => 'Add font-display: swap to all @font-face declarations']], ], I18n::tr('Without font-display: swap you get FOIT (Flash of Invisible Text) — text stays hidden until the font loads, which hurts LCP and CLS.', 'بدون هذه القاعدة يختفي النص حتى يكتمل تحميل الخط، فيتأخر ظهور المحتوى وقد تتحرك عناصر الصفحة.'), [ I18n::tr('Add font-display: swap to every @font-face in CSS.', 'أضف font-display: swap إلى كل تعريف خط (@font-face) في ملفات التنسيق.'), I18n::tr('In Google Fonts: add &display=swap to the font URL.', 'في خطوط جوجل أضف &display=swap إلى رابط الخط.'), I18n::tr('In WordPress: use the OMGF or Perfmatters plugin to apply swap automatically.', 'في ووردبريس يمكن لإضافة استضافة الخطوط محليًا أو إضافة الأداء تطبيق ذلك تلقائيًا.'), ], [ 'how' => I18n::tr('Run the audit after adding swap.', 'شغّل فحصًا جديدًا بعد إضافة القاعدة.'), 'pass_condition' => I18n::tr('Every @font-face rule has font-display: swap.', 'كل تعريف خط (@font-face) يحتوي font-display: swap.'), ], $this->id() ); } if (!empty($builder_hits)) { // v3.3.49 — round-4 review: builder detection alone shouldn't // ding the score. Many sites run Elementor/Divi with perfectly // healthy CWV. Drop the -1 impact to 0; the finding stays as // info so the AI consumer knows what's running, but it no // longer subtracts a point on detection alone. If actual // asset bloat exists, perf_many_css_js_requests / perf_render_blocking // already catch it with their own scoring. $findings[] = $this->finding( 'perf_builder_detected', I18n::tr('Page Builder detected (diagnostic only — not a penalty)', 'رُصدت أداة لبناء الصفحات (للمعلومة فقط، دون خصم نقاط)'), 'Performance', 'info', 0, [ 'source' => 'options', 'details' => I18n::tr('Detected builder plugins/theme from active list. No scoring impact — pure diagnostic.', 'رُصدت إضافات أو قوالب لبناء الصفحات من القائمة المفعّلة. لا أثر لها على الدرجة فهذا تشخيص فقط.'), 'samples' => $builder_hits, ], I18n::tr('Builders may add assets, but that is a neutral trait until the numbers prove otherwise. If real-user speed data is good, you can ignore this. The score only drops through the "many files" and "blocking scripts" findings when there is real bloat.', 'قد تضيف أدوات البناء ملفات إضافية، لكنها ليست مشكلة بذاتها. إذا كانت بيانات السرعة الحقيقية جيدة فتجاهل هذه الملاحظة، وإذا وُجد ثقل فعلي فسيظهر في نتائج عدد الطلبات والسكربتات.'), [ I18n::tr('Diagnostic only — no action needed if CWV is healthy.', 'للمعلومة فقط، ولا يلزم أي إجراء إذا كانت بيانات السرعة جيدة.'), I18n::tr('If real-user data shows the main content appearing after 2.5 seconds, review the "many files" and "blocking scripts" findings.', 'إذا كان المحتوى الرئيسي يظهر للزوار بعد أكثر من 2.5 ثانية فراجع نتائج عدد الطلبات والسكربتات الموقفة للعرض.'), ], [ 'how' => I18n::tr('Run a new audit, then check the request and blocking-script findings.', 'شغّل فحصًا جديدًا ثم راجع نتائج عدد الطلبات والسكربتات الموقفة للعرض.'), 'pass_condition' => 'n/a (info finding).', ], $this->id() ); } return $findings; } /** * @param array $origins * @return array> */ private function compact_asset_origins(array $origins, int $max = 6): array { $out = []; foreach ($origins as $origin) { if (!is_array($origin)) { continue; } $label = trim((string) ($origin['label'] ?? '')); if ($label === '') { continue; } $out[] = [ 'label' => $label, 'type' => (string) ($origin['type'] ?? ''), 'slug' => (string) ($origin['slug'] ?? ''), 'count' => (int) ($origin['count'] ?? 0), 'examples' => array_values(array_slice(array_map('strval', (array) ($origin['examples'] ?? [])), 0, 2)), ]; if (count($out) >= $max) { break; } } return $out; } /** * @param array> $samples * @return array> */ private function aggregate_asset_origins(array $samples, string $key, int $max = 8): array { $agg = []; foreach ($samples as $sample) { if (!is_array($sample)) { continue; } $origins = isset($sample[$key]) && is_array($sample[$key]) ? $sample[$key] : []; $seenInPage = []; foreach ($origins as $origin) { if (!is_array($origin)) { continue; } $label = trim((string) ($origin['label'] ?? '')); if ($label === '') { continue; } if (!isset($agg[$label])) { $agg[$label] = [ 'label' => $label, 'type' => (string) ($origin['type'] ?? ''), 'slug' => (string) ($origin['slug'] ?? ''), 'pages' => 0, 'total_count' => 0, 'examples' => [], ]; } if (!isset($seenInPage[$label])) { $agg[$label]['pages'] = (int) $agg[$label]['pages'] + 1; $seenInPage[$label] = true; } $agg[$label]['total_count'] = (int) $agg[$label]['total_count'] + (int) ($origin['count'] ?? 0); foreach ((array) ($origin['examples'] ?? []) as $example) { $example = trim((string) $example); if ($example === '' || in_array($example, $agg[$label]['examples'], true)) { continue; } if (count($agg[$label]['examples']) >= 3) { break; } $agg[$label]['examples'][] = $example; } } } if (empty($agg)) { return []; } $rows = array_values($agg); usort($rows, static function (array $a, array $b): int { $pagesA = (int) ($a['pages'] ?? 0); $pagesB = (int) ($b['pages'] ?? 0); if ($pagesA === $pagesB) { $countA = (int) ($a['total_count'] ?? 0); $countB = (int) ($b['total_count'] ?? 0); if ($countA === $countB) { return strcmp((string) ($a['label'] ?? ''), (string) ($b['label'] ?? '')); } return $countB <=> $countA; } return $pagesB <=> $pagesA; }); return array_slice($rows, 0, $max); } /** * v2.1.3: detect WP Rocket Delay JavaScript Execution. Used to gate the * `perf_many_js_requests` finding — Delay JS holds non-essential JS until * user interaction, so high JS counts have much smaller INP impact. * * @param array $wp_ctx */ private function is_delay_js_active(array $wp_ctx): bool { if (!$this->plugin_active($wp_ctx, ['wp rocket', 'wp-rocket'])) { return false; } if (function_exists('get_option')) { $opts = get_option('wp_rocket_settings'); if (is_array($opts) && !empty($opts['delay_js'])) { return true; } } return false; } /** * v2.1.3: detect WP Rocket / Perfmatters / Autoptimize / Async JavaScript * "Defer JS" toggle. Defer is weaker than Delay (the JS still runs on * page load, just non-blocking) — used as a partial mitigation step. * * @param array $wp_ctx */ private function is_defer_js_active(array $wp_ctx): bool { if (function_exists('get_option')) { $rocket = get_option('wp_rocket_settings'); if (is_array($rocket) && !empty($rocket['defer_all_js'])) { return true; } $perfmatters = get_option('perfmatters_options'); if (is_array($perfmatters)) { $assets = isset($perfmatters['assets']) && is_array($perfmatters['assets']) ? $perfmatters['assets'] : $perfmatters; if (!empty($assets['defer_js'])) { return true; } } $autoptimize = get_option('autoptimize_settings'); if (is_array($autoptimize) && !empty($autoptimize['autoptimize_js_defer_not_aggregate'])) { return true; } } return false; } /** * v2.1.3: detect Perfmatters Script Manager active. When the user has * Script Manager, it's reasonable to assume per-page JS is curated, * so a high global JS count is less alarming. * * @param array $wp_ctx */ private function is_script_manager_active(array $wp_ctx): bool { if (!$this->plugin_active($wp_ctx, ['perfmatters'])) { return false; } if (function_exists('get_option')) { $opts = get_option('perfmatters_options'); if (is_array($opts)) { $assets = isset($opts['assets']) && is_array($opts['assets']) ? $opts['assets'] : $opts; if (!empty($assets['script_manager']) || !empty($assets['script_manager_enabled'])) { return true; } // Older Perfmatters used 'enable_script_manager'. if (!empty($opts['enable_script_manager'])) { return true; } } } return false; } /** * v2.1.3: shared helper for plugin presence detection in $wp_ctx['plugins']. * * @param array $wp_ctx * @param array $needles case-insensitive substrings */ private function plugin_active(array $wp_ctx, array $needles): bool { $plugins = isset($wp_ctx['plugins']) && is_array($wp_ctx['plugins']) ? $wp_ctx['plugins'] : []; foreach ($plugins as $p) { if (!is_array($p)) continue; $hay = strtolower((string) ($p['name'] ?? '')) . ' ' . strtolower((string) ($p['slug'] ?? '')); foreach ($needles as $n) { if (strpos($hay, strtolower($n)) !== false) { return true; } } } return false; } /** * v2.0.1: detect WP Rocket Remove Unused CSS active. Used to gate the * "many external CSS" finding — RUCSS already inlines used-CSS so the * remaining external sheets are deferred non-render-blocking. * * @param array $wp_ctx $ctx->get('wp', []) */ private function is_rucss_active(array $wp_ctx): bool { $plugins = isset($wp_ctx['plugins']) && is_array($wp_ctx['plugins']) ? $wp_ctx['plugins'] : []; $wp_rocket_active = false; foreach ($plugins as $p) { if (!is_array($p)) continue; $name = strtolower((string) ($p['name'] ?? '')) . ' ' . strtolower((string) ($p['slug'] ?? '')); if (strpos($name, 'wp rocket') !== false || strpos($name, 'wp-rocket') !== false) { $wp_rocket_active = true; break; } } if (!$wp_rocket_active) { return false; } if (function_exists('get_option')) { $opts = get_option('wp_rocket_settings'); if (is_array($opts) && !empty($opts['remove_unused_css'])) { return true; } } return false; } /** * v2.0.1: detect HTTP/2 or HTTP/3 from the home page response. Used to * gate the external-CSS finding — multiplexing reduces the round-trip * cost of having many small CSS files. * * @param array $home $ctx->get('http_home', []) */ private function is_http3_or_h2_active(array $home): bool { $headers = isset($home['headers']) && is_array($home['headers']) ? $home['headers'] : []; // alt-svc advertises HTTP/3 via h3=… on most CDN-fronted sites. $alt_svc = ''; foreach ($headers as $k => $v) { if (strtolower((string) $k) === 'alt-svc') { $alt_svc = is_array($v) ? implode(',', array_map('strval', $v)) : (string) $v; break; } } if ($alt_svc !== '' && (stripos($alt_svc, 'h3') !== false || stripos($alt_svc, 'h2') !== false)) { return true; } // x-firefox-spdy / :status pseudo-headers won't survive WP HTTP API. // Fallback: check for cf-ray / x-amz-cf-id / akamai / cloudfront — // any of these indicates a CDN edge that defaults to HTTP/2+. foreach ($headers as $k => $_v) { $lk = strtolower((string) $k); if (in_array($lk, ['cf-ray', 'x-amz-cf-id', 'x-akamai-transformed', 'x-amz-cf-pop', 'fly-request-id'], true)) { return true; } } return false; } }