queue as $handle ) { if ( ! isset( $wp_styles->registered[ $handle ] ) ) { continue; } $style = $wp_styles->registered[ $handle ]; // Skip if no source. if ( empty( $style->src ) ) { continue; } // Skip excluded handles. if ( in_array( $handle, $exclusions, true ) ) { continue; } // Skip external stylesheets (different domain). $src = $style->src; if ( strpos( $src, '//' ) === 0 ) { $src = 'https:' . $src; } $site_url = site_url(); if ( strpos( $src, $site_url ) === false && strpos( $src, '/' ) !== 0 ) { continue; } if ( $should_minify ) { $this->process_stylesheet( $handle, $style, $cache_version ); } } } /** * Process a single stylesheet: minify and create cached version. * * @param string $handle Style handle. * @param _WP_Dependency $style Style dependency object. * @param string $cache_version Cache version. */ private function process_stylesheet( $handle, $style, $cache_version ) { $src = $style->src; // Convert URL to file path. $file_path = $this->url_to_path( $src ); if ( ! $file_path || ! file_exists( $file_path ) ) { return; // Cannot read file — serve original. } // Generate cache file path. $hash = md5( $handle . $file_path . $cache_version . filemtime( $file_path ) ); $cache_file = wpsco_get_cache_dir( 'css/' . $handle . '.' . substr( $hash, 0, 8 ) . '.min.css' ); $cache_url = wpsco_get_cache_url( 'css/' . $handle . '.' . substr( $hash, 0, 8 ) . '.min.css' ); $css_ver = ! empty( $style->ver ) ? $style->ver : ( ! empty( $cache_version ) ? $cache_version : WPSCO_VERSION ); // Check if cached version exists and is valid. if ( file_exists( $cache_file ) ) { // Serve cached version. wp_deregister_style( $handle ); wp_register_style( $handle, $cache_url, $style->deps, $css_ver, $style->args ); wp_enqueue_style( $handle ); return; } // Read original CSS. $css = wpsco_read_file( $file_path ); if ( false === $css ) { return; // Read failed — serve original. } // Minify. $minified = $this->minify_css( $css ); if ( false === $minified || empty( $minified ) ) { wpsco_log( sprintf( 'CSS minification failed for %s — serving original.', $handle ), 'warning' ); return; // Minification failed — serve original. } // Fix relative URLs in CSS. $minified = $this->fix_relative_urls( $minified, dirname( $src ) ); // Write cached file via safe WordPress Filesystem API. $written = wpsco_write_file( $cache_file, $minified ); if ( false === $written ) { wpsco_log( sprintf( 'Failed to write CSS cache for %s.', $handle ), 'error' ); return; // Write failed — serve original. } // Replace with cached version. wp_deregister_style( $handle ); wp_register_style( $handle, $cache_url, $style->deps, $css_ver, $style->args ); wp_enqueue_style( $handle ); } /** * Minify CSS content. * * Uses a safe, robust minification approach that preserves: * - CSS variables (custom properties) * - calc() expressions * - @media, @supports, @keyframes * - Gradients, URLs, data URIs * - Escaped characters * - Modern CSS features * * @param string $css CSS content. * @return string|false Minified CSS or false on failure. */ public function minify_css( $css ) { if ( empty( $css ) ) { return ''; } try { // Step 1: Preserve strings and important content. $preserved = array(); $index = 0; // Preserve data URIs and url() content. $css = preg_replace_callback( '/url\s*\(\s*([\'"]?)(.+?)\1\s*\)/is', function ( $matches ) use ( &$preserved, &$index ) { $placeholder = '___WPSCO_URL_' . $index . '___'; $preserved[ $placeholder ] = $matches[0]; $index++; return $placeholder; }, $css ); // Preserve strings (single and double quoted). $css = preg_replace_callback( '/("(?:[^"\\\\]|\\\\.)*"|\'(?:[^\'\\\\]|\\\\.)*\')/s', function ( $matches ) use ( &$preserved, &$index ) { $placeholder = '___WPSCO_STR_' . $index . '___'; $preserved[ $placeholder ] = $matches[0]; $index++; return $placeholder; }, $css ); // Preserve calc() expressions. $css = preg_replace_callback( '/calc\s*\([^)]*(?:\([^)]*\)[^)]*)*\)/is', function ( $matches ) use ( &$preserved, &$index ) { $placeholder = '___WPSCO_CALC_' . $index . '___'; $preserved[ $placeholder ] = $matches[0]; $index++; return $placeholder; }, $css ); // Step 2: Remove comments (but keep /*! important */ comments). $css = preg_replace( '/\/\*(?!\!)[^*]*\*+(?:[^/*][^*]*\*+)*\//', '', $css ); // Step 3: Remove unnecessary whitespace. $css = preg_replace( '/\s+/', ' ', $css ); // Step 4: Remove spaces around specific characters. $css = preg_replace( '/\s*([{}:;,>~+])\s*/', '$1', $css ); // Step 5: Remove trailing semicolons before closing braces. $css = str_replace( ';}', '}', $css ); // Step 6: Remove empty rules. $css = preg_replace( '/[^{}]+\{\s*\}/', '', $css ); // Step 6b: Inject font-display: swap into @font-face rules if missing. if ( wpsco_get_setting( 'font_display_swap', true ) ) { $css = preg_replace_callback( '/@font-face\s*\{([^}]+)\}/is', function ( $matches ) { if ( stripos( $matches[1], 'font-display' ) === false ) { return '@font-face{font-display:swap;' . $matches[1] . '}'; } return $matches[0]; }, $css ); } // Step 7: Restore preserved content. foreach ( $preserved as $placeholder => $original ) { $css = str_replace( $placeholder, $original, $css ); } // Step 8: Final trim. $css = trim( $css ); return $css; } catch ( \Exception $e ) { wpsco_log( 'CSS minification error: ' . $e->getMessage(), 'error' ); return false; } } /** * Fix relative URLs in CSS content. * * @param string $css CSS content. * @param string $base_url Base URL of the original CSS file. * @return string CSS with corrected URLs. */ private function fix_relative_urls( $css, $base_url ) { return preg_replace_callback( '/url\s*\(\s*([\'"]?)(?!(?:data|https?|\/\/):)([^\'"\)]+)\1\s*\)/i', function ( $matches ) use ( $base_url ) { $url = $matches[2]; if ( strpos( $url, '/' ) === 0 ) { return $matches[0]; // Absolute path — leave as-is. } $resolved = trailingslashit( $base_url ) . $url; return 'url(' . $matches[1] . $resolved . $matches[1] . ')'; }, $css ); } /** * Enqueue critical CSS in the head using WordPress core style APIs. */ public function enqueue_critical_css() { if ( is_admin() ) { return; } $critical_css = $this->get_critical_css(); if ( ! empty( $critical_css ) ) { wp_register_style( 'speednest-critical-css', false, array(), WPSCO_VERSION ); wp_enqueue_style( 'speednest-critical-css' ); wp_add_inline_style( 'speednest-critical-css', $critical_css ); } } /** * Backward-compatible inline critical CSS method. */ public function inline_critical_css() { $this->enqueue_critical_css(); } /** * Get critical CSS for the current page. * * @return string */ private function get_critical_css() { $page_type = $this->get_page_type(); $cache_file = wpsco_get_cache_dir( 'critical-css/' . $page_type . '.css' ); if ( file_exists( $cache_file ) ) { return wpsco_read_file( $cache_file ); } return ''; } /** * Get the current page type for critical CSS. * * @return string */ private function get_page_type() { if ( is_front_page() || is_home() ) { return 'front-page'; } if ( is_singular( 'post' ) ) { return 'single-post'; } if ( is_singular( 'page' ) ) { return 'single-page'; } if ( is_singular( 'product' ) ) { return 'single-product'; } if ( is_archive() ) { return 'archive'; } if ( is_search() ) { return 'search'; } return 'default'; } /** * Maybe load a stylesheet asynchronously using rel="preload". * * @param string $tag HTML link tag. * @param string $handle Style handle. * @param string $href Style URL. * @param string $media Style media attribute. * @return string */ public function maybe_async_style( $tag, $handle, $href, $media ) { if ( is_admin() || ! wpsco_get_setting( 'css_async', false ) ) { return $tag; } $exclusions = (array) wpsco_get_setting( 'css_exclusions', array() ); if ( in_array( $handle, $exclusions, true ) ) { return $tag; } // Never async core layout, theme, or Elementor styles to prevent layout shifts and responsive hiding failures. $protected_styles = array( 'elementor', 'elementor-frontend', 'elementor-post', 'elementor-kit', 'elementskit', 'ekit-responsive', 'ekit-widget-styles', 'responsive', 'header', 'logo', 'header-footer', 'theme', 'style.css', 'main.css', 'woocommerce', 'wc-', 'bootstrap', ); foreach ( $protected_styles as $prot ) { if ( stripos( $handle, $prot ) !== false || stripos( $href, $prot ) !== false ) { return $tag; } } if ( strpos( $tag, 'media="print"' ) !== false || strpos( $tag, 'rel="preload"' ) !== false ) { return $tag; } $clean_tag = preg_replace( '/\s*media=["\'][^"\']*["\']/i', '', $tag ); return str_replace( '' . $tag . ''; } /** * Convert a URL to a local file path. * * @param string $url URL. * @return string|false File path or false. */ private function url_to_path( $url ) { return wpsco_url_to_path( $url ); } /** * Generate critical CSS for a URL (manual trigger). * * @param string $url Target URL. * @param string $page_type Page type identifier. * @return bool */ public function generate_critical_css( $url, $page_type = 'default' ) { // Fetch the page HTML. $response = wp_remote_get( $url, array( 'timeout' => 30, 'user-agent' => 'WPSpeedCacheOptimizer/CriticalCSS', ) ); if ( is_wp_error( $response ) ) { wpsco_log( 'Critical CSS generation failed for ' . $url . ': ' . $response->get_error_message(), 'error' ); return false; } $html = wp_remote_retrieve_body( $response ); if ( empty( $html ) ) { return false; } // Extract linked CSS files. $css_content = ''; preg_match_all( '/]+rel=["\']stylesheet["\'][^>]*href=["\']([^"\']+)["\'][^>]*>/i', $html, $matches ); if ( ! empty( $matches[1] ) ) { foreach ( $matches[1] as $css_url ) { $css_response = wp_remote_get( $css_url, array( 'timeout' => 10 ) ); if ( ! is_wp_error( $css_response ) ) { $css_content .= wp_remote_retrieve_body( $css_response ) . "\n"; } } } // Extract above-the-fold critical CSS (simplified heuristic). $critical = $this->extract_critical_selectors( $css_content, $html ); if ( empty( $critical ) ) { return false; } // Save critical CSS using safe WordPress Filesystem API. $cache_file = wpsco_get_cache_dir( 'critical-css/' . sanitize_file_name( $page_type ) . '.css' ); $minified = $this->minify_css( $critical ); wpsco_write_file( $cache_file, $minified ); wpsco_log( sprintf( 'Critical CSS generated for %s (%s)', $url, $page_type ) ); return true; } /** * Extract critical CSS selectors from full CSS based on HTML content. * * This is a safe, conservative extraction that includes: * - All @font-face rules * - All CSS custom properties (:root) * - All @keyframes * - Selectors matching elements found in the HTML * * @param string $css Full CSS content. * @param string $html HTML content. * @return string Critical CSS. */ private function extract_critical_selectors( $css, $html ) { $critical = ''; // Always include @font-face, :root, @keyframes. preg_match_all( '/@font-face\s*\{[^}]*\}/is', $css, $font_matches ); if ( ! empty( $font_matches[0] ) ) { $critical .= implode( "\n", $font_matches[0] ) . "\n"; } preg_match_all( '/:root\s*\{[^}]*\}/is', $css, $root_matches ); if ( ! empty( $root_matches[0] ) ) { $critical .= implode( "\n", $root_matches[0] ) . "\n"; } preg_match_all( '/@keyframes\s+[^{]+\{(?:[^{}]*\{[^}]*\})*[^}]*\}/is', $css, $keyframe_matches ); if ( ! empty( $keyframe_matches[0] ) ) { $critical .= implode( "\n", $keyframe_matches[0] ) . "\n"; } // Extract class names and IDs from HTML. $classes = array(); $ids = array(); preg_match_all( '/class=["\']([^"\']+)["\']/', $html, $class_matches ); if ( ! empty( $class_matches[1] ) ) { foreach ( $class_matches[1] as $class_list ) { $classes = array_merge( $classes, explode( ' ', $class_list ) ); } } preg_match_all( '/id=["\']([^"\']+)["\']/', $html, $id_matches ); if ( ! empty( $id_matches[1] ) ) { $ids = $id_matches[1]; } $classes = array_unique( array_filter( $classes ) ); $ids = array_unique( array_filter( $ids ) ); // Extract tag names. $common_tags = array( 'html', 'body', 'header', 'nav', 'main', 'h1', 'h2', 'h3', 'p', 'a', 'img', 'div', 'section', 'article', 'ul', 'li', 'button', 'input', 'form' ); // Match CSS rules that reference these selectors. // Split CSS into rules (simplified). preg_match_all( '/([^{}]+)\{([^}]*)\}/s', $css, $rules ); if ( ! empty( $rules[1] ) ) { $count = 0; foreach ( $rules[1] as $i => $selector ) { if ( $count > 500 ) { break; // Limit critical CSS size. } $selector = trim( $selector ); // Skip @media wrappers (handled separately). if ( strpos( $selector, '@' ) === 0 ) { continue; } $matched = false; // Check tag selectors. foreach ( $common_tags as $tag ) { if ( preg_match( '/(?:^|[\s,>~+])' . preg_quote( $tag, '/' ) . '(?:[\s,.:#{>\[~+]|$)/i', $selector ) ) { $matched = true; break; } } // Check class selectors. if ( ! $matched ) { foreach ( $classes as $class ) { if ( strpos( $selector, '.' . $class ) !== false ) { $matched = true; break; } } } // Check ID selectors. if ( ! $matched ) { foreach ( $ids as $id ) { if ( strpos( $selector, '#' . $id ) !== false ) { $matched = true; break; } } } if ( $matched ) { $critical .= $selector . '{' . $rules[2][ $i ] . "}\n"; $count++; } } } return $critical; } }