Skip to content

Page profiler never treats the detected LCP image as LCP, so it gets no fetchpriority="high" (stored ID is a string, compared with === to an int) #1202

Closed

Description

@kushh23

Description

The page profiler detects the LCP image in the browser and sends its data-opt-id to /optimizations as an integer. The REST handler stores it with sanitize_text_field(), which always returns a string. Profile::is_lcp_image_in_all_viewports( int $image_id ) then compares that string to the int ID with ===, so it is always false.

Result, with the default "fixed|viewport" lazy load type and profile data stored for both devices:

  • Optimole never adds fetchpriority="high" to the LCP image (tag_replacer.php LCP branch is dead, and the fixed-count fallback is skipped because profile data exists).
  • If the LCP image is not in the above-the-fold list for both devices, it is lazy-loaded anyway (and any fetchpriority="high" on it is stripped).

Expected: the stored LCP image gets fetchpriority="high" and is never lazy-loaded. Likely fix: store absint( $lcp_data['i'] ?? 0 ) in inc/rest.php (or cast before comparing in Profile.php). Existing profiles in transients/object cache keep the string until they expire, so casting at compare time also covers those.

The unit test test_lazyload_viewport_with_lcp_data stores imageId as an int directly through Profile::store(), so it does not cover the REST path.

Step-by-step reproduction instructions

  1. On a test site with Optimole 4.2.17 connected and default Lazy Load settings, run this with WP-CLI (it stores LCP data the same way inc/rest.php:1032 does):
    wp eval '
    $p   = new OptimoleWP\PageProfiler\Profile();
    $img = 1578474861;
    $lcp = [ "imageId" => sanitize_text_field( $img ), "type" => "img" ];
    foreach ( [ 1, 2 ] as $d ) { $p->store( "bugcheck", $d, [], [], $lcp ); }
    OptimoleWP\PageProfiler\Profile::set_current_profile_id( "bugcheck" );
    $p->set_current_profile_data();
    var_dump( $p->is_lcp_image_in_all_viewports( $img ) );
    '
    
  2. Expected bool(true). Actual bool(false).
  3. Clean up: wp transient delete _oprof_bugcheck_1 && wp transient delete _oprof_bugcheck_2 (or flush the optimole_page_profiler cache group if an object cache is used).

End to end check on a real page: open a page with a large hero image once on desktop and once in mobile emulation so both profiles are stored, then reload and view source. The hero <img data-opt-id=...> has no fetchpriority="high" added by Optimole, and wp transient get _oprof_<profile id>_2 --format=json shows "imageId":"<id>" as a string.

Pure PHP reproduction of the same path (json_decode of the REST body, core _sanitize_text_fields cast, transient serialize round trip, the Profile.php:387 comparison), PHP 8.4:

REST param l.i type: integer
[transient] stored imageId='1578474861' (string), page image_id=1578474861 (integer) -> is_lcp_image_in_all_viewports=false; loose == would be true
[object-cache-json] stored imageId='1578474861' (string), page image_id=1578474861 (integer) -> is_lcp_image_in_all_viewports=false; loose == would be true

Screenshots, screen recording, code snippet or Help Scout ticket

Browser sends an int: https://github.com/Codeinwp/optimole-wp/blob/v4.2.17/assets/js/modules/lcp.js#L88-L92

lcpData.imageId = parseInt(id, 10);

REST handler stores a string: https://github.com/Codeinwp/optimole-wp/blob/v4.2.17/inc/rest.php#L1032

$sanitized_lcp_data['imageId'] = sanitize_text_field( $lcp_data['i'] ?? '' );

Strict compare with the int data-opt-id: https://github.com/Codeinwp/optimole-wp/blob/v4.2.17/inc/v2/PageProfiler/Profile.php#L385-L393

