do_lazy = (bool) apply_filters(
'ablocks/perf/perf_lazy_images',
(bool) Helper::get_settings( 'perf_lazy_images', true )
);
$self->do_dimensions = (bool) apply_filters(
'ablocks/perf/perf_image_dimensions',
(bool) Helper::get_settings( 'perf_image_dimensions', true )
);
$self->do_responsive = (bool) apply_filters(
'ablocks/perf/perf_responsive_images',
(bool) Helper::get_settings( 'perf_responsive_images', true )
);
if ( ! $self->do_lazy && ! $self->do_dimensions && ! $self->do_responsive ) {
return;
}
$self->eager_count = (int) apply_filters(
'ablocks/perf/lcp_eager_count',
(int) Helper::get_settings( 'perf_lcp_eager_count', 1 )
);
add_filter( 'render_block', [ $self, 'process' ], 20, 2 );
}
public function process( $content, $block ) {
if ( empty( $block['blockName'] ) || false === strpos( $block['blockName'], 'ablocks' ) ) {
return $content;
}
if ( false === strpos( $content, '
block_image_id = 0;
foreach ( [ 'imgId', 'imgIdMobile', 'imgIdTablet' ] as $key ) {
if ( ! empty( $attrs[ $key ] ) ) {
$this->block_image_id = (int) $attrs[ $key ];
break;
}
}
return preg_replace_callback(
'/
]*>/i',
[ $this, 'rewrite_img' ],
$content
);
}
private function rewrite_img( $matches ) {
$tag = $matches[0];
$this->image_index++;
$attrs = '';
// Lazy-loading / priority hints.
if ( $this->do_lazy ) {
$has_loading = false !== stripos( $tag, 'loading=' );
$is_eager = $this->image_index <= $this->eager_count;
if ( ! $has_loading ) {
// No loading hint yet — add one (idempotent, never overrides markup
// that set its own).
$attrs .= $is_eager ? ' loading="eager" fetchpriority="high"' : ' loading="lazy"';
if ( false === stripos( $tag, 'decoding=' ) ) {
$attrs .= ' decoding="async"';
}
} elseif ( $is_eager && false === stripos( $tag, 'fetchpriority=' ) ) {
// Above-the-fold image that markup hardcoded as loading="lazy" (e.g.
// aBlocks image save output) — upgrade it to eager + high priority so
// the likely-LCP image isn't deferred.
$tag = preg_replace(
'/\bloading=(["\'])(?:lazy|auto)\1/i',
'loading="eager" fetchpriority="high"',
$tag,
1
);
}
}
// CLS fix — make sure the image always carries BOTH width and height so the
// browser has an intrinsic aspect ratio. A missing dimension is as harmful as
// missing both: an
with width but no height (e.g. a block with a custom
// width) has no aspect ratio, so once core's "sizes=auto" is added the
// `contain-intrinsic-size:3000px 1500px` fallback stretches it tall. When only
// one dimension is present we derive the other from the intrinsic ratio rather
// than skipping. This runs when the dimensions feature is on OR responsive
// images are on — adding a srcset is what triggers "sizes=auto", so any image
// that gets a srcset must also carry both dimensions.
if ( $this->do_dimensions || $this->do_responsive ) {
$has_w = false !== stripos( $tag, 'width=' );
$has_h = false !== stripos( $tag, 'height=' );
if ( ! $has_w || ! $has_h ) {
$dim = $this->resolve_dimensions( $tag );
if ( $dim && $dim[0] > 0 && $dim[1] > 0 ) {
if ( ! $has_w && ! $has_h ) {
$attrs .= ' width="' . (int) $dim[0] . '" height="' . (int) $dim[1] . '"';
} elseif ( $has_w ) {
// Width present, height missing → height = width × (natH / natW).
$w = $this->get_attr( $tag, 'width' );
if ( preg_match( '/^\d+$/', $w ) && (int) $w > 0 ) {
$attrs .= ' height="' . (int) round( (int) $w * $dim[1] / $dim[0] ) . '"';
}
} else {
// Height present, width missing → width = height × (natW / natH).
$h = $this->get_attr( $tag, 'height' );
if ( preg_match( '/^\d+$/', $h ) && (int) $h > 0 ) {
$attrs .= ' width="' . (int) round( (int) $h * $dim[0] / $dim[1] ) . '"';
}
}
}
}
}
// Responsive delivery — add a width-descriptor srcset + sizes so the
// browser downloads a right-sized file instead of the full image (the
// single biggest mobile payload win). Only when the image maps to an
// attachment and doesn't already declare srcset.
if ( $this->do_responsive && false === stripos( $tag, 'srcset=' ) ) {
$attrs .= $this->responsive_attrs( $tag );
}
if ( '' === $attrs ) {
return $tag;
}
return preg_replace( '/^
resolve_attachment_id( $tag );
if ( ! $id ) {
return '';
}
$size = $this->resolve_dimensions( $tag );
$size = $size ? $size : 'full';
$srcset = wp_get_attachment_image_srcset( $id, $size );
if ( ! $srcset ) {
return '';
}
$sizes = wp_get_attachment_image_sizes( $id, $size );
$out = ' srcset="' . esc_attr( $srcset ) . '"';
if ( $sizes && false === stripos( $tag, 'sizes=' ) ) {
$out .= ' sizes="' . esc_attr( $sizes ) . '"';
}
return $out;
}
/**
* Resolve an image's intrinsic [width, height] without touching the
* filesystem: first from the attachment metadata (via the wp-image-{id}
* class, matching the exact rendered size), then from WordPress's
* `-WIDTHxHEIGHT` resized-filename convention. Returns null when unknown.
*/
private function resolve_dimensions( $tag ) {
$src = $this->get_attr( $tag, 'src' );
$id = $this->resolve_attachment_id( $tag );
if ( $id ) {
$dim = $this->dimensions_from_attachment( $id, $src );
if ( $dim ) {
return $dim;
}
}
if ( $src && preg_match( '/-(\d+)x(\d+)\.(?:jpe?g|png|gif|webp|avif|bmp)(?:\?.*)?$/i', $src, $m ) ) {
return [ (int) $m[1], (int) $m[2] ];
}
return null;
}
/**
* Resolve the attachment id for an
: prefer the WordPress `wp-image-{id}`
* class, otherwise map the src URL back to an attachment (aBlocks image blocks
* don't emit the class). URL lookups are cached per request — and only run
* when the class is absent — so the DB is hit at most once per unique URL.
*/
private $id_cache = [];
private $block_image_id = 0;
private function resolve_attachment_id( $tag ) {
if ( preg_match( '/wp-image-(\d+)/', $tag, $m ) ) {
return (int) $m[1];
}
// The current block's stored attachment id (authoritative for aBlocks
// image blocks, which don't emit the class).
if ( $this->block_image_id ) {
return $this->block_image_id;
}
$src = $this->get_attr( $tag, 'src' );
if ( ! $src ) {
return 0;
}
if ( ! array_key_exists( $src, $this->id_cache ) ) {
$this->id_cache[ $src ] = (int) attachment_url_to_postid( $src );
}
return $this->id_cache[ $src ];
}
/**
* Pull width/height from an attachment's stored metadata, preferring the
* registered sub-size whose file matches the rendered src, falling back to
* the full-size dimensions.
*/
private function dimensions_from_attachment( $id, $src ) {
if ( ! $id ) {
return null;
}
$meta = wp_get_attachment_metadata( $id );
if ( ! is_array( $meta ) || empty( $meta['width'] ) || empty( $meta['height'] ) ) {
return null;
}
if ( $src && ! empty( $meta['sizes'] ) && is_array( $meta['sizes'] ) ) {
$base = wp_basename( strtok( $src, '?' ) );
foreach ( $meta['sizes'] as $size ) {
if ( isset( $size['file'], $size['width'], $size['height'] ) && $size['file'] === $base ) {
return [ (int) $size['width'], (int) $size['height'] ];
}
}
}
return [ (int) $meta['width'], (int) $meta['height'] ];
}
private function get_attr( $tag, $name ) {
if ( preg_match( '/\b' . preg_quote( $name, '/' ) . '=(["\'])(.*?)\1/i', $tag, $m ) ) {
return $m[2];
}
return '';
}
}