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 +475 -45 2.0.2trunk View file →
@@ -6,8 +6,13 @@
6 6 */
7 7
8 8 namespace PoweredCache;
9 9
10 +use PoweredCache\Dependencies\voku\helper\HtmlMin;
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;
14 +use const PoweredCache\Constants\POST_META_DISABLE_JS_OPTIMIZATION;
10 15 use PoweredCache\Optimizer\CSS;
11 16 use PoweredCache\Optimizer\Helper;
12 17 use PoweredCache\Optimizer\JS;
13 18 use function PoweredCache\Utils\get_cache_dir;
@@ -54,8 +59,22 @@
54 59 * Setup routine
55 60 */
56 61 public function setup() {
57 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() {
58 77 /**
59 78 * Filters whether apply or not apply file optimizations on wp-admin.
60 79 *
61 80 * @hook powered_cache_fo_dashboard
@@ -77,12 +96,41 @@
77 96 if ( is_admin() && ! $this->optimize_dashboard ) {
78 97 return;
79 98 }
80 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 +
81 125 add_action( 'init', [ $this, 'setup_css_combine' ] );
82 126 add_action( 'init', [ $this, 'setup_js_combine' ] );
83 127 add_filter( 'script_loader_tag', [ $this, 'js_minify' ], 10, 3 );
84 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' ] );
85 133
86 134 if ( ! $this->settings['combine_js'] ) {
87 135 add_filter( 'powered_cache_fo_js_do_concat', '__return_false' );
88 136 }
@@ -90,24 +138,122 @@
90 138 if ( ! $this->settings['combine_css'] ) {
91 139 add_filter( 'powered_cache_fo_css_do_concat', '__return_false' );
92 140 }
93 141
94 - 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 + }
95 146
96 - if ( ! $this->settings['js_execution_optimized_only'] ) {
97 - 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;
98 155 }
156 + $request_uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) );
99 157
100 - add_action( 'after_setup_theme', [ $this, 'html_minify' ] );
158 + if ( strpos( $request_uri, 'robots.txt' ) !== false || strpos( $request_uri, '.htaccess' ) !== false ) {
159 + return;
160 + }
101 161
102 - if ( $this->settings['combine_google_fonts'] ) {
103 - add_action( 'wp_enqueue_scripts', [ $this, 'combine_google_fonts' ], 99 );
162 + if ( ! isset( $_SERVER['REQUEST_METHOD'] ) || 'GET' !== $_SERVER['REQUEST_METHOD'] ) {
163 + return;
104 164 }
105 - add_action( 'wp_enqueue_scripts', [ $this, 'combine_google_fonts' ], 99 );
106 165
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' ] );
107 178 }
108 179
109 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 );
192 +
193 + return $html;
194 + }
195 +
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 + /**
223 + * Maybe suppress optimizations based on the post meta options
224 + *
225 + * @since 2.1
226 + */
227 + public function maybe_suppress_optimizations() {
228 + if ( is_singular() ) {
229 + $disable_css_optimization = (bool) get_post_meta( get_the_ID(), POST_META_DISABLE_CSS_OPTIMIZATION, true );
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 );
233 +
234 + if ( $disable_css_optimization ) {
235 + add_filter( 'powered_cache_fo_css_do_concat', '__return_false' );
236 + add_filter( 'powered_cache_fo_disable_css_minify', '__return_true' );
237 + }
238 +
239 + if ( $disable_js_optimization ) {
240 + add_filter( 'powered_cache_fo_js_do_concat', '__return_false' );
241 + add_filter( 'powered_cache_fo_disable_js_minify', '__return_true' );
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 + }
252 + }
253 + }
254 +
255 + /**
110 256 * Purge FO cache
111 257 *
112 258 * @since 2.0
113 259 */
@@ -117,15 +263,23 @@
117 263 }
118 264 }
119 265
120 266 /**
121 - * Minify HTML output
267 + * Minify given HTML output
268 + *
269 + * @param string $buffer HTML
270 + *
271 + * @return string|string[]|null
122 272 */
123 - public function html_minify() {
273 + public function maybe_minify_html( $buffer ) {
124 274 if ( ! $this->settings['minify_html'] ) {
125 - return;
275 + return $buffer;
126 276 }
127 277
278 + if ( false === stripos( $buffer, '<html' ) && false === stripos( $buffer, '<!DOCTYPE html' ) ) {
279 + return $buffer;
280 + }
281 +
128 282 /**
129 283 * Filters whether disable or not disable HTML minification.
130 284 *
131 285 * @hook powered_cache_fo_disable_html_minify
@@ -135,32 +289,25 @@
135 289 * @return {boolean} New value.
136 290 * @since 2.0
137 291 */
138 292 if ( apply_filters( 'powered_cache_fo_disable_html_minify', false ) ) {
139 - return;
293 + return $buffer;
140 294 }
141 295
142 - ob_start( [ $this, 'run_html_minify' ] );
143 - }
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 );
144 302
145 - /**
146 - * Minify given HTML output
147 - *
148 - * @param string $buffer HTML
149 - *
150 - * @return string|string[]|null
151 - */
152 - public function run_html_minify( $buffer ) {
153 - $search = array(
154 - '/\>[^\S ]+/s', // strip whitespaces after tags, except space
155 - '/[^\S ]+\</s', // strip whitespaces before tags, except space
156 - '/(\s)+/s', // shorten multiple whitespace sequences
157 - '/^\\s+|\\s+$/', // shorten multiple whitespace sequences
158 - );
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 + }
159 309
160 - $replace = array( '>', '<', '\\1', ' ' );
161 - $buffer = preg_replace( $search, $replace, $buffer );
162 -
163 310 return $buffer;
164 311 }
165 312
166 313
@@ -171,29 +318,40 @@
171 318 *
172 319 * @return mixed
173 320 */
174 321 public function change_js_execute_method( $tag ) {
175 - $js_execution = $this->settings['js_execution_method'];
176 - if ( ! $js_execution ) {
322 + $is_defer = $this->settings['js_defer'];
323 +
324 + if ( ! $is_defer ) {
177 325 return $tag;
178 326 }
179 327
180 - if ( 'blocking' === $js_execution ) {
328 + if ( preg_match( '/<script\s+[^>]*\b(?:async|defer)\b[^>]*>/i', $tag ) ) {
181 329 return $tag;
182 330 }
183 331
184 - if ( 'async' === $js_execution ) {
185 - $js_attr = 'async="async"';
186 - } elseif ( 'defer' === $js_execution ) {
187 - $js_attr = 'defer="defer"';
332 + if ( Helper::is_defer_excluded( $tag ) ) {
333 + return $tag;
188 334 }
189 335
190 - if ( false === stripos( $tag, $js_attr ) ) {
191 - $search = '<script ';
192 - $replace = sprintf( '<script %s ', $js_attr );
193 - $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;
194 348 }
195 349
350 + $search = '<script ';
351 + $replace = '<script defer="defer" ';
352 + $tag = str_replace( $search, $replace, $tag );
353 +
196 354 return $tag;
197 355 }
198 356
199 357 /**
@@ -272,13 +430,16 @@
272 430 *
273 431 * @hook powered_cache_fo_disable_js_minify
274 432 *
275 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
276 437 *
277 438 * @return {boolean} New value.
278 439 * @since 2.0
279 440 */
280 - if ( apply_filters( 'powered_cache_fo_disable_js_minify', false ) ) {
441 + if ( apply_filters( 'powered_cache_fo_disable_js_minify', false, $tag, $handle, $src ) ) {
281 442 return $tag;
282 443 }
283 444
284 445 // only minify static .css
@@ -404,9 +565,9 @@
404 565 }
405 566
406 567 $style_src = $wp_styles->registered[ $handle ]->src;
407 568
408 - 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' ) ) {
409 570 $url = wp_parse_url( $style_src );
410 571
411 572 if ( is_string( $url['query'] ) ) {
412 573 parse_str( $url['query'], $parsed_url );
@@ -469,8 +630,17 @@
469 630 $font_args['subset'] = implode( ',', $subsets );
470 631 }
471 632
472 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 + /**
473 643 * Filters font display attribute of the google fonts.
474 644 *
475 645 * @hook powered_cache_fo_google_font_display
476 646 *
@@ -475,9 +645,9 @@
475 645 * @hook powered_cache_fo_google_font_display
476 646 *
477 647 * @param {string} $font_display font display attribute.
478 648 *
479 - * @return {boolean} New value.
649 + * @return {string} New value.
480 650 * @since 2.0
481 651 */
482 652 $font_display = apply_filters( 'powered_cache_fo_google_font_display', $font_display );
483 653
@@ -484,10 +654,22 @@
484 654 if ( ! empty( $font_display ) ) {
485 655 $font_args['display'] = $font_display;
486 656 }
487 657
488 - $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 );
489 669
670 + $src = esc_url_raw( add_query_arg( $font_args, $fonts_domain ) );
671 +
490 672 // Enqueue google fonts into one URL request
491 673 wp_enqueue_style( // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
492 674 'pc-google-fonts-' . md5( $src ),
493 675 $src,
@@ -498,6 +680,254 @@
498 680 }
499 681 }
500 682 }
501 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 +
502 932
503 933 }