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 ''; } }