public function is_lcp_image_in_all_viewports( int $image_id ): bool {
    ...
    ( ( self::$current_profile_data[ $device ]['lcp']['imageId'] ?? null ) === $image_id )

Callers that never fire:

$image_id comes from get_id_by_url(), which returns rand() (an int): https://github.com/Codeinwp/optimole-wp/blob/v4.2.17/inc/app_replacer.php#L691-L697

Core: _sanitize_text_fields() casts to string: https://github.com/WordPress/WordPress/blob/7.1.3/wp-includes/formatting.php#L5708-L5713

Environment info

  • Optimole 4.2.17 (same code on master and back to 4.0.0)
  • WordPress 5.5 to 7.1 (_sanitize_text_fields() casts to string in 5.5, 6.8.3 and 7.1.3)
  • PHP 7.4 to 8.4 (strict === behaves the same on all)
  • Lazy load type "fixed|viewport" (default), page profiler enabled (default)

Is the issue you are reporting a regression

No. The string store and the strict compare are both present in 4.0.0.

Found while writing the WordPress compatibility checks for Optimole.

Activity

  1. added
    bugThis label could be used to identify issues that are caused by a defect in the product.
    on Oct 28, 2026
  2. added theissue type on Oct 28, 2026
  3. girishpanchal30 commented on Oct 29, 2026

    @girishpanchal30
    Contributor

    I have fixed it with PR #1213

  4. milicavs commented on Oct 29, 2026

    @milicavs

    🧪 QA steps and test links

    Optimole's page profiler finds each page's largest image (the LCP image) so it can load it first, but it never recognizes the image it saved. That LCP image gets no fetchpriority="high" from Optimole, and when it isn't among the first 6 above-the-fold images (for example a large photo under a row of logos) Optimole lazy-loads it, which slows the page down.

    Link Build Bot check
    Reproduce the bug Optimole 4.2.17 (released) ✅ Reproduced: the large photo is lazy-loaded (data-opt-src and a placeholder image), with no fetchpriority; its saved LCP ID is text
    Verify the fix PR #1213 build f0d986d (Plugins shows 4.2.17) ✅ Fixed: the large photo loads at once with fetchpriority="high" and no data-opt-src; its saved LCP ID is a number

    In the links:

    • Steps 1 to 5 are already done, and the links open on the LCP test page.
    • Optimole uses a stand-in connection (no account), so images point at a fake Optimole address and don't display. Check the markup with right-click → Inspect.
    • Instead of real visits, the setup sends the page's desktop and mobile profiles to Optimole the way a visitor's browser does. A must-use plugin, "QA: fixed page profile ID for the LCP test page", makes that possible.
    • Tools → QA Logs shows debug.log, including the saved LCP ID and its type (profile_lcp).

    Reproduce on Optimole 4.2.17 (connected, Lazy load on with its default viewport mode):

    1. Pages → Add Page, title "LCP test".
    2. Add a Gallery block with 6 small images (logos, around 160×80).
    3. Below it, add an Image block with a large photo (1600px wide or more). Publish.
    4. In a private window, open the page at desktop width and wait 5 seconds.
    5. Turn on DevTools' device toolbar (a phone size), reload, wait 5 seconds, then turn it off.
    6. Reload the page, right-click the large photo and choose Inspect.
    • Bug: the large photo's <img> has data-opt-src and a placeholder src (it is lazy-loaded), and no fetchpriority="high".
    • Fixed: it has a normal src, fetchpriority="high", and no data-opt-src.
    Edge cases
    • Run steps 1 to 5 on 4.2.17, then update to the PR build and only reload: fixed right away (profiles saved before the update work too).
    • Page with only the large photo, no logos: the photo is not lazy-loaded on either build.
    • Lazy load type set to a fixed number of images (no viewport mode): unchanged.
    • Hero set as a Cover block background instead of an Image block: still loads at once, unchanged.

    Assumption: the profiler lists above-the-fold images in page order, so 6 logos above the photo push it out of the list.

    Fix: #1213 (open)

  5. pirate-bot commented on Oct 29, 2026

    @pirate-bot
    Collaborator

    QA agent — 🟢 Passing checks · LCP images now get high priority and avoid lazy loading, including profiles cached before the update.

    The base build reproduced both problems on a page with six logos followed by a large hero image. After updating, the same cached profiles gave the hero high priority and stopped lazy-loading it; fresh integer profiles worked too. Real desktop and mobile browser reports stored integer LCP IDs, and the added tests and edge-case checks passed. Images were served locally using a seeded Optimole connection, not a real account.

    Worth a human look

    1. Verify account activation and live Optimole delivery, including the automated QA hint’s fixed-count-only and Cover-background examples.
    Checks (3 passed)
    • ✅ The frontend profiler's desktop and mobile LCP image reports are stored with an integer imageId through the optimizations REST endpoint.
    • ✅ On reloading a page with desktop and mobile profiles, Optimole adds fetchpriority="high" to the stored LCP image, including immediately after upgrading profiles whose imageId is a numeric string.
    • ✅ The stored LCP image is not lazy-loaded even when absent from both devices' above-the-fold lists, for both fresh integer profiles and cached numeric-string profiles.

    Full report, evidence and session trail

  6. pirate-bot commented on Nov 2, 2026

    @pirate-bot
    Collaborator

    🎉 This issue has been resolved in version 4.2.18 🎉

    The release is available on GitHub release

    Your semantic-release bot 📦🚀

  7. added
    releasedIndicate that an issue has been resolved and released in a particular version of the product.
    on Nov 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugThis label could be used to identify issues that are caused by a defect in the product.releasedIndicate that an issue has been resolved and released in a particular version of the product.

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions