PluginProbe
ezCache / 2.6.4
ezCache v2.6.4
2.6.5 2.6.4 2.6.2 2.6.3 2.6.1 2.6.0 2.5.6 2.5.5 2.5.4 2.5.3 2.5.2 2.5.1 2.5 2.2.1 2.2.2 trunk 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.3 1.3.1 1.3.10 1.3.11 All 49 releases
ezcache / includes / Optimizations.php

Optimizations.php in ezCache 2.6.4, at includes/Optimizations.php

234 lines 6.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Upress\EzCache;
4
5 /**
6 * Front-end performance optimizations inspired by WP Rocket.
7 *
8 * Each optimization is opt-in via the plugin settings. Heavy lifting that
9 * needs to alter the HTML of cached pages happens through buffer filters that
10 * run before the cache file is written, so the work only happens once per
11 * cache generation.
12 */
13 class Optimizations {
14
15 /** @var object */
16 protected $settings;
17
18 public function __construct() {
19 $this->settings = Settings::get_settings();
20
21 add_action( 'init', [ $this, 'maybe_control_heartbeat' ], 1 );
22 add_filter( 'heartbeat_settings', [ $this, 'filter_heartbeat_settings' ] );
23
24 add_action( 'wp_head', [ $this, 'output_resource_hints' ], 1 );
25
26 add_filter( 'ezcache_before_save_cache', [ $this, 'lazy_load_html' ], 20 );
27 add_filter( 'ezcache_before_save_cache', [ $this, 'remove_query_strings' ], 30 );
28 add_filter( 'ezcache_before_save_cache', [ $this, 'rewrite_cdn_urls' ], 40 );
29 add_filter( 'ezcache_before_save_cache', [ $this, 'defer_js' ], 50 );
30 }
31
32 /**
33 * Disable or throttle the WP Heartbeat API based on settings.
34 */
35 public function maybe_control_heartbeat() {
36 if ( empty( $this->settings->heartbeat_control ) ) {
37 return;
38 }
39
40 $mode = isset( $this->settings->heartbeat_mode ) ? $this->settings->heartbeat_mode : 'reduce';
41
42 if ( 'disable' === $mode ) {
43 wp_deregister_script( 'heartbeat' );
44 }
45 }
46
47 /**
48 * Slow the heartbeat tick interval down (default 60s) when "reduce" mode is on.
49 *
50 * @param array $settings
51 * @return array
52 */
53 public function filter_heartbeat_settings( $settings ) {
54 if ( empty( $this->settings->heartbeat_control ) ) {
55 return $settings;
56 }
57
58 $mode = isset( $this->settings->heartbeat_mode ) ? $this->settings->heartbeat_mode : 'reduce';
59
60 if ( 'reduce' === $mode ) {
61 $settings['interval'] = 60;
62 $settings['minimalInterval'] = 60;
63 }
64
65 return $settings;
66 }
67
68 /**
69 * Output `<link rel="dns-prefetch">` and `<link rel="preconnect">` tags
70 * configured by the user.
71 */
72 public function output_resource_hints() {
73 if ( ! empty( $this->settings->dns_prefetch ) ) {
74 $hosts = preg_split( "/\r\n|\r|\n/", trim( $this->settings->dns_prefetch ), -1, PREG_SPLIT_NO_EMPTY );
75 foreach ( $hosts as $host ) {
76 $host = esc_url( trim( $host ) );
77 if ( $host ) {
78 echo "<link rel='dns-prefetch' href='" . esc_attr( $host ) . "'>\n";
79 }
80 }
81 }
82
83 if ( ! empty( $this->settings->preconnect ) ) {
84 $hosts = preg_split( "/\r\n|\r|\n/", trim( $this->settings->preconnect ), -1, PREG_SPLIT_NO_EMPTY );
85 foreach ( $hosts as $host ) {
86 $host = esc_url( trim( $host ) );
87 if ( $host ) {
88 echo "<link rel='preconnect' href='" . esc_attr( $host ) . "' crossorigin>\n";
89 }
90 }
91 }
92 }
93
94 /**
95 * Add native lazy-loading to images and iframes that don't already opt out.
96 *
97 * @param string $html
98 * @return string
99 */
100 public function lazy_load_html( $html ) {
101 if ( empty( $this->settings->lazy_load_images ) && empty( $this->settings->lazy_load_iframes ) ) {
102 return $html;
103 }
104
105 if ( ! empty( $this->settings->lazy_load_images ) ) {
106 $html = preg_replace_callback(
107 '#<img\b([^>]*)>#i',
108 function ( $match ) {
109 $attrs = $match[1];
110 if ( false !== stripos( $attrs, 'data-no-lazy' ) || preg_match( '/loading\s*=\s*["\'][^"\']*["\']/i', $attrs ) ) {
111 return $match[0];
112 }
113 return '<img loading="lazy" decoding="async"' . $attrs . '>';
114 },
115 $html
116 );
117 }
118
119 if ( ! empty( $this->settings->lazy_load_iframes ) ) {
120 $html = preg_replace_callback(
121 '#<iframe\b([^>]*)>#i',
122 function ( $match ) {
123 $attrs = $match[1];
124 if ( false !== stripos( $attrs, 'data-no-lazy' ) || preg_match( '/loading\s*=\s*["\'][^"\']*["\']/i', $attrs ) ) {
125 return $match[0];
126 }
127 return '<iframe loading="lazy"' . $attrs . '>';
128 },
129 $html
130 );
131 }
132
133 return $html;
134 }
135
136 /**
137 * Remove the `?ver=` query string from local static asset URLs to improve
138 * proxy/CDN cacheability.
139 *
140 * @param string $html
141 * @return string
142 */
143 public function remove_query_strings( $html ) {
144 if ( empty( $this->settings->remove_query_strings ) ) {
145 return $html;
146 }
147
148 $home_host = preg_quote( wp_parse_url( home_url(), PHP_URL_HOST ), '#' );
149
150 return preg_replace_callback(
151 '#(["\'])((?:https?:)?//' . $home_host . '/[^"\']+\.(?:css|js))\?[^"\']*\1#i',
152 function ( $match ) {
153 return $match[1] . $match[2] . $match[1];
154 },
155 $html
156 );
157 }
158
159 /**
160 * Rewrite local static URLs to point at the configured CDN host.
161 *
162 * @param string $html
163 * @return string
164 */
165 public function rewrite_cdn_urls( $html ) {
166 if ( empty( $this->settings->cdn_enabled ) || empty( $this->settings->cdn_url ) ) {
167 return $html;
168 }
169
170 $cdn = untrailingslashit( $this->settings->cdn_url );
171 $cdn = preg_replace( '#^https?:#', '', $cdn );
172
173 $home_url = home_url();
174 $home_no_proto = preg_replace( '#^https?:#', '', $home_url );
175 $home_no_proto = rtrim( $home_no_proto, '/' );
176
177 // Rewrite /wp-content/ and /wp-includes/ URLs.
178 $pattern = '#(["\'(=\s])((?:https?:)?' . preg_quote( $home_no_proto, '#' ) . ')?(/wp-content/(?:uploads|themes|plugins)/[^"\'\s)]+\.(?:jpg|jpeg|png|gif|webp|svg|css|js|woff|woff2|ttf|eot|ico))#i';
179
180 return preg_replace_callback(
181 $pattern,
182 function ( $match ) use ( $cdn ) {
183 return $match[1] . $cdn . $match[3];
184 },
185 $html
186 );
187 }
188
189 /**
190 * Add `defer` to local script tags to reduce render-blocking JS.
191 *
192 * @param string $html
193 * @return string
194 */
195 public function defer_js( $html ) {
196 if ( empty( $this->settings->defer_js ) ) {
197 return $html;
198 }
199
200 $excluded = [ 'jquery-core', 'jquery-migrate', 'jquery.js' ];
201 if ( ! empty( $this->settings->defer_js_exclusions ) ) {
202 $excluded = array_merge(
203 $excluded,
204 preg_split( "/\r\n|\r|\n/", trim( $this->settings->defer_js_exclusions ), -1, PREG_SPLIT_NO_EMPTY )
205 );
206 }
207
208 return preg_replace_callback(
209 '#<script\b([^>]*?)src=(["\'])([^"\']+)\2([^>]*)></script>#i',
210 function ( $match ) use ( $excluded ) {
211 $attrs_before = $match[1];
212 $src = $match[3];
213 $attrs_after = $match[4];
214 $all_attrs = $attrs_before . ' ' . $attrs_after;
215
216 // Already deferred / async / module.
217 if ( preg_match( '/\b(defer|async|type\s*=\s*["\']?module)\b/i', $all_attrs ) ) {
218 return $match[0];
219 }
220
221 foreach ( $excluded as $needle ) {
222 $needle = trim( $needle );
223 if ( '' !== $needle && false !== strpos( $src, $needle ) ) {
224 return $match[0];
225 }
226 }
227
228 return '<script' . $attrs_before . 'defer src=' . $match[2] . $src . $match[2] . $attrs_after . '></script>';
229 },
230 $html
231 );
232 }
233 }
234