settings = $settings;
// Tell WordPress to skip lazy loading for the first N images (WP 5.9+)
$skip = (int) ($settings['lcp_skip_count'] ?? 2);
add_filter('wp_omit_loading_attr_threshold', fn() => $skip);
// For broader control (including non-WP images and iframes), filter content
add_filter('the_content', [$this, 'process_content'], 25);
add_filter('post_thumbnail_html', [$this, 'process_content'], 25);
add_filter('widget_text', [$this, 'process_content'], 25);
}
/**
* Process HTML content to add/manage lazy loading attributes.
*/
public function process_content(string $content): string {
if (empty($content) || is_admin() || is_feed() || $this->is_amp_request()) {
return $content;
}
$content = $this->process_images($content);
if (!empty($this->settings['lazy_load_iframes'])) {
$content = $this->process_iframes($content);
}
return $content;
}
/**
* Add loading="lazy" to
tags, skipping the first N for LCP.
*/
private function process_images(string $content): string {
$skip = (int) ($this->settings['lcp_skip_count'] ?? 2);
$exclude_classes = array_filter(array_map('trim', explode(',', $this->settings['exclude_classes'] ?? '')));
// The LCP budget is scoped to a single content block (the same
// semantics as core's wp_omit_loading_attr_threshold): an
// instance-wide counter shared across the_content, thumbnails, and
// widgets counted tags in filter-execution order, not document
// order, so the hero could still end up lazy-loaded.
$position = 0;
return preg_replace_callback('/
]+>/i', function ($matches) use ($skip, $exclude_classes, &$position) {
$tag = $matches[0];
// Already has a loading attribute - don't touch it
if (preg_match('/\sloading\s*=/i', $tag)) {
return $tag;
}
// Check excluded classes
if (!empty($exclude_classes) && preg_match('/class=["\']([^"\']+)["\']/i', $tag, $class_match)) {
$classes = explode(' ', $class_match[1]);
foreach ($exclude_classes as $excluded) {
if (in_array($excluded, $classes, true)) {
return $tag;
}
}
}
// Only images this filter actually modifies consume the budget;
// already-lazy and excluded tags above must not.
$position++;
// Skip first N images to protect LCP
if ($position <= $skip) {
return $this->inject_attribute($tag, 'loading', 'eager');
}
// Add loading="lazy" and decoding="async" for below-fold images
$tag = $this->inject_attribute($tag, 'loading', 'lazy');
$tag = $this->inject_attribute($tag, 'decoding', 'async');
return $tag;
}, $content);
}
/**
* Add loading="lazy" to