PluginProbe
Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score / trunk
Powered Cache – Caching and Optimization for WordPress – Easily Improve PageSpeed & Web Vitals Score vtrunk
trunk 1.0 1.0.1 1.1 1.1.1 1.1.2 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 2.0 2.0.1 2.0.2 2.0.3 2.0.4 2.1 2.1.1 2.1.2 2.2 2.2.1 All 69 releases
← All changes | includes/classes/FileOptimizer.php +450 -45 2.1trunk View file →
@@ -6,9 +6,12 @@
6 6 */
7 7
8 8 namespace PoweredCache;
9 9
10 +use PoweredCache\Dependencies\voku\helper\HtmlMin;
10 11 use const PoweredCache\Constants\POST_META_DISABLE_CSS_OPTIMIZATION;
12 +use const PoweredCache\Constants\POST_META_DISABLE_JS_DEFER;
13 +use const PoweredCache\Constants\POST_META_DISABLE_JS_DELAY;
11 14 use const PoweredCache\Constants\POST_META_DISABLE_JS_OPTIMIZATION;
12 15 use PoweredCache\Optimizer\CSS;
13 16 use PoweredCache\Optimizer\Helper;
14 17 use PoweredCache\Optimizer\JS;
@@ -56,8 +59,22 @@
56 59 * Setup routine
57 60 */
58 61 public function setup() {
59 62 $this->settings = \PoweredCache\Utils\get_settings();
63 +
64 + // Check request method
65 + if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || ! in_array( $_SERVER['REQUEST_METHOD'], [ 'GET', 'HEAD' ], true ) ) {
66 + return true;
67 + }
68 +
69 + add_action( 'plugins_loaded', [ $this, 'setup_file_optimizer' ] );
70 +
71 + }
72 +
73 + /**
74 + * Setup file optimizer module
75 + */
76 + public function setup_file_optimizer() {
60 77 /**
61 78 * Filters whether apply or not apply file optimizations on wp-admin.
62 79 *
63 80 * @hook powered_cache_fo_dashboard
@@ -79,12 +96,41 @@
79 96 if ( is_admin() && ! $this->optimize_dashboard ) {
80 97 return;
81 98 }
82 99
100 + /**
101 + * Don't optimize in customizer preview
102 + */
103 + if ( ! empty( $_GET['customize_changeset_uuid'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
104 + \PoweredCache\Utils\log( 'Do not run file optimizer in customizer preview' );
105 +
106 + return;
107 + }
108 +
109 + /**
110 + * Filters FileOptimizer integration
111 + *
112 + * @hook powered_cache_fo_disable
113 + *
114 + * @param {boolean} False by default.
115 + *
116 + * @return {boolean} New value.
117 + * @since 2.2
118 + */
119 + $disable_file_optimizer = apply_filters( 'powered_cache_fo_disable', false );
120 +
121 + if ( $disable_file_optimizer ) {
122 + return;
123 + }
124 +
83 125 add_action( 'init', [ $this, 'setup_css_combine' ] );
84 126 add_action( 'init', [ $this, 'setup_js_combine' ] );
85 127 add_filter( 'script_loader_tag', [ $this, 'js_minify' ], 10, 3 );
86 128 add_filter( 'style_loader_tag', [ $this, 'css_minify' ], 10, 4 );
129 + add_filter( 'powered_cache_fo_script_loader_tag', [ $this, 'change_js_execute_method' ] );
130 + add_filter( 'script_loader_tag', [ $this, 'change_js_execute_method' ], 99 );
131 + add_action( 'after_setup_theme', [ $this, 'maybe_start_buffer' ], 999 );
132 + add_action( 'template_redirect', [ $this, 'maybe_suppress_optimizations' ] );
87 133
88 134 if ( ! $this->settings['combine_js'] ) {
89 135 add_filter( 'powered_cache_fo_js_do_concat', '__return_false' );
90 136 }
@@ -92,25 +138,89 @@
92 138 if ( ! $this->settings['combine_css'] ) {
93 139 add_filter( 'powered_cache_fo_css_do_concat', '__return_false' );
94 140 }
95 141
96 - add_filter( 'powered_cache_fo_script_loader_tag', [ $this, 'change_js_execute_method' ] );
142 + if ( $this->settings['combine_google_fonts'] ) {
143 + add_action( 'wp_enqueue_scripts', [ $this, 'combine_google_fonts' ], 99 );
144 + }
145 + }
97 146
98 - if ( ! $this->settings['js_execution_optimized_only'] ) {
99 - add_filter( 'script_loader_tag', [ $this, 'change_js_execute_method' ], 99 );
147 + /**
148 + * Process output buffer
149 + *
150 + * @return void
151 + */
152 + public function maybe_start_buffer() {
153 + if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
154 + return;
100 155 }
156 + $request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
101 157
102 - add_action( 'after_setup_theme', [ $this, 'html_minify' ] );
158 + if ( strpos( $request_uri, 'robots.txt' ) !== false || strpos( $request_uri, '.htaccess' ) !== false ) {
159 + return;
160 + }
103 161
104 - if ( $this->settings['combine_google_fonts'] ) {
105 - add_action( 'wp_enqueue_scripts', [ $this, 'combine_google_fonts' ], 99 );
162 + if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'GET' !== $_SERVER['REQUEST_METHOD'] ) {
163 + return;
106 164 }
107 165
108 - add_action( 'template_redirect', [ $this, 'maybe_suppress_optimizations' ] );
166 + $file_extension = preg_replace( '#^(.*?)\?.*$#', '$1', $request_uri );
167 + $file_extension = trim( preg_replace( '#^.*\.(.*)$#', '$1', $file_extension ) );
168 +
169 + if ( ! preg_match( '#index\.php$#i', $request_uri ) && in_array( $file_extension, array( 'php', 'xml', 'xsl' ), true ) ) {
170 + return;
171 + }
172 +
173 + if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
174 + return;
175 + }
176 +
177 + ob_start( [ $this, 'process_buffer' ] );
109 178 }
110 179
180 + /**
181 + * Process output buffer
182 + *
183 + * @param string $html Output buffer
184 + *
185 + * @return string|string[]|null
186 + */
187 + public function process_buffer( $html ) {
188 + $html = $this->maybe_defer_inline_scripts( $html );
189 + $html = $this->maybe_delay_scripts( $html );
190 + $html = $this->maybe_replace_google_fonts_with_bunny_fonts( $html );
191 + $html = $this->maybe_minify_html( $html );
111 192
193 + return $html;
194 + }
195 +
112 196 /**
197 + * Replace Google Fonts with Bunny drop-in replacement's
198 + *
199 + * @param string $html Output buffer
200 + *
201 + * @return array|mixed|string|string[]
202 + * @since 3.0
203 + */
204 + public function maybe_replace_google_fonts_with_bunny_fonts( $html ) {
205 + if ( ! $this->settings['use_bunny_fonts'] ) {
206 + return $html;
207 + }
208 +
209 + $html = str_replace(
210 + [
211 + 'https://fonts.googleapis.com',
212 + 'http://fonts.googleapis.com',
213 + '//fonts.googleapis.com',
214 + ],
215 + 'https://fonts.bunny.net',
216 + $html
217 + );
218 +
219 + return $html;
220 + }
221 +
222 + /**
113 223 * Maybe suppress optimizations based on the post meta options
114 224 *
115 225 * @since 2.1
116 226 */
@@ -117,8 +227,10 @@
117 227 public function maybe_suppress_optimizations() {
118 228 if ( is_singular() ) {
119 229 $disable_css_optimization = (bool) get_post_meta( get_the_ID(), POST_META_DISABLE_CSS_OPTIMIZATION, true );
120 230 $disable_js_optimization = (bool) get_post_meta( get_the_ID(), POST_META_DISABLE_JS_OPTIMIZATION, true );
231 + $disable_js_defer = (bool) get_post_meta( get_the_ID(), POST_META_DISABLE_JS_DEFER, true );
232 + $disable_js_delay = (bool) get_post_meta( get_the_ID(), POST_META_DISABLE_JS_DELAY, true );
121 233
122 234 if ( $disable_css_optimization ) {
123 235 add_filter( 'powered_cache_fo_css_do_concat', '__return_false' );
124 236 add_filter( 'powered_cache_fo_disable_css_minify', '__return_true' );
@@ -127,8 +239,17 @@
127 239 if ( $disable_js_optimization ) {
128 240 add_filter( 'powered_cache_fo_js_do_concat', '__return_false' );
129 241 add_filter( 'powered_cache_fo_disable_js_minify', '__return_true' );
130 242 }
243 +
244 + if ( $disable_js_defer ) {
245 + add_filter( 'powered_cache_disable_js_defer', '__return_true' );
246 + add_filter( 'powered_cache_disable_js_defer_inline', '__return_true' );
247 + }
248 +
249 + if ( $disable_js_delay ) {
250 + add_filter( 'powered_cache_delayed_js_skip', '__return_true' );
251 + }
131 252 }
132 253 }
133 254
134 255 /**
@@ -142,15 +263,23 @@
142 263 }
143 264 }
144 265
145 266 /**
146 - * Minify HTML output
267 + * Minify given HTML output
268 + *
269 + * @param string $buffer HTML
270 + *
271 + * @return string|string[]|null
147 272 */
148 - public function html_minify() {
273 + public function maybe_minify_html( $buffer ) {
149 274 if ( ! $this->settings['minify_html'] ) {
150 - return;
275 + return $buffer;
151 276 }
152 277
278 + if ( false === stripos( $buffer, '<html' ) && false === stripos( $buffer, '<!DOCTYPE html' ) ) {
279 + return $buffer;
280 + }
281 +
153 282 /**
154 283 * Filters whether disable or not disable HTML minification.
155 284 *
156 285 * @hook powered_cache_fo_disable_html_minify
@@ -160,32 +289,25 @@
160 289 * @return {boolean} New value.
161 290 * @since 2.0
162 291 */
163 292 if ( apply_filters( 'powered_cache_fo_disable_html_minify', false ) ) {
164 - return;
293 + return $buffer;
165 294 }
166 295
167 - ob_start( [ $this, 'run_html_minify' ] );
168 - }
296 + $html_min = new HtmlMin();
297 + $html_min->doOptimizeViaHtmlDomParser( $this->settings['minify_html_dom_optimization'] );
298 + $html_min->doRemoveOmittedQuotes( false );
299 + $html_min->doRemoveOmittedHtmlTags( false );
300 + $html_min->overwriteSpecialScriptTags( [ 'x-tmpl-mustache', 'text/template' ] );
301 + $buffer = $html_min->minify( $buffer );
169 302
170 - /**
171 - * Minify given HTML output
172 - *
173 - * @param string $buffer HTML
174 - *
175 - * @return string|string[]|null
176 - */
177 - public function run_html_minify( $buffer ) {
178 - $search = array(
179 - '/\>[^\S ]+/s', // strip whitespaces after tags, except space
180 - '/[^\S ]+\</s', // strip whitespaces before tags, except space
181 - '/(\s)+/s', // shorten multiple whitespace sequences
182 - '/^\\s+|\\s+$/', // shorten multiple whitespace sequences
183 - );
303 + if ( ! $this->settings['minify_html_dom_optimization'] ) {
304 + // Remove line breaks and spaces between tags
305 + $buffer = preg_replace( '/>\s+</', '><', $buffer );
306 + // Remove comments
307 + $buffer = preg_replace( '/<!--(.|\s)*?-->/', '', $buffer );
308 + }
184 309
185 - $replace = array( '>', '<', '\\1', ' ' );
186 - $buffer = preg_replace( $search, $replace, $buffer );
187 -
188 310 return $buffer;
189 311 }
190 312
191 313
@@ -196,29 +318,40 @@
196 318 *
197 319 * @return mixed
198 320 */
199 321 public function change_js_execute_method( $tag ) {
200 - $js_execution = $this->settings['js_execution_method'];
201 - if ( ! $js_execution ) {
322 + $is_defer = $this->settings['js_defer'];
323 +
324 + if ( ! $is_defer ) {
202 325 return $tag;
203 326 }
204 327
205 - if ( 'blocking' === $js_execution ) {
328 + if ( preg_match( '/<script\s+[^>]*\b(?:async|defer)\b[^>]*>/i', $tag ) ) {
206 329 return $tag;
207 330 }
208 331
209 - if ( 'async' === $js_execution ) {
210 - $js_attr = 'async="async"';
211 - } elseif ( 'defer' === $js_execution ) {
212 - $js_attr = 'defer="defer"';
332 + if ( Helper::is_defer_excluded( $tag ) ) {
333 + return $tag;
213 334 }
214 335
215 - if ( false === stripos( $tag, $js_attr ) ) {
216 - $search = '<script ';
217 - $replace = sprintf( '<script %s ', $js_attr );
218 - $tag = str_replace( $search, $replace, $tag );
336 + /**
337 + * Filters whether disable or not disable Defer
338 + *
339 + * @hook powered_cache_disable_js_defer
340 + *
341 + * @param {boolean} true to disable defer
342 + *
343 + * @return {boolean} New value.
344 + * @since 3.2
345 + */
346 + if ( apply_filters( 'powered_cache_disable_js_defer', false, $tag ) ) {
347 + return $tag;
219 348 }
220 349
350 + $search = '<script ';
351 + $replace = '<script defer="defer" ';
352 + $tag = str_replace( $search, $replace, $tag );
353 +
221 354 return $tag;
222 355 }
223 356
224 357 /**
@@ -297,13 +430,16 @@
297 430 *
298 431 * @hook powered_cache_fo_disable_js_minify
299 432 *
300 433 * @param {boolean} true to disable JS minify
434 + * @param {string} $tag script tag <script...
435 + * @param {string} $handle JS handle
436 + * @param {string} $src Resource URL
301 437 *
302 438 * @return {boolean} New value.
303 439 * @since 2.0
304 440 */
305 - if ( apply_filters( 'powered_cache_fo_disable_js_minify', false ) ) {
441 + if ( apply_filters( 'powered_cache_fo_disable_js_minify', false, $tag, $handle, $src ) ) {
306 442 return $tag;
307 443 }
308 444
309 445 // only minify static .css
@@ -429,9 +565,9 @@
429 565 }
430 566
431 567 $style_src = $wp_styles->registered[ $handle ]->src;
432 568
433 - if ( false !== strpos( $style_src, 'fonts.googleapis.com/css' ) ) {
569 + if ( false !== strpos( $style_src, 'fonts.googleapis.com/css' ) || false !== strpos( $style_src, 'fonts.bunny.net/css' ) ) {
434 570 $url = wp_parse_url( $style_src );
435 571
436 572 if ( is_string( $url['query'] ) ) {
437 573 parse_str( $url['query'], $parsed_url );
@@ -494,8 +630,17 @@
494 630 $font_args['subset'] = implode( ',', $subsets );
495 631 }
496 632
497 633 /**
634 + * Force font display: swap
635 + *
636 + * @since 2.2
637 + */
638 + if ( $this->settings['swap_google_fonts_display'] ) {
639 + $font_display = 'swap';
640 + }
641 +
642 + /**
498 643 * Filters font display attribute of the google fonts.
499 644 *
500 645 * @hook powered_cache_fo_google_font_display
501 646 *
@@ -500,9 +645,9 @@
500 645 * @hook powered_cache_fo_google_font_display
501 646 *
502 647 * @param {string} $font_display font display attribute.
503 648 *
504 - * @return {boolean} New value.
649 + * @return {string} New value.
505 650 * @since 2.0
506 651 */
507 652 $font_display = apply_filters( 'powered_cache_fo_google_font_display', $font_display );
508 653
@@ -509,10 +654,22 @@
509 654 if ( ! empty( $font_display ) ) {
510 655 $font_args['display'] = $font_display;
511 656 }
512 657
513 - $src = esc_url_raw( add_query_arg( $font_args, $google_fonts_domain ) );
658 + /**
659 + * Filters google font's domain
660 + *
661 + * @hook powered_cache_fo_google_fonts_domain
662 + *
663 + * @param {string} $font_display font display attribute.
664 + *
665 + * @return {string} New value.
666 + * @since 3.0
667 + */
668 + $fonts_domain = apply_filters( 'powered_cache_fo_google_fonts_domain', $google_fonts_domain );
514 669
670 + $src = esc_url_raw( add_query_arg( $font_args, $fonts_domain ) );
671 +
515 672 // Enqueue google fonts into one URL request
516 673 wp_enqueue_style( // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
517 674 'pc-google-fonts-' . md5( $src ),
518 675 $src,
@@ -523,6 +680,254 @@
523 680 }
524 681 }
525 682 }
526 683 }
684 +
685 + /**
686 + * DelayedJS execution
687 + * Similar logic with image lazy-loading but this time for scripts.
688 + *
689 + * @param string $html HTML Buffer
690 + *
691 + * @return string
692 + * @since 3.0
693 + */
694 + public function maybe_delay_scripts( $html ) {
695 + if ( ! $this->settings['js_delay'] ) {
696 + return $html;
697 + }
698 +
699 + $pattern = '/<script([^>]*)>(.*?)<\/script>/si';
700 +
701 + preg_match_all( $pattern, $html, $matches, PREG_SET_ORDER );
702 +
703 + if ( empty( $matches ) ) {
704 + return $html;
705 + }
706 +
707 + $delayed_script_count = 0;
708 +
709 + foreach ( $matches as $match ) {
710 + $script = $match[0];
711 + $attributes = $match[1];
712 + $content = $match[2];
713 +
714 + $is_delay_skipped = function_exists( 'is_user_logged_in' ) && is_user_logged_in();
715 +
716 + /**
717 + * Whether skip or not skip js for delay
718 + *
719 + * @hook powered_cache_delayed_js_skip
720 + *
721 + * @param {bool} Depends on logged-in status by default.
722 + *
723 + * @return {bool} New value.
724 + * @since 3.0
725 + */
726 + if ( apply_filters( 'powered_cache_delayed_js_skip', $is_delay_skipped, $script, $attributes, $content ) ) {
727 + continue;
728 + }
729 +
730 + if ( Helper::is_delay_excluded( $script ) ) {
731 + continue;
732 + }
733 +
734 + if ( ! preg_match( '/type=/', $script ) || preg_match( '/type=[\'"]text\/javascript[\'"]/', $script ) ) {
735 + $new_script = $script;
736 +
737 + // Check if script has a src attribute
738 + if ( strpos( $attributes, 'src=' ) !== false ) {
739 + $new_script = preg_replace( '/<script([^>]*)>/', '<script type="pc-delayed-js"$1>', $new_script );
740 + } else {
741 + $new_script = preg_replace( '/<script([^>]*)>/', '<script type="pc-delayed-js"$1>', $new_script );
742 + }
743 +
744 + $html = str_replace( $script, $new_script, $html );
745 + $delayed_script_count ++;
746 + }
747 + }
748 +
749 + if ( 0 === $delayed_script_count ) {
750 + return $html;
751 + }
752 +
753 + /**
754 + * Filter delay time for JS execution fallback
755 + *
756 + * @hook powered_cache_delayed_js_timeout
757 + *
758 + * @param {int} $delay_in_ms Delay time in milliseconds
759 + *
760 + * @return {int} New value.
761 + * @since 3.0
762 + */
763 + $delay_timeout = apply_filters( 'powered_cache_delayed_js_timeout', $this->settings['js_delay_timeout'] );
764 + $script_path = POWERED_CACHE_PATH . 'dist/js/script-loader.js';
765 + $script_content = file_get_contents( $script_path ); // phpcs:ignore
766 +
767 + if ( ! $script_content ) {
768 + return $html;
769 + }
770 +
771 + $head_pos = strpos( $html, '</head>' );
772 +
773 + if ( false === $head_pos ) { // bail if no head tag
774 + return $html;
775 + }
776 +
777 + $delay_js_script_content = '<script id="powered-cache-delayed-js">' . $script_content . '</script>' . PHP_EOL;
778 +
779 + /**
780 + * Delayed JS script content
781 + *
782 + * @hook powered_cache_delayed_js_script_content
783 + *
784 + * @param {string} $delay_js_script_content Delayed JS script content
785 + * @param {string} $script_path Script path
786 + * @param {string} $html HTML buffer
787 + * @param {int} $delay_timeout Delay time in milliseconds
788 + *
789 + * @return {string} New value.
790 + * @since 3.4
791 + */
792 + $delay_js_script_content = apply_filters( 'powered_cache_delayed_js_script_content', $delay_js_script_content, $script_path, $html, $delay_timeout );
793 +
794 + $html = substr_replace( $html, $delay_js_script_content, $head_pos, 0 );
795 +
796 + $script_loader = PHP_EOL . '<script id="powered-cache-delayed-script-loader">' . PHP_EOL;
797 + $script_loader .= 'console.log("[Powered Cache] - Script(s) will be loaded with delay or interaction");' . PHP_EOL;
798 + $script_loader .= 'window.PCScriptLoaderTimeout=' . absint( $delay_timeout ) . ';' . PHP_EOL;
799 +
800 + $script_loader .= 'Defer.all(\'script[type="pc-delayed-js"]\', window.PCScriptLoaderTimeout, true);' . PHP_EOL;
801 +
802 + // Dispatch DOMContentLoaded event after delayed scripts are loaded
803 + // This ensures scripts that listen for DOMContentLoaded still execute
804 + $script_loader .= '(function(){' . PHP_EOL;
805 + $script_loader .= ' var pcDelayedScripts=document.querySelectorAll(\'script[type="pc-delayed-js"]\');' . PHP_EOL;
806 + $script_loader .= ' if(pcDelayedScripts.length===0)return;' . PHP_EOL;
807 + $script_loader .= ' var pcScriptCount=pcDelayedScripts.length;' . PHP_EOL;
808 + $script_loader .= ' var pcCheckInterval=setInterval(function(){' . PHP_EOL;
809 + $script_loader .= ' var remaining=document.querySelectorAll(\'script[type="pc-delayed-js"]\').length;' . PHP_EOL;
810 + $script_loader .= ' if(remaining===0){' . PHP_EOL;
811 + $script_loader .= ' clearInterval(pcCheckInterval);' . PHP_EOL;
812 + $script_loader .= ' setTimeout(function(){' . PHP_EOL;
813 + $script_loader .= ' if(document.readyState==="complete"||document.readyState==="interactive"){' . PHP_EOL;
814 + $script_loader .= ' var event=document.createEvent?document.createEvent("Event"):new Event("DOMContentLoaded");' . PHP_EOL;
815 + $script_loader .= ' if(document.createEvent){event.initEvent("DOMContentLoaded",true,true);}' . PHP_EOL;
816 + $script_loader .= ' document.dispatchEvent(event);' . PHP_EOL;
817 + $script_loader .= ' console.log("[Powered Cache] - DOMContentLoaded event dispatched for "+pcScriptCount+" delayed script(s)");' . PHP_EOL;
818 + $script_loader .= ' }' . PHP_EOL;
819 + $script_loader .= ' },10);' . PHP_EOL;
820 + $script_loader .= ' }' . PHP_EOL;
821 + $script_loader .= ' },50);' . PHP_EOL;
822 + $script_loader .= '})();' . PHP_EOL;
823 +
824 + $script_loader .= '</script>' . PHP_EOL;
825 +
826 + /**
827 + * Delayed JS script loader content
828 + *
829 + * @hook powered_cache_delayed_js_script_loader
830 + *
831 + * @param {string} $script_loader Delayed JS script loader content
832 + * @param {string} $html HTML buffer
833 + * @param {int} $delay_timeout Delay time in milliseconds
834 + *
835 + * @return {string} New value.
836 + * @since 3.4
837 + */
838 + $script_loader = apply_filters( 'powered_cache_delayed_js_script_loader', $script_loader, $html, $delay_timeout );
839 +
840 + $body_pos = strpos( $html, '</body>' );
841 +
842 + if ( false !== $body_pos ) {
843 + $html = substr_replace( $html, $script_loader, $body_pos, 0 );
844 + } else {
845 + $html_pos = strpos( $html, '</html>' );
846 + if ( false !== $html_pos ) {
847 + $html = substr_replace( $html, $script_loader, $html_pos, 0 );
848 + }
849 + }
850 +
851 + return $html;
852 + }
853 +
854 + /**
855 + * Defer jQuery depended inline scripts
856 + *
857 + * @param string $html Output buffer
858 + *
859 + * @return array|mixed|string|string[]|null
860 + * @since 3.2
861 + */
862 + public function maybe_defer_inline_scripts( $html ) {
863 + if ( ! $this->settings['js_defer'] ) {
864 + return $html;
865 + }
866 +
867 + $html = preg_replace_callback(
868 + '/(<script[^>]*>)(.*?)<\/script>/s',
869 + function ( $matches ) {
870 + $script_open_tag = $matches[1];
871 + $script_content = $matches[2];
872 +
873 + if ( ! $this->should_defer_script( $script_content, $script_open_tag ) ) {
874 + return $matches[0]; // Return the script unchanged
875 + }
876 +
877 + /**
878 + * Filters whether disable or not disable inline defer
879 + *
880 + * @hook powered_cache_disable_js_defer_inline
881 + *
882 + * @param {boolean} true to disable defer
883 + *
884 + * @return {boolean} New value.
885 + * @since 3.2
886 + */
887 + if ( apply_filters( 'powered_cache_disable_js_defer_inline', false, $script_content, $script_open_tag ) ) {
888 + return $matches[0]; // Return the script unchanged
889 + }
890 +
891 + return $script_open_tag . 'window.addEventListener("DOMContentLoaded", function() {(function($) {' . $script_content . '})(jQuery);});</script>';
892 + },
893 + $html
894 + );
895 +
896 + return $html;
897 + }
898 +
899 + /**
900 + * Determine whether defer or not defer inline script
901 + *
902 + * @param string $script_content Script content
903 + * @param string $script_attributes Script attributes
904 + *
905 + * @return bool
906 + * @since 3.2
907 + */
908 + private function should_defer_script( $script_content, $script_attributes ) {
909 + // Ignore scripts with a 'src' attribute (external scripts)
910 + if ( false !== strpos( $script_attributes, 'src=' ) ) {
911 + return false;
912 + }
913 +
914 + // ignore ld+json
915 + if ( false !== strpos( $script_attributes, 'type="application/ld+json"' ) ) {
916 + return false;
917 + }
918 +
919 + // Check if the script contains 'DOMContentLoaded' or 'document.write'
920 + if ( false !== strpos( $script_content, 'DOMContentLoaded' ) || false !== strpos( $script_content, 'document.write' ) ) {
921 + return false;
922 + }
923 +
924 + // Check if the script does NOT contain jQuery-related functions
925 + if ( false === strpos( $script_content, 'jQuery(' ) && false === strpos( $script_content, '$.(' ) && false === strpos( $script_content, '$(' ) ) {
926 + return false;
927 + }
928 +
929 + return true;
930 + }
931 +
527 932
528 933 }