diff --git a/README.md b/README.md
index 76601f34..8da99532 100644
--- a/README.md
+++ b/README.md
@@ -345,3 +345,11 @@ add_filter('optml_dont_replace_url', function( $old, $url ) {
}, 10, 2);
+
+= Can I keep the sizes attribute I wrote for an image ? =
+
+Yes. Add the `data-optml-keep-sizes` attribute to the image tag and the page profiler won't add its own entries to the image's `sizes` attribute. Images lazy loaded by Optimole don't keep `srcset` or `sizes`, since the lazy loader doesn't need them. To keep the existing `sizes` attribute on every image, add this snippet:
+
+
+add_filter( 'optml_keep_existing_sizes', '__return_true' );
+
diff --git a/composer.lock b/composer.lock
index 5dadbdc8..047a51e6 100644
--- a/composer.lock
+++ b/composer.lock
@@ -64,16 +64,16 @@
},
{
"name": "codeinwp/themeisle-sdk",
- "version": "3.3.67",
+ "version": "3.3.68",
"source": {
"type": "git",
"url": "https://github.com/Codeinwp/themeisle-sdk.git",
- "reference": "e1dda44811660a65c3da6994349c054a46686d69"
+ "reference": "8477d5100ffdddd316a3e51129d775d8854d8a77"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/e1dda44811660a65c3da6994349c054a46686d69",
- "reference": "e1dda44811660a65c3da6994349c054a46686d69",
+ "url": "https://api.github.com/repos/Codeinwp/themeisle-sdk/zipball/8477d5100ffdddd316a3e51129d775d8854d8a77",
+ "reference": "8477d5100ffdddd316a3e51129d775d8854d8a77",
"shasum": ""
},
"require-dev": {
@@ -99,22 +99,22 @@
],
"support": {
"issues": "https://github.com/Codeinwp/themeisle-sdk/issues",
- "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.67"
+ "source": "https://github.com/Codeinwp/themeisle-sdk/tree/v3.3.68"
},
- "time": "2026-10-13T12:16:34+00:00"
+ "time": "2026-10-27T13:38:50+00:00"
},
{
"name": "enshrined/svg-sanitize",
- "version": "1.0.0",
+ "version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/darylldoyle/svg-sanitizer.git",
- "reference": "f3300fcd1bbf67d205b52217c75d0f7d6a8c47ff"
+ "reference": "1976ff93c8d8a234cdd3ff99626bd43ecf11bf49"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/darylldoyle/svg-sanitizer/zipball/f3300fcd1bbf67d205b52217c75d0f7d6a8c47ff",
- "reference": "f3300fcd1bbf67d205b52217c75d0f7d6a8c47ff",
+ "url": "https://api.github.com/repos/darylldoyle/svg-sanitizer/zipball/1976ff93c8d8a234cdd3ff99626bd43ecf11bf49",
+ "reference": "1976ff93c8d8a234cdd3ff99626bd43ecf11bf49",
"shasum": ""
},
"require": {
@@ -144,9 +144,9 @@
"description": "An SVG sanitizer for PHP",
"support": {
"issues": "https://github.com/darylldoyle/svg-sanitizer/issues",
- "source": "https://github.com/darylldoyle/svg-sanitizer/tree/1.0.0"
+ "source": "https://github.com/darylldoyle/svg-sanitizer/tree/1.1.1"
},
- "time": "2026-09-01T09:35:47+00:00"
+ "time": "2026-10-20T09:45:10+00:00"
},
{
"name": "symfony/polyfill-php80",
diff --git a/inc/attachment_cache.php b/inc/attachment_cache.php
index 64fa4a62..392caa9c 100644
--- a/inc/attachment_cache.php
+++ b/inc/attachment_cache.php
@@ -5,6 +5,13 @@
*/
class Optml_Attachment_Cache {
const CACHE_GROUP = 'om_att';
+ /**
+ * Most URLs kept in the in-memory map during one request.
+ *
+ * A page with many distinct image URLs would otherwise grow the map for the whole request.
+ * When it is full, the older half is dropped; those URLs are still in the persistent cache.
+ */
+ const MAX_MEMORY_ENTRIES = 2000;
/**
* Local cache map.
*
@@ -32,10 +39,10 @@ public static function get_cached_attachment_id( $url ) {
return self::$cache_map[ $cache_key ];
}
- $value = wp_using_ext_object_cache()
+ $value = wp_using_ext_object_cache()
? wp_cache_get( $cache_key, self::CACHE_GROUP )
: get_transient( self::CACHE_GROUP . $cache_key );
- self::$cache_map[ $cache_key ] = $value;
+ self::remember( $cache_key, $value );
return $value;
}
@@ -51,7 +58,7 @@ public static function get_cached_attachment_id( $url ) {
public static function set_cached_attachment_id( $url, $id ) {
$cache_key = self::get_cache_key( $url );
// We cache also in memory to avoid calling DB every time when not using Object Cache.
- self::$cache_map[ $cache_key ] = $id;
+ self::remember( $cache_key, $id );
// If the ID is not found we cache for 10 minutes, otherwise for a week.
// We try to reduce the cache time when is not found to
// avoid caching for situation when this might be temporary.
@@ -61,6 +68,21 @@ public static function set_cached_attachment_id( $url, $id ) {
: set_transient( self::CACHE_GROUP . $cache_key, $id, $expiration );
}
+ /**
+ * Keep a value in the in-memory map, dropping the older half when it is full.
+ *
+ * @param string $cache_key The cache key.
+ * @param int|numeric-string|false $value The attachment ID (a numeric string when read back from a transient), or false when it is not cached.
+ *
+ * @return void
+ */
+ private static function remember( $cache_key, $value ) {
+ if ( ! isset( self::$cache_map[ $cache_key ] ) && count( self::$cache_map ) >= self::MAX_MEMORY_ENTRIES ) {
+ self::$cache_map = array_slice( self::$cache_map, (int) ( self::MAX_MEMORY_ENTRIES / 2 ), null, true );
+ }
+ self::$cache_map[ $cache_key ] = $value;
+ }
+
/**
* Generate cache key for URL.
*
diff --git a/inc/manager.php b/inc/manager.php
index 587fe756..dc5dce2c 100644
--- a/inc/manager.php
+++ b/inc/manager.php
@@ -142,6 +142,13 @@ final class Optml_Manager {
*/
private static $ob_processed = false;
+ /**
+ * Reads the last PHP error; tests swap it to simulate a fatal error.
+ *
+ * @var callable(): (array{type: int, message: string, file: string, line: int}|null)
+ */
+ private static $error_reader = 'error_get_last';
+
/**
* Class instance method.
*
@@ -698,7 +705,9 @@ public static function parse_images_from_html( $content ) {
if ( OPTML_DEBUG ) {
do_action( 'optml_log', 'Content to parse images from: ' . $content );
}
- $regex = '/(?:]+?href=["|\'](?P[^\s]+?)["|\'][^>]*?>\s*)?(?P(?: