PluginProbe
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization / 4.2.12
Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization v4.2.12
4.2.13 4.2.12 4.2.11 4.2.10 4.2.9 4.2.8 4.2.7 4.2.6 4.2.5 2.5.5 2.5.6 2.5.7 3.0.0 3.0.1 3.1.0 3.1.1 3.1.2 3.1.3 3.10.0 3.11.0 3.11.1 3.11.2 3.11.3 3.12.0 3.12.1 All 134 releases
optimole-wp / assets / js / modules / srcset-detector.js

srcset-detector.js in Optimole – Optimize Images | Convert WebP & AVIF | CDN & Lazy Load | Image Optimization 4.2.12, at assets/js/modules/srcset-detector.js

650 lines 23.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Optimole Srcset Detection Module
3 * Detects images without data-opt-src (lazyload) and calculates missing srcset variations
4 * for different device types and viewport widths
5 */
6
7 import { optmlLogger } from './logger.js';
8 import { optmlDevice } from './device.js';
9
10 /**
11 * Srcset detection and calculation utilities
12 */
13 export const optmlSrcsetDetector = {
14 /**
15 * Common device breakpoints for responsive images
16 * Based on popular CSS frameworks and real-world usage patterns
17 */
18 DEVICE_BREAKPOINTS: {
19 // Mobile breakpoints
20 MOBILE_SMALL: 320, // Small mobile devices
21 MOBILE_MEDIUM: 375, // Medium mobile devices
22 MOBILE_LARGE: 425, // Large mobile devices
23
24 // Tablet breakpoints
25 TABLET_SMALL: 768, // Small tablets
26 TABLET_LARGE: 1024, // Large tablets
27
28 // Desktop breakpoints
29 DESKTOP_SMALL: 1200, // Small desktop
30 DESKTOP_MEDIUM: 1440, // Medium desktop
31 DESKTOP_LARGE: 1920, // Large desktop
32 DESKTOP_XL: 2560 // Extra large desktop
33 },
34
35 /**
36 * Device pixel ratio multipliers for high-DPI displays
37 */
38 DPR_MULTIPLIERS: [1, 2],
39
40 /**
41 * Configuration for srcset generation
42 */
43 CONFIG: {
44 // Minimum image size to consider (in pixels)
45 MIN_SIZE: 200,
46
47 // Maximum number of srcset variations per image
48 MAX_VARIATIONS: 8,
49
50 // Tolerance for existing srcset sizes (in pixels)
51 SIZE_TOLERANCE: 50
52 },
53
54 /**
55 * Configure srcset generation settings
56 * @param {Object} config - Configuration options
57 * @param {number} config.minSize - Minimum image size to consider (default: 200)
58 * @param {number} config.maxVariations - Maximum srcset variations per image (default: 8)
59 * @param {number} config.sizeTolerance - Tolerance for existing sizes (default: 50)
60 */
61 configure: function(config) {
62 if (config.minSize) this.CONFIG.MIN_SIZE = config.minSize;
63 if (config.maxVariations) this.CONFIG.MAX_VARIATIONS = config.maxVariations;
64 if (config.sizeTolerance) this.CONFIG.SIZE_TOLERANCE = config.sizeTolerance;
65
66 optmlLogger.info('Srcset detector configured:', this.CONFIG);
67 },
68
69 /**
70 * Wait for images to load and get their natural dimensions
71 * @param {NodeList} images - Collection of image elements
72 * @returns {Promise} Promise that resolves when all images are loaded
73 */
74 _waitForImagesToLoad: function(images) {
75 const imagePromises = Array.from(images).map(img => {
76 return new Promise((resolve) => {
77 // If image is already loaded
78 if (img.complete && img.naturalWidth > 0) {
79 resolve(img);
80 return;
81 }
82
83 // Wait for image to load
84 const onLoad = () => {
85 img.removeEventListener('load', onLoad);
86 img.removeEventListener('error', onError);
87 resolve(img);
88 };
89
90 const onError = () => {
91 img.removeEventListener('load', onLoad);
92 img.removeEventListener('error', onError);
93 optmlLogger.warn('Image failed to load:', img.src);
94 resolve(img); // Still resolve to continue processing
95 };
96
97 img.addEventListener('load', onLoad);
98 img.addEventListener('error', onError);
99
100 // Timeout after 5 seconds
101 setTimeout(() => {
102 img.removeEventListener('load', onLoad);
103 img.removeEventListener('error', onError);
104 optmlLogger.warn('Image load timeout:', img.src);
105 resolve(img);
106 }, 5000);
107 });
108 });
109
110 return Promise.all(imagePromises);
111 },
112
113 /**
114 * Detect all Optimole images that are NOT using lazyload (no data-opt-src)
115 * and calculate missing srcset variations
116 * @returns {Promise<Object>} Promise that resolves to object with srcset data and crop status
117 */
118 detectMissingSrcsets: async function() {
119 const missingSrcsetData = {};
120 const cropStatusData = {};
121
122 // Find all Optimole images
123 const optimoleImages = document.querySelectorAll('img[data-opt-id]');
124
125 if (optimoleImages.length === 0) {
126 optmlLogger.info('No Optimole images found for srcset analysis');
127 return { srcset: missingSrcsetData, crop: cropStatusData };
128 }
129
130 optmlLogger.info(`Found ${optimoleImages.length} Optimole images, waiting for them to load...`);
131
132 // Wait for all images to load to get accurate natural dimensions
133 const loadedImages = await this._waitForImagesToLoad(optimoleImages);
134
135 optmlLogger.info(`Loaded ${loadedImages.length} images, analyzing srcset requirements...`);
136
137 loadedImages.forEach(img => {
138 try {
139 const imageId = parseInt(img.getAttribute('data-opt-id'), 10);
140 if (isNaN(imageId)) return;
141
142 // Improved skip logic:
143 // - Include if image doesn't have data-opt-src
144 // - Include if image has data-opt-src but also has data-opt-lazy-loaded
145 // - Skip otherwise
146 const hasOptSrc = img.hasAttribute('data-opt-src');
147 const hasLazyLoaded = img.hasAttribute('data-opt-lazy-loaded');
148
149 const shouldInclude = !hasOptSrc || (hasOptSrc && hasLazyLoaded);
150
151 if (shouldInclude) {
152 const reason = !hasOptSrc ?
153 'not using lazyload' :
154 'lazyload completed (has data-opt-lazy-loaded)';
155 optmlLogger.info(`Image ${imageId} ${reason}, analyzing srcset requirements`);
156
157 // Analyze the image and calculate required srcset variations
158 const analysisResult = this._analyzeSrcsetRequirements(img, imageId);
159
160 if (analysisResult && analysisResult.srcset && analysisResult.srcset.length > 0) {
161 missingSrcsetData[imageId] = analysisResult.srcset;
162 cropStatusData[imageId] = analysisResult.requiresCropping;
163 }
164 }
165 } catch (error) {
166 optmlLogger.error('Error analyzing image for srcset:', img, error);
167 }
168 });
169
170 optmlLogger.info('Images requiring srcset variations:', Object.keys(missingSrcsetData).length);
171 optmlLogger.info('Images with crop status:', Object.keys(cropStatusData).length);
172 return { srcset: missingSrcsetData, crop: cropStatusData };
173 },
174
175 /**
176 * Analyze an image and determine what srcset variations are needed
177 * @private
178 * @param {HTMLImageElement} img - Image element to analyze
179 * @param {number} imageId - Optimole image ID
180 * @returns {Object|null} Srcset requirements data or null
181 */
182 _analyzeSrcsetRequirements: function(img, imageId) {
183 // Get current image dimensions
184 const currentWidth = img.offsetWidth || img.clientWidth;
185 const currentHeight = img.offsetHeight || img.clientHeight;
186 const naturalWidth = img.naturalWidth || 0;
187 const naturalHeight = img.naturalHeight || 0;
188
189 // Skip if we can't determine dimensions
190 if (!currentWidth || !currentHeight || !naturalWidth || !naturalHeight) {
191 optmlLogger.warn(`Skipping image ${imageId}: insufficient dimension data`);
192 return null;
193 }
194
195 optmlLogger.info(`Analyzing image ${imageId}:`, {
196 current: `${currentWidth}x${currentHeight}`,
197 natural: `${naturalWidth}x${naturalHeight}`
198 });
199
200 // Calculate aspect ratios
201 const naturalAspectRatio = naturalWidth / naturalHeight;
202 const currentAspectRatio = currentWidth / currentHeight;
203
204 // Determine if image requires cropping based on object-fit and aspect ratio difference
205 const aspectRatioDifference = Math.abs(naturalAspectRatio - currentAspectRatio);
206 const requiresCropping = this._requiresCropping(img, aspectRatioDifference, naturalAspectRatio, currentAspectRatio);
207
208 // Get current device type
209 const currentDeviceType = optmlDevice.getDeviceType();
210
211 // Calculate required sizes for different breakpoints
212 // Use current aspect ratio for srcset generation to match the rendered dimensions
213 const aspectRatioForSizing = requiresCropping ? currentAspectRatio : naturalAspectRatio;
214 const requiredSizes = this._calculateRequiredSizes(
215 currentWidth,
216 currentHeight,
217 aspectRatioForSizing,
218 currentDeviceType,
219 naturalWidth,
220 naturalHeight
221 );
222
223 // Check if current image has existing srcset
224 const existingSrcset = img.getAttribute('srcset');
225 const existingSizes = this._parseExistingSrcset(existingSrcset);
226
227 // Determine missing sizes
228 const missingSizes = this._findMissingSizes(requiredSizes, existingSizes);
229
230 if (missingSizes.length === 0) {
231 optmlLogger.info(`Image ${imageId} already has adequate srcset coverage`);
232 return null;
233 }
234
235 // Log full analysis for debugging
236 optmlLogger.info(`Image ${imageId} srcset analysis:`, {
237 currentSize: { w: currentWidth, h: currentHeight },
238 naturalSize: { w: naturalWidth, h: naturalHeight },
239 requiredSizes: requiredSizes,
240 naturalAspectRatio: Math.round(naturalAspectRatio * 1000) / 1000,
241 currentAspectRatio: Math.round(currentAspectRatio * 1000) / 1000,
242 aspectRatioDifference: Math.round(aspectRatioDifference * 1000) / 1000,
243 requiresCropping: requiresCropping,
244 aspectRatioForSizing: Math.round(aspectRatioForSizing * 1000) / 1000,
245 deviceType: currentDeviceType,
246 missingSizes: missingSizes,
247 existingSrcset: existingSrcset || null
248 });
249
250 // Additional debug logging for aspect ratio analysis
251 optmlLogger.info(`[Optimole Debug] Image ${imageId} aspect ratio analysis:`, {
252 natural: `${naturalWidth}x${naturalHeight} (${Math.round(naturalAspectRatio * 1000) / 1000}:1)`,
253 current: `${currentWidth}x${currentHeight} (${Math.round(currentAspectRatio * 1000) / 1000}:1)`,
254 difference: Math.round(aspectRatioDifference * 1000) / 1000,
255 requiresCropping: requiresCropping,
256 aspectRatioForSizing: Math.round(aspectRatioForSizing * 1000) / 1000,
257 reason: requiresCropping ? 'Aspect ratio significantly different' : 'Aspect ratios match within tolerance'
258 });
259
260 // Return both srcset data and crop status separately
261 return {
262 srcset: missingSizes.map(size => ({
263 w: size.w,
264 h: size.h,
265 d: size.dpr, // dpr -> d
266 s: size.descriptor, // descriptor -> s (srcset)
267 b: size.breakpoint // breakpoint -> b
268 })),
269 requiresCropping: requiresCropping
270 };
271 },
272
273 /**
274 * Determine if an image requires cropping based on object-fit and aspect ratio differences
275 * Requires BOTH object-fit: cover AND significant aspect ratio difference to return true
276 * @private
277 * @param {HTMLImageElement} img - Image element to analyze
278 * @param {number} aspectRatioDifference - Absolute difference between natural and current aspect ratios
279 * @param {number} naturalAspectRatio - Natural image aspect ratio
280 * @param {number} currentAspectRatio - Current displayed aspect ratio
281 * @returns {boolean} True if the image requires cropping (both conditions must be true)
282 */
283 _requiresCropping: function(img, aspectRatioDifference, naturalAspectRatio, currentAspectRatio) {
284 // First check if the image has object-fit: cover
285 let hasObjectFitCover = false;
286 try {
287 const computedStyle = window.getComputedStyle(img);
288 hasObjectFitCover = computedStyle.objectFit === 'cover';
289 if (hasObjectFitCover) {
290 optmlLogger.info(`Image has object-fit: cover`);
291 }
292 } catch (error) {
293 optmlLogger.warn('Could not get computed style for object-fit check:', error);
294 }
295
296 // Define thresholds for determining when cropping is needed
297 const ASPECT_RATIO_TOLERANCE = 0.05; // 5% tolerance for minor differences
298 const SIGNIFICANT_DIFFERENCE_THRESHOLD = 0.15; // 15% for significant differences
299
300 // If the difference is very small, no cropping needed
301 if (aspectRatioDifference <= ASPECT_RATIO_TOLERANCE) {
302 return false;
303 }
304
305 // Check aspect ratio conditions
306 let aspectRatioRequiresCropping = false;
307
308 // If the difference is significant, definitely needs cropping
309 if (aspectRatioDifference >= SIGNIFICANT_DIFFERENCE_THRESHOLD) {
310 aspectRatioRequiresCropping = true;
311 } else {
312 // For moderate differences, check if the current aspect ratio is significantly different
313 // from the natural one (indicating intentional resizing that would require cropping)
314 const ratioChange = Math.abs(currentAspectRatio - naturalAspectRatio) / naturalAspectRatio;
315
316 // If the current aspect ratio is more than 10% different from natural, likely needs cropping
317 aspectRatioRequiresCropping = ratioChange > 0.1;
318 }
319
320 // Both object-fit: cover AND aspect ratio difference must be true to require cropping
321 const requiresCropping = hasObjectFitCover && aspectRatioRequiresCropping;
322
323 if (requiresCropping) {
324 optmlLogger.info(`Image requires cropping: object-fit cover=${hasObjectFitCover}, aspect ratio=${aspectRatioRequiresCropping}`);
325 }
326
327 return requiresCropping;
328 },
329
330 /**
331 * Calculate required image sizes for different breakpoints and DPR
332 * @private
333 * @param {number} currentWidth - Current displayed width
334 * @param {number} currentHeight - Current displayed height
335 * @param {number} aspectRatio - Image aspect ratio
336 * @param {number} currentDeviceType - Current device type
337 * @param {number} naturalWidth - Natural image width
338 * @param {number} naturalHeight - Natural image height
339 * @returns {Array} Array of required size objects
340 */
341 _calculateRequiredSizes: function(currentWidth, currentHeight, aspectRatio, currentDeviceType, naturalWidth, naturalHeight) {
342 const requiredSizes = [];
343
344 // Generate responsive sizes based on common viewport widths and typical image usage
345 const responsiveSizes = this._generateResponsiveSizes(currentWidth, currentHeight, aspectRatio, naturalWidth, naturalHeight);
346
347 // Add all responsive sizes
348 responsiveSizes.forEach(size => {
349 requiredSizes.push(size);
350 });
351
352 // Remove duplicates and sort by width
353 const uniqueSizes = this._removeDuplicateSizes(requiredSizes);
354 const sortedSizes = uniqueSizes.sort((a, b) => a.w - b.w);
355
356 // Apply MAX_VARIATIONS limit to the final result with smart selection
357 if (sortedSizes.length > this.CONFIG.MAX_VARIATIONS) {
358 optmlLogger.info(`Limiting srcset variations from ${sortedSizes.length} to ${this.CONFIG.MAX_VARIATIONS}`);
359 return this._selectBestVariations(sortedSizes, this.CONFIG.MAX_VARIATIONS);
360 }
361
362 return sortedSizes;
363 },
364
365 /**
366 * Parse existing srcset attribute to understand what sizes are already available
367 * @private
368 * @param {string|null} srcset - Existing srcset attribute value
369 * @returns {Array} Array of existing size descriptors
370 */
371 _parseExistingSrcset: function(srcset) {
372 if (!srcset) return [];
373
374 const existingSizes = [];
375 const srcsetEntries = srcset.split(',').map(entry => entry.trim());
376
377 srcsetEntries.forEach(entry => {
378 const parts = entry.split(/\s+/);
379 if (parts.length >= 2) {
380 const descriptor = parts[parts.length - 1];
381
382 // Parse width descriptor (e.g., "800w")
383 if (descriptor.endsWith('w')) {
384 const width = parseInt(descriptor.slice(0, -1), 10);
385 if (!isNaN(width)) {
386 existingSizes.push({
387 w: width,
388 descriptor: descriptor,
389 url: parts.slice(0, -1).join(' ')
390 });
391 }
392 }
393 }
394 });
395
396 return existingSizes;
397 },
398
399 /**
400 * Generate responsive sizes based on the current image ratio
401 * Device-aware approach: generates sizes for different viewports while maintaining the current ratio
402 * @private
403 * @param {number} currentWidth - Current displayed width
404 * @param {number} currentHeight - Current displayed height
405 * @param {number} aspectRatio - Image aspect ratio
406 * @param {number} naturalWidth - Natural image width
407 * @param {number} naturalHeight - Natural image height
408 * @returns {Array} Array of responsive size objects
409 */
410 _generateResponsiveSizes: function(currentWidth, currentHeight, aspectRatio, naturalWidth, naturalHeight) {
411 const sizes = [];
412
413 // Calculate the current ratio: what percentage of viewport width does this image occupy?
414 const viewportWidth = window.innerWidth;
415 const currentRatio = Math.min(currentWidth / viewportWidth, 1.0);
416
417 optmlLogger.info(`Generating srcset for current ratio: ${Math.round(currentRatio * 100)}% of viewport`);
418
419 // Use configured device breakpoints
420 const breakpoints = Object.entries(this.DEVICE_BREAKPOINTS).map(([key, viewport]) => ({
421 viewport,
422 label: key.toLowerCase().replace('_', '-'),
423 dpr: this.DPR_MULTIPLIERS
424 }));
425
426 // Generate sizes for each viewport using the current ratio
427 breakpoints.forEach(bp => {
428 const baseWidth = Math.round(bp.viewport * currentRatio);
429
430 // Skip if the calculated width is too small or too large
431 if (baseWidth < this.CONFIG.MIN_SIZE || baseWidth > naturalWidth * 1.2) {
432 return;
433 }
434
435 // Generate for each DPR
436 bp.dpr.forEach(dprValue => {
437 const targetWidth = Math.round(baseWidth * dprValue);
438 const targetHeight = Math.round(targetWidth / aspectRatio);
439
440 // Don't generate 1x DPR variations larger than natural image size
441 // But allow 2x DPR variations for retina displays
442 // Fixes: srcset capped at profiling container width (issue #1030)
443 if (dprValue === 1 && targetWidth > naturalWidth) {
444 return;
445 }
446
447 // Only include if within reasonable bounds
448 if (this._isValidSize(targetWidth, targetHeight, naturalWidth, naturalHeight) &&
449 targetWidth >= this.CONFIG.MIN_SIZE) {
450
451 sizes.push({
452 w: targetWidth,
453 h: targetHeight,
454 dpr: dprValue,
455 breakpoint: bp.viewport,
456 descriptor: `${targetWidth}w`,
457 source: 'responsive',
458 category: `${bp.label}-${Math.round(currentRatio * 100)}`,
459 label: `${bp.label}-${targetWidth}w${dprValue > 1 ? `-${dprValue}x` : ''}`
460 });
461 }
462 });
463 });
464
465 // Add current size if not already covered
466 const currentExists = sizes.some(size =>
467 Math.abs(size.w - currentWidth) <= this.CONFIG.SIZE_TOLERANCE
468 );
469
470 if (!currentExists && this._isValidSize(currentWidth, currentHeight, naturalWidth, naturalHeight)) {
471 sizes.push({
472 w: currentWidth,
473 h: currentHeight,
474 dpr: 1,
475 breakpoint: window.innerWidth,
476 descriptor: `${currentWidth}w`,
477 source: 'current',
478 category: 'current',
479 label: 'current-size'
480 });
481 }
482
483 return sizes;
484 },
485
486 /**
487 * Check if a size is valid for srcset generation
488 * @private
489 * @param {number} width - Target width
490 * @param {number} height - Target height
491 * @param {number} naturalWidth - Natural image width
492 * @param {number} naturalHeight - Natural image height
493 * @returns {boolean} True if size is valid
494 */
495 _isValidSize: function(width, height, naturalWidth, naturalHeight) {
496 return width >= this.CONFIG.MIN_SIZE &&
497 height >= this.CONFIG.MIN_SIZE &&
498 width <= naturalWidth &&
499 height <= naturalHeight;
500 },
501
502 /**
503 * Find missing sizes by comparing required vs existing
504 * @private
505 * @param {Array} requiredSizes - Required size variations
506 * @param {Array} existingSizes - Existing size variations
507 * @returns {Array} Array of missing size variations
508 */
509 _findMissingSizes: function(requiredSizes, existingSizes) {
510 const existingWidths = new Set(existingSizes.map(size => size.w));
511 const tolerance = this.CONFIG.SIZE_TOLERANCE;
512
513 return requiredSizes.filter(required => {
514 // Check if we already have a size within tolerance
515 const hasCloseMatch = Array.from(existingWidths).some(existingWidth =>
516 Math.abs(existingWidth - required.w) <= tolerance
517 );
518
519 return !hasCloseMatch;
520 });
521 },
522
523 /**
524 * Select the best variations when limiting the number of srcset sizes
525 * Prioritizes 1x variants and ensures good coverage across the size range
526 * @private
527 * @param {Array} sortedSizes - Array of size objects sorted by width
528 * @param {number} maxVariations - Maximum number of variations to select
529 * @returns {Array} Array of selected size variations
530 */
531 _selectBestVariations: function(sortedSizes, maxVariations) {
532 if (sortedSizes.length <= maxVariations) {
533 return sortedSizes;
534 }
535
536 // Separate 1x and 2x variants
537 const dpr1Sizes = sortedSizes.filter(size => size.dpr === 1);
538 const dpr2Sizes = sortedSizes.filter(size => size.dpr === 2);
539
540 const selected = [];
541
542 // Strategy: Prioritize 1x variants, then add strategic 2x variants
543 const target1xCount = Math.min(dpr1Sizes.length, Math.ceil(maxVariations * 0.75)); // 75% should be 1x
544 const target2xCount = maxVariations - target1xCount;
545
546 // Select 1x variants with even distribution
547 if (dpr1Sizes.length > 0) {
548 if (dpr1Sizes.length <= target1xCount) {
549 // Include all 1x variants
550 selected.push(...dpr1Sizes);
551 } else {
552 // Distribute 1x variants evenly
553 const step = (dpr1Sizes.length - 1) / (target1xCount - 1);
554 for (let i = 0; i < target1xCount; i++) {
555 const index = Math.round(i * step);
556 selected.push(dpr1Sizes[index]);
557 }
558 }
559 }
560
561 // Select 2x variants strategically (prefer middle sizes for 2x)
562 if (target2xCount > 0 && dpr2Sizes.length > 0) {
563 if (dpr2Sizes.length <= target2xCount) {
564 // Include all 2x variants
565 selected.push(...dpr2Sizes);
566 } else {
567 // Select middle-range 2x variants (most useful for retina displays)
568 const sortedDpr2 = dpr2Sizes.sort((a, b) => a.w - b.w);
569 const middleStart = Math.floor(sortedDpr2.length / 4);
570 const middleEnd = Math.ceil(sortedDpr2.length * 3 / 4);
571 const middleRange = sortedDpr2.slice(middleStart, middleEnd);
572
573 const step = Math.max(1, Math.floor(middleRange.length / target2xCount));
574 for (let i = 0; i < target2xCount && i * step < middleRange.length; i++) {
575 selected.push(middleRange[i * step]);
576 }
577 }
578 }
579
580 // Remove duplicates and sort by width
581 const uniqueSelected = selected.filter((size, index, arr) =>
582 arr.findIndex(s => s.w === size.w) === index
583 );
584
585 return uniqueSelected.sort((a, b) => a.w - b.w);
586 },
587
588 /**
589 * Remove duplicate sizes from the required sizes array
590 * @private
591 * @param {Array} sizes - Array of size objects
592 * @returns {Array} Array with duplicates removed
593 */
594 _removeDuplicateSizes: function(sizes) {
595 const seen = new Set();
596 return sizes.filter(size => {
597 const key = `${size.w}x${size.h}`;
598 if (seen.has(key)) {
599 return false;
600 }
601 seen.add(key);
602 return true;
603 });
604 },
605
606 /**
607 * Get a summary of srcset analysis for all images
608 * @returns {Object} Summary statistics
609 */
610 getSrcsetAnalysisSummary: function() {
611 const allImages = document.querySelectorAll('img[data-opt-id]');
612
613 // Count images based on new logic
614 let includedImages = 0;
615 let skippedImages = 0;
616 let lazyLoadedImages = 0;
617 let nonLazyloadImages = 0;
618
619 allImages.forEach(img => {
620 const hasOptSrc = img.hasAttribute('data-opt-src');
621 const hasLazyLoaded = img.hasAttribute('data-opt-lazy-loaded');
622 const shouldInclude = !hasOptSrc || (hasOptSrc && hasLazyLoaded);
623
624 if (shouldInclude) {
625 includedImages++;
626 if (!hasOptSrc) {
627 nonLazyloadImages++;
628 } else if (hasLazyLoaded) {
629 lazyLoadedImages++;
630 }
631 } else {
632 skippedImages++;
633 }
634 });
635
636 return {
637 totalOptimoleImages: allImages.length,
638 includedImages: includedImages,
639 skippedImages: skippedImages,
640 nonLazyloadImages: nonLazyloadImages,
641 lazyLoadedImages: lazyLoadedImages,
642 currentDeviceType: optmlDevice.getDeviceType(),
643 viewportSize: {
644 w: window.innerWidth,
645 h: window.innerHeight
646 }
647 };
648 }
649 };
650