@@ -133,6 +133,7 @@ function es_optimizer_init_frontend_optimizations() {
133133 add_action ( 'wp_enqueue_scripts ' , 'disable_classic_theme_styles ' , 100 );
134134 add_action ( 'init ' , 'remove_header_items ' );
135135 add_action ( 'init ' , 'remove_recent_comments_style ' );
136+ add_action ( 'wp_head ' , 'add_preconnect ' , 0 );
136137 add_action ( 'wp_head ' , 'add_dns_prefetch ' , 0 );
137138 add_action ( 'init ' , 'disable_jetpack_ads ' );
138139 add_action ( 'init ' , 'disable_post_via_email ' );
@@ -186,17 +187,20 @@ function es_optimizer_get_default_options() {
186187 'remove_wlw_manifest ' => 0 ,
187188 'remove_shortlink ' => 0 ,
188189 'remove_recent_comments_style ' => 0 ,
189- 'enable_dns_prefetch ' => 0 ,
190- 'dns_prefetch_domains ' => implode (
190+ 'enable_preconnect ' => 0 ,
191+ 'preconnect_domains ' => implode (
191192 "\n" ,
192193 array (
193194 'https://fonts.googleapis.com ' ,
194195 'https://fonts.gstatic.com ' ,
195196 'https://s.w.org ' ,
196197 'https://wordpress.com ' ,
197198 'https://cdnjs.cloudflare.com ' ,
199+ 'https://www.googletagmanager.com ' ,
198200 )
199201 ),
202+ 'enable_dns_prefetch ' => 0 ,
203+ 'dns_prefetch_domains ' => 'https://adservice.google.com ' ,
200204 'disable_jetpack_ads ' => 0 ,
201205 'disable_post_via_email ' => 0 ,
202206 );
@@ -392,22 +396,6 @@ function es_optimizer_render_header_options( $options ) {
392396 * @param array $options Plugin options.
393397 */
394398function es_optimizer_render_additional_options ( $ options ) {
395- // DNS Prefetch settings.
396- es_optimizer_render_checkbox_option (
397- $ options ,
398- 'enable_dns_prefetch ' ,
399- esc_html__ ( 'Enable DNS Prefetch ' , 'simple-wp-optimizer ' ),
400- esc_html__ ( 'Add DNS prefetch for common external domains ' , 'simple-wp-optimizer ' )
401- );
402-
403- // DNS Prefetch Domains textarea.
404- es_optimizer_render_textarea_option (
405- $ options ,
406- 'dns_prefetch_domains ' ,
407- esc_html__ ( 'DNS Prefetch Domains ' , 'simple-wp-optimizer ' ),
408- esc_html__ ( 'Enter one HTTPS domain per line (e.g., https://fonts.googleapis.com). Only clean domains are allowed - no file paths, query parameters, or fragments. Only secure HTTPS domains are accepted for security reasons. ' , 'simple-wp-optimizer ' )
409- );
410-
411399 // Jetpack Ads settings.
412400 es_optimizer_render_checkbox_option (
413401 $ options ,
@@ -423,6 +411,38 @@ function es_optimizer_render_additional_options( $options ) {
423411 esc_html__ ( 'Disable Post via Email ' , 'simple-wp-optimizer ' ),
424412 esc_html__ ( 'Disable WordPress post via email functionality for security and performance ' , 'simple-wp-optimizer ' )
425413 );
414+
415+ // Preconnect settings.
416+ es_optimizer_render_checkbox_option (
417+ $ options ,
418+ 'enable_preconnect ' ,
419+ esc_html__ ( 'Enable Preconnect ' , 'simple-wp-optimizer ' ),
420+ esc_html__ ( 'Preconnect to external domains for faster resource loading ' , 'simple-wp-optimizer ' )
421+ );
422+
423+ // Preconnect Domains textarea.
424+ es_optimizer_render_textarea_option (
425+ $ options ,
426+ 'preconnect_domains ' ,
427+ esc_html__ ( 'Preconnect Domains ' , 'simple-wp-optimizer ' ),
428+ esc_html__ ( 'Use preconnect for domains that host critical, frequently used resources, like Google Fonts. This hint tells the browser to establish a connection (including DNS lookup, TCP handshake, and TLS negotiation) as soon as possible, which can save 100–500ms on the subsequent request. Enter one HTTPS domain per line (e.g., https://fonts.googleapis.com). Only clean domains are allowed - no file paths, query parameters, or fragments. ' , 'simple-wp-optimizer ' )
429+ );
430+
431+ // DNS Prefetch settings.
432+ es_optimizer_render_checkbox_option (
433+ $ options ,
434+ 'enable_dns_prefetch ' ,
435+ esc_html__ ( 'Enable DNS Prefetch ' , 'simple-wp-optimizer ' ),
436+ esc_html__ ( 'DNS prefetch for less critical external domains ' , 'simple-wp-optimizer ' )
437+ );
438+
439+ // DNS Prefetch Domains textarea.
440+ es_optimizer_render_textarea_option (
441+ $ options ,
442+ 'dns_prefetch_domains ' ,
443+ esc_html__ ( 'DNS Prefetch Domains ' , 'simple-wp-optimizer ' ),
444+ esc_html__ ( 'DNS-prefetch is a lighter-weight alternative to preconnect that performs only the DNS lookup. Use it for less critical domains or as a fallback for browsers that don \'t support preconnect. Enter one HTTPS domain per line (e.g., https://adservice.google.com). Only clean domains are allowed - no file paths, query parameters, or fragments. ' , 'simple-wp-optimizer ' )
445+ );
426446}
427447
428448/**
@@ -580,6 +600,7 @@ function es_optimizer_validate_options( $input ) {
580600 'remove_wlw_manifest ' ,
581601 'remove_shortlink ' ,
582602 'remove_recent_comments_style ' ,
603+ 'enable_preconnect ' ,
583604 'enable_dns_prefetch ' ,
584605 'disable_jetpack_ads ' ,
585606 'disable_post_via_email ' ,
@@ -589,22 +610,27 @@ function es_optimizer_validate_options( $input ) {
589610 $ valid [ $ checkbox ] = isset ( $ input [ $ checkbox ] ) ? 1 : 0 ;
590611 }
591612
613+ // Validate and sanitize the preconnect domains with enhanced security.
614+ if ( isset ( $ input ['preconnect_domains ' ] ) ) {
615+ $ valid ['preconnect_domains ' ] = es_optimizer_validate_preconnect_domains ( $ input ['preconnect_domains ' ] );
616+ }
617+
592618 // Validate and sanitize the DNS prefetch domains with enhanced security.
593619 if ( isset ( $ input ['dns_prefetch_domains ' ] ) ) {
594- $ valid ['dns_prefetch_domains ' ] = es_optimizer_validate_dns_domains ( $ input ['dns_prefetch_domains ' ] );
620+ $ valid ['dns_prefetch_domains ' ] = es_optimizer_validate_dns_prefetch_domains ( $ input ['dns_prefetch_domains ' ] );
595621 }
596622
597623 return $ valid ;
598624}
599625
600626/**
601- * Validate DNS prefetch domains with enhanced security
627+ * Validate preconnect domains with enhanced security
602628 *
603629 * @since 1.4.0
604630 * @param string $domains_input Raw domain input from user.
605631 * @return string Validated and sanitized domains.
606632 */
607- function es_optimizer_validate_dns_domains ( $ domains_input ) {
633+ function es_optimizer_validate_preconnect_domains ( $ domains_input ) {
608634 $ domains = explode ( "\n" , trim ( $ domains_input ) );
609635 $ sanitized_domains = array ();
610636 $ rejected_domains = array ();
@@ -633,7 +659,71 @@ function es_optimizer_validate_dns_domains( $domains_input ) {
633659}
634660
635661/**
636- * Validate a single DNS prefetch domain
662+ * Validate DNS prefetch domains with enhanced security
663+ *
664+ * @since 1.8.0
665+ * @param string $domains_input Raw domain input from user.
666+ * @return string Validated and sanitized domains.
667+ */
668+ function es_optimizer_validate_dns_prefetch_domains ( $ domains_input ) {
669+ $ domains = explode ( "\n" , trim ( $ domains_input ) );
670+ $ sanitized_domains = array ();
671+ $ rejected_domains = array ();
672+
673+ foreach ( $ domains as $ domain ) {
674+ $ domain = trim ( $ domain );
675+ if ( empty ( $ domain ) ) {
676+ continue ;
677+ }
678+
679+ $ validation_result = es_optimizer_validate_single_domain ( $ domain );
680+
681+ if ( $ validation_result ['valid ' ] ) {
682+ $ sanitized_domains [] = $ validation_result ['domain ' ];
683+ } else {
684+ $ rejected_domains [] = $ validation_result ['error ' ];
685+ }
686+ }
687+
688+ // Show admin notice if any domains were rejected for security reasons.
689+ if ( ! empty ( $ rejected_domains ) ) {
690+ es_optimizer_show_dns_prefetch_rejection_notice ( $ rejected_domains );
691+ }
692+
693+ return implode ( "\n" , $ sanitized_domains );
694+ }
695+
696+ /**
697+ * Show admin notice for rejected DNS prefetch domains
698+ *
699+ * @since 1.8.0
700+ * @param array $rejected_domains Array of rejected domain strings.
701+ */
702+ function es_optimizer_show_dns_prefetch_rejection_notice ( $ rejected_domains ) {
703+ // Security: Properly escape and limit the rejected domains in error messages.
704+ $ escaped_domains = array_map ( 'esc_html ' , array_slice ( $ rejected_domains , 0 , 3 ) );
705+ $ rejected_message = implode ( ', ' , $ escaped_domains );
706+
707+ if ( count ( $ rejected_domains ) > 3 ) {
708+ $ rejected_message .= esc_html__ ( '... ' , 'simple-wp-optimizer ' );
709+ }
710+
711+ $ message = sprintf (
712+ // translators: %s is the list of rejected domain names.
713+ esc_html__ ( 'Some DNS prefetch domains were rejected for security reasons: %s ' , 'simple-wp-optimizer ' ),
714+ $ rejected_message
715+ );
716+
717+ add_settings_error (
718+ 'es_optimizer_options ' ,
719+ 'dns_prefetch_security ' ,
720+ $ message ,
721+ 'warning '
722+ );
723+ }
724+
725+ /**
726+ * Validate a single preconnect domain
637727 *
638728 * @since 1.4.0
639729 * @param string $domain Domain to validate.
@@ -651,7 +741,7 @@ function es_optimizer_validate_single_domain( $domain ) {
651741 // Use wp_parse_url instead of parse_url for WordPress compatibility.
652742 $ parsed_url = wp_parse_url ( $ domain );
653743
654- // Security: Enforce HTTPS-only domains for DNS prefetch .
744+ // Security: Enforce HTTPS-only domains for preconnect .
655745 if ( ! isset ( $ parsed_url ['scheme ' ] ) || 'https ' !== $ parsed_url ['scheme ' ] ) {
656746 return array (
657747 'valid ' => false ,
@@ -667,19 +757,19 @@ function es_optimizer_validate_single_domain( $domain ) {
667757 );
668758 }
669759
670- // Security: DNS prefetch should only use clean domains, not file paths.
760+ // Security: Preconnect should only use clean domains, not file paths.
671761 // Reject URLs with paths, query parameters, or fragments.
672762 if ( isset ( $ parsed_url ['path ' ] ) && '/ ' !== $ parsed_url ['path ' ] && '' !== $ parsed_url ['path ' ] ) {
673763 return array (
674764 'valid ' => false ,
675- 'error ' => $ domain . ' (file paths not allowed for DNS prefetch - use domain only) ' ,
765+ 'error ' => $ domain . ' (file paths not allowed for preconnect - use domain only) ' ,
676766 );
677767 }
678768
679769 if ( isset ( $ parsed_url ['query ' ] ) || isset ( $ parsed_url ['fragment ' ] ) ) {
680770 return array (
681771 'valid ' => false ,
682- 'error ' => $ domain . ' (query parameters and fragments not allowed for DNS prefetch ) ' ,
772+ 'error ' => $ domain . ' (query parameters and fragments not allowed for preconnect ) ' ,
683773 );
684774 }
685775
@@ -727,13 +817,13 @@ function es_optimizer_show_domain_rejection_notice( $rejected_domains ) {
727817
728818 $ message = sprintf (
729819 // translators: %s is the list of rejected domain names.
730- esc_html__ ( 'Some DNS prefetch domains were rejected for security reasons: %s ' , 'simple-wp-optimizer ' ),
820+ esc_html__ ( 'Some preconnect domains were rejected for security reasons: %s ' , 'simple-wp-optimizer ' ),
731821 $ rejected_message
732822 );
733823
734824 add_settings_error (
735825 'es_optimizer_options ' ,
736- 'dns_prefetch_security ' ,
826+ 'preconnect_security ' ,
737827 $ message ,
738828 'warning '
739829 );
@@ -906,15 +996,82 @@ function remove_recent_comments_style() {
906996}
907997
908998/**
909- * Add DNS prefetching for common external domains.
999+ * Add preconnect hints for common external domains.
9101000 *
911- * DNS prefetching can reduce latency when connecting to common external services.
912- * This is particularly helpful for sites using Google Fonts, Analytics, etc.
1001+ * Preconnect establishes early connections (DNS + TCP + TLS handshake) to third-party domains.
1002+ * This reduces latency when loading resources from external origins and improves LCP/FCP metrics.
1003+ * More effective than dns-prefetch as it completes the full connection setup.
9131004 *
9141005 * Security note: All output is properly escaped with esc_url() before output to prevent XSS.
9151006 *
9161007 * @since 1.4.1
9171008 */
1009+ function add_preconnect () {
1010+ // Only add if not admin and not doing AJAX.
1011+ if ( is_admin () || wp_doing_ajax () ) {
1012+ return ;
1013+ }
1014+
1015+ // Use static caching to avoid repeated option retrieval.
1016+ static $ domains_cache = null ;
1017+ static $ options_checked = false ;
1018+
1019+ if ( ! $ options_checked ) {
1020+ $ options = get_option ( 'es_optimizer_options ' );
1021+ $ options_checked = true ;
1022+
1023+ // Only proceed if the option is enabled.
1024+ if ( ! isset ( $ options ['enable_preconnect ' ] ) || ! $ options ['enable_preconnect ' ] ) {
1025+ $ domains_cache = array (); // Cache empty array to avoid re-checking.
1026+ return ;
1027+ }
1028+
1029+ // Get and process domains from settings.
1030+ if ( isset ( $ options ['preconnect_domains ' ] ) && ! empty ( $ options ['preconnect_domains ' ] ) ) {
1031+ // Process domains with optimization.
1032+ $ domains = explode ( "\n" , $ options ['preconnect_domains ' ] );
1033+ $ domains = array_map ( 'trim ' , $ domains );
1034+ $ domains = array_filter ( $ domains );
1035+
1036+ // Remove duplicates and validate domains.
1037+ $ domains = array_unique ( $ domains );
1038+ $ valid_domains = array ();
1039+
1040+ foreach ( $ domains as $ domain ) {
1041+ // Validate URL format and ensure HTTPS.
1042+ if ( filter_var ( $ domain , FILTER_VALIDATE_URL ) && strpos ( $ domain , 'https:// ' ) === 0 ) {
1043+ $ valid_domains [] = $ domain ;
1044+ }
1045+ }
1046+
1047+ $ domains_cache = $ valid_domains ;
1048+ } else {
1049+ $ domains_cache = array ();
1050+ }
1051+ }
1052+
1053+ // Output the preconnect links.
1054+ if ( ! empty ( $ domains_cache ) ) {
1055+ foreach ( $ domains_cache as $ domain ) {
1056+ // Add crossorigin attribute for font domains (required for CORS requests).
1057+ $ crossorigin = ( strpos ( $ domain , 'fonts.g ' ) !== false || strpos ( $ domain , 'gstatic ' ) !== false ) ? ' crossorigin ' : '' ;
1058+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
1059+ echo '<link rel="preconnect" href=" ' . esc_url ( $ domain ) . '" ' . $ crossorigin . '> ' . "\n" ;
1060+ }
1061+ }
1062+ }
1063+
1064+ /**
1065+ * Add DNS prefetch hints for external domains.
1066+ *
1067+ * DNS-prefetch performs only the DNS lookup for third-party domains.
1068+ * This is a lighter-weight alternative to preconnect for less critical resources.
1069+ * Use for domains that may not be used immediately or as a fallback.
1070+ *
1071+ * Security note: All output is properly escaped with esc_url() before output to prevent XSS.
1072+ *
1073+ * @since 1.8.0
1074+ */
9181075function add_dns_prefetch () {
9191076 // Only add if not admin and not doing AJAX.
9201077 if ( is_admin () || wp_doing_ajax () ) {
@@ -959,7 +1116,7 @@ function add_dns_prefetch() {
9591116 }
9601117 }
9611118
962- // Output the prefetch links.
1119+ // Output the DNS prefetch links.
9631120 if ( ! empty ( $ domains_cache ) ) {
9641121 foreach ( $ domains_cache as $ domain ) {
9651122 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
0 commit comments