PluginProbe
Elementor Website Builder – more than just a page builder / 3.21.0-beta2
Elementor Website Builder – more than just a page builder v3.21.0-beta2
4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 4.1.0-beta1 4.1.0-dev1 4.0.7 All 451 releases
elementor / modules / image-loading-optimization / module.php

module.php in Elementor Website Builder – more than just a page builder 3.21.0-beta2, at modules/image-loading-optimization/module.php

350 lines 10.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Elementor\Modules\ImageLoadingOptimization;
3
4 use Elementor\Core\Base\Module as BaseModule;
5 use Elementor\Core\Experiments\Manager as Experiments_Manager;
6
7 if ( ! defined( 'ABSPATH' ) ) {
8 exit; // Exit if accessed directly
9 }
10
11 class Module extends BaseModule {
12 /**
13 * @var int Minimum square-pixels threshold.
14 */
15 private $min_priority_img_pixels = 50000;
16
17 /**
18 * @var int The number of content media elements to not lazy-load.
19 */
20 private $omit_threshold = 3;
21
22 /**
23 * @var array Keep a track of images for which loading optimization strategy were computed.
24 */
25 private static $image_visited = [];
26
27 /**
28 * Get Module name.
29 */
30 public function get_name() {
31 return 'image-loading-optimization';
32 }
33
34 /**
35 * Constructor.
36 */
37 public function __construct() {
38 parent::__construct();
39
40 // Stop wp core logic.
41 add_action( 'init', [ $this, 'stop_core_fetchpriority_high_logic' ] );
42 add_filter( 'wp_lazy_loading_enabled', '__return_false' );
43
44 // Run optimization logic on header.
45 add_action( 'get_header', [ $this, 'set_buffer' ] );
46
47 // Ensure buffer is flushed (if any) before the content logic.
48 add_filter( 'the_content', [ $this, 'flush_header_buffer' ], 0 );
49
50 // Run optimization logic on content.
51 add_filter( 'wp_content_img_tag', [ $this, 'loading_optimization_image' ] );
52 }
53
54 /**
55 * Stop WordPress core fetchpriority logic by setting the wp_high_priority_element_flag flag to false.
56 */
57 public function stop_core_fetchpriority_high_logic() {
58 // wp_high_priority_element_flag was only introduced in 6.3.0
59 if ( function_exists( 'wp_high_priority_element_flag' ) ) {
60 wp_high_priority_element_flag( false );
61 }
62 }
63
64 /**
65 * Set buffer to handle header and footer content.
66 */
67 public function set_buffer() {
68 ob_start( [ $this, 'handle_buffer_content' ] );
69 }
70
71 /**
72 * This function ensure that buffer if any is flushed before the content is called.
73 * This function behaves more like an action than a filter.
74 *
75 * @param string $content the content.
76 * @return string We simply return the content from parameter.
77 */
78 public function flush_header_buffer( $content ) {
79 $buffer_status = ob_get_status();
80
81 if ( ! empty( $buffer_status ) &&
82 1 === $buffer_status['type'] &&
83 get_class( $this ) . '::handle_buffer_content' === $buffer_status['name'] ) {
84 ob_end_flush();
85 }
86
87 return $content;
88 }
89
90 /**
91 * Callback to handle image optimization logic on buffered content.
92 *
93 * @param string $buffer Buffered content.
94 * @return string Content with optimized images.
95 */
96 public function handle_buffer_content( $buffer ) {
97 return $this->filter_images( $buffer );
98 }
99
100 /**
101 * Check for image in the content provided and apply optimization logic on them.
102 *
103 * @param string $content Content to be analyzed.
104 * @return string Content with optimized images.
105 */
106 private function filter_images( $content ) {
107 return preg_replace_callback(
108 '/<img\s[^>]+>/',
109 function ( $matches ) {
110 return $this->loading_optimization_image( $matches[0] );
111 },
112 $content
113 );
114 }
115
116 /**
117 * Apply loading optimization logic on the image.
118 *
119 * @param mixed $image Original image tag.
120 * @return string Optimized image.
121 */
122 public function loading_optimization_image( $image ) {
123 if ( isset( self::$image_visited[ $image ] ) ) {
124 return self::$image_visited[ $image ];
125 }
126
127 $optimized_image = $this->add_loading_optimization_attrs( $image );
128 self::$image_visited[ $image ] = $optimized_image;
129
130 return $optimized_image;
131 }
132
133 /**
134 * Adds optimization attributes to an `img` HTML tag.
135 *
136 * @param string $image The HTML `img` tag where the attribute should be added.
137 * @return string Converted `img` tag with optimization attributes added.
138 */
139 private function add_loading_optimization_attrs( $image ) {
140 $width = preg_match( '/ width=["\']([0-9]+)["\']/', $image, $match_width ) ? (int) $match_width[1] : null;
141 $height = preg_match( '/ height=["\']([0-9]+)["\']/', $image, $match_height ) ? (int) $match_height[1] : null;
142 $loading_val = preg_match( '/ loading=["\']([A-Za-z]+)["\']/', $image, $match_loading ) ? $match_loading[1] : null;
143 $fetchpriority_val = preg_match( '/ fetchpriority=["\']([A-Za-z]+)["\']/', $image, $match_fetchpriority ) ? $match_fetchpriority[1] : null;
144
145 // Images should have height and dimension width for the loading optimization attributes to be added.
146 if ( ! str_contains( $image, ' width="' ) || ! str_contains( $image, ' height="' ) ) {
147 return $image;
148 }
149
150 $optimization_attrs = $this->get_loading_optimization_attributes(
151 [
152 'width' => $width,
153 'height' => $height,
154 'loading' => $loading_val,
155 'fetchpriority' => $fetchpriority_val,
156 ]
157 );
158
159 if ( ! empty( $optimization_attrs['fetchpriority'] ) ) {
160 $image = str_replace( '<img', '<img fetchpriority="' . esc_attr( $optimization_attrs['fetchpriority'] ) . '"', $image );
161 }
162
163 if ( ! empty( $optimization_attrs['loading'] ) ) {
164 $image = str_replace( '<img', '<img loading="' . esc_attr( $optimization_attrs['loading'] ) . '"', $image );
165 }
166
167 return $image;
168 }
169
170 /**
171 * Return loading Loading optimization attributes for a image with give attribute.
172 *
173 * @param array $attr Existing image attributes.
174 * @return array Loading optimization attributes.
175 */
176 private function get_loading_optimization_attributes( $attr ) {
177 $loading_attrs = [];
178
179 // For any resources, width and height must be provided, to avoid layout shifts.
180 if ( ! isset( $attr['width'], $attr['height'] ) ) {
181 return $loading_attrs;
182 }
183
184 /*
185 * The key function logic starts here.
186 */
187 $maybe_in_viewport = null;
188 $increase_count = false;
189 $maybe_increase_count = false;
190
191 /*
192 * Logic to handle a `loading` attribute that is already provided.
193 *
194 * Copied from `wp_get_loading_optimization_attributes()`.
195 */
196 if ( isset( $attr['loading'] ) ) {
197 /*
198 * Interpret "lazy" as not in viewport. Any other value can be
199 * interpreted as in viewport (realistically only "eager" or `false`
200 * to force-omit the attribute are other potential values).
201 */
202 if ( 'lazy' === $attr['loading'] ) {
203 $maybe_in_viewport = false;
204 } else {
205 $maybe_in_viewport = true;
206 }
207 }
208
209 // Logic to handle a `fetchpriority` attribute that is already provided.
210 $has_fetchpriority_high_attr = ( isset( $attr['fetchpriority'] ) && 'high' === $attr['fetchpriority'] );
211
212 /*
213 * Handle cases where a `fetchpriority="high"` has already been set.
214 *
215 * Copied from `wp_get_loading_optimization_attributes()`.
216 */
217 if ( $has_fetchpriority_high_attr ) {
218 /*
219 * If the image was already determined to not be in the viewport (e.g.
220 * from an already provided `loading` attribute), trigger a warning.
221 * Otherwise, the value can be interpreted as in viewport, since only
222 * the most important in-viewport image should have `fetchpriority` set
223 * to "high".
224 */
225 if ( false === $maybe_in_viewport ) {
226 _doing_it_wrong(
227 __FUNCTION__,
228 esc_html__( 'An image should not be lazy-loaded and marked as high priority at the same time.', 'elementor' ),
229 ''
230 );
231 /*
232 * Set `fetchpriority` here for backward-compatibility as we should
233 * not override what a developer decided, even though it seems
234 * incorrect.
235 */
236 $loading_attrs['fetchpriority'] = 'high';
237 } else {
238 $maybe_in_viewport = true;
239 }
240 }
241
242 if ( null === $maybe_in_viewport && ! is_admin() ) {
243 $content_media_count = $this->increase_content_media_count( 0 );
244 $increase_count = true;
245 if ( $content_media_count < $this->omit_threshold ) {
246 $maybe_in_viewport = true;
247 } else {
248 $maybe_in_viewport = false;
249 }
250 }
251
252 if ( $maybe_in_viewport ) {
253 $loading_attrs = $this->maybe_add_fetchpriority_high_attr( $loading_attrs, $attr );
254 } else {
255 $loading_attrs['loading'] = 'lazy';
256 }
257
258 if ( $increase_count ) {
259 $this->increase_content_media_count();
260 } elseif ( $maybe_increase_count ) {
261 if ( $this->get_min_priority_img_pixels() <= $attr['width'] * $attr['height'] ) {
262 $this->increase_content_media_count();
263 }
264 }
265
266 return $loading_attrs;
267 }
268
269 /**
270 * Helper to get the minimum threshold for number of pixels an image needs to have to be considered "priority".
271 *
272 * @return int The minimum number of pixels (width * height). Default is 50000.
273 */
274 private function get_min_priority_img_pixels() {
275 /**
276 * Filter the minimum pixel threshold used to determine if an image should have fetchpriority="high" applied.
277 *
278 * @see https://developer.wordpress.org/reference/hooks/wp_min_priority_img_pixels/
279 *
280 * @param int $pixels The minimum number of pixels (with * height).
281 * @return int The filtered value.
282 */
283 return apply_filters( 'elementor/image-loading-optimization/min_priority_img_pixels', $this->min_priority_img_pixels );
284 }
285
286 /**
287 * Keeps a count of media image.
288 *
289 * @param int $amount Amount by which count must be increased.
290 * @return int current image count.
291 */
292 private function increase_content_media_count( $amount = 1 ) {
293 static $content_media_count = 0;
294
295 $content_media_count += $amount;
296
297 return $content_media_count;
298 }
299
300 /**
301 * Determines whether to add `fetchpriority='high'` to loading attributes.
302 *
303 * @param array $loading_attrs Array of the loading optimization attributes for the element.
304 * @param array $attr Array of the attributes for the element.
305 * @return array Updated loading optimization attributes for the element.
306 */
307 private function maybe_add_fetchpriority_high_attr( $loading_attrs, $attr ) {
308 if ( isset( $attr['fetchpriority'] ) ) {
309 if ( 'high' === $attr['fetchpriority'] ) {
310 $loading_attrs['fetchpriority'] = 'high';
311 $this->high_priority_element_flag( false );
312 }
313
314 return $loading_attrs;
315 }
316
317 // Lazy-loading and `fetchpriority="high"` are mutually exclusive.
318 if ( isset( $loading_attrs['loading'] ) && 'lazy' === $loading_attrs['loading'] ) {
319 return $loading_attrs;
320 }
321
322 if ( ! $this->high_priority_element_flag() ) {
323 return $loading_attrs;
324 }
325
326 if ( $this->get_min_priority_img_pixels() <= $attr['width'] * $attr['height'] ) {
327 $loading_attrs['fetchpriority'] = 'high';
328 $this->high_priority_element_flag( false );
329 }
330
331 return $loading_attrs;
332 }
333
334 /**
335 * Accesses a flag that indicates if an element is a possible candidate for `fetchpriority='high'`.
336 *
337 * @param bool $value Optional. Used to change the static variable. Default null.
338 * @return bool Returns true if high-priority element was marked already, otherwise false.
339 */
340 private function high_priority_element_flag( $value = null ) {
341 static $high_priority_element = true;
342
343 if ( is_bool( $value ) ) {
344 $high_priority_element = $value;
345 }
346
347 return $high_priority_element;
348 }
349 }
350