Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial detection logic for Image Loading Optimization #876

Merged

Conversation

westonruter
Copy link
Member

@westonruter westonruter commented Oct 26, 2023

Summary

Add logic to detect the LCP element and images and elements with background images which are in the initial viewport.

See #870.

This is currently a sub-PR of #875.

Relevant technical choices

The logic is contained in a detect.js script which gets imported as a module from an inline module script. This module script is currently printed to the page with every page load, but once storage (#871) is implemented, it will be conditionally added based on whether no existing metrics are available.

The logic in detect.js only runs if it has been invoked within 5 seconds of the page being generated. This ensures that if the page output is cached, any future conditional serving of the detection script will not end up resulting in many clients running the logic unnecessarily when page caching is involved. The 5-second window can be filtered by perflab_image_loading_detection_time_window.

The detection logic involves gathering up a list of the optimizable elements, namely images and elements with background images (for now). For each of these elements, breadcrumbs are obtained which is an array of objects containing the tag name and element index. For example, the custom logo's breadcrumbs are:

[
    {
        "tagName": "HTML",
        "index": 0
    },
    {
        "tagName": "BODY",
        "index": 1
    },
    {
        "tagName": "DIV",
        "index": 2
    },
    {
        "tagName": "HEADER",
        "index": 0
    },
    {
        "tagName": "DIV",
        "index": 0
    },
    {
        "tagName": "DIV",
        "index": 0
    },
    {
        "tagName": "DIV",
        "index": 0
    },
    {
        "tagName": "A",
        "index": 0
    },
    {
        "tagName": "IMG",
        "index": 0
    }
]

These breadcrumbs are captured as soon as the module runs so that any subsequent dynamic insertions of new elements by JavaScript won't invalidate the breadcrumbs to look up the elements on the server-rendered page at the end of output buffering.

With the list of optimizable elements in hand, an IntersectionObserver is used to keep track of when they become visible (if ever). If the initial page load is not at the top of the page then the detection logic aborts since we don't want to capture metrics for an anchor link. Similarly, the IntersectionObserver disconnects as soon as the user starts scrolling. Elements contained inside of the admin bar are ignored.

In addition to observing for visible optimizable elements, the onLCP function from web-vitals.js library is used to obtain the LCP element metric. Currently onLCP is invoked with reportAllChanges as true as otherwise it will only fire when the user clicks or presses a key. So at the very least for development purposes, reportAllChanges is enabled. When the window's load event fires, then the latest LCP candidate metric reported is considered LCP.

Finally, page metrics are gathered to send to the server, including:

  • The viewport width and height
  • Metrics for each optimizable element, including:
    • Whether it is LCP
    • Whether it was an LCP candidate (i.e. second most important)
    • Breadcrumbs
    • Intersection ratio
    • Intersection rectangle
    • Bounding client rectangle

For example:

{
    "viewport": {
        "width": 3201,
        "height": 1644
    },
    "elements": [
        {
            "isLCP": false,
            "isLCPCandidate": false,
            "breadcrumbs": [
                {
                    "tagName": "HTML",
                    "index": 0
                },
                {
                    "tagName": "BODY",
                    "index": 1
                },
                {
                    "tagName": "DIV",
                    "index": 2
                },
                {
                    "tagName": "HEADER",
                    "index": 0
                },
                {
                    "tagName": "DIV",
                    "index": 0
                },
                {
                    "tagName": "DIV",
                    "index": 0
                },
                {
                    "tagName": "DIV",
                    "index": 0
                },
                {
                    "tagName": "A",
                    "index": 0
                },
                {
                    "tagName": "IMG",
                    "index": 0
                }
            ],
            "intersectionRatio": 1,
            "intersectionRect": {
                "x": 1000.40625,
                "y": 79.984375,
                "width": 120.015625,
                "height": 120.015625,
                "top": 79.984375,
                "right": 1120.421875,
                "bottom": 200,
                "left": 1000.40625
            },
            "boundingClientRect": {
                "x": 1000.40625,
                "y": 79.984375,
                "width": 120.015625,
                "height": 120.015625,
                "top": 79.984375,
                "right": 1120.421875,
                "bottom": 200,
                "left": 1000.40625
            }
        },
        {
            "isLCP": false,
            "isLCPCandidate": false,
            "breadcrumbs": [
                {
                    "tagName": "HTML",
                    "index": 0
                },
                {
                    "tagName": "BODY",
                    "index": 1
                },
                {
                    "tagName": "DIV",
                    "index": 2
                },
                {
                    "tagName": "MAIN",
                    "index": 1
                },
                {
                    "tagName": "DIV",
                    "index": 1
                },
                {
                    "tagName": "FIGURE",
                    "index": 0
                },
                {
                    "tagName": "IMG",
                    "index": 0
                }
            ],
            "intersectionRatio": 1,
            "intersectionRect": {
                "x": 1275.40625,
                "y": 466.328125,
                "width": 0.015625,
                "height": 0.015625,
                "top": 466.328125,
                "right": 1275.421875,
                "bottom": 466.34375,
                "left": 1275.40625
            },
            "boundingClientRect": {
                "x": 1275.40625,
                "y": 466.328125,
                "width": 0.015625,
                "height": 0.015625,
                "top": 466.328125,
                "right": 1275.421875,
                "bottom": 466.34375,
                "left": 1275.40625
            }
        },
        {
            "isLCP": true,
            "isLCPCandidate": true,
            "breadcrumbs": [
                {
                    "tagName": "HTML",
                    "index": 0
                },
                {
                    "tagName": "BODY",
                    "index": 1
                },
                {
                    "tagName": "DIV",
                    "index": 2
                },
                {
                    "tagName": "MAIN",
                    "index": 1
                },
                {
                    "tagName": "DIV",
                    "index": 1
                },
                {
                    "tagName": "FIGURE",
                    "index": 1
                },
                {
                    "tagName": "IMG",
                    "index": 0
                }
            ],
            "intersectionRatio": 1,
            "intersectionRect": {
                "x": 1275.40625,
                "y": 490.328125,
                "width": 300.015625,
                "height": 196.015625,
                "top": 490.328125,
                "right": 1575.421875,
                "bottom": 686.34375,
                "left": 1275.40625
            },
            "boundingClientRect": {
                "x": 1275.40625,
                "y": 490.328125,
                "width": 300.015625,
                "height": 196.015625,
                "top": 490.328125,
                "right": 1575.421875,
                "bottom": 686.34375,
                "left": 1275.40625
            }
        }
    ]
}

With these metrics in hand, the server can to store them (#871) and make use of them to optimize the loading of those elements on the page (#872).

Checklist

  • PR has either [Focus] or Infrastructure label.
  • PR has a [Type] label.
  • PR has a milestone or the no milestone label.

@westonruter westonruter added [Focus] Images Issues related to the Images focus area [Type] Feature A new feature within an existing module labels Oct 26, 2023
@westonruter westonruter added this to the PL Plugin 2.8.0 milestone Oct 26, 2023
.gitignore Outdated Show resolved Hide resolved
} );
}

// TODO: Use a local copy of web-vitals.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know if you'd like help with that

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This todo isn't to be done in this PR. I'm working on this in #878.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops. I thought you commented on a different todo :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was presuming that there would be a JS build process introduced to the plugin as part of #864 and so we could incorporate the use of a local copy of web-vitals as part of that. But actually it looks like it's not going to involve JS. If you think a JS build process could be incorporated as part of the PR, I'd appreciate your help. But otherwise I think this can be tackled in a subsequent PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@westonruter Just looking through these changes now, I think it would be good to open a separate issue/PR for introducing a build process for JS. This can be against the feature branch because no other module needs it so far, and I don't anticipate any new module being introduced in the near future where it would be needed. So we can do that as part of this one. Let's just make sure the approach could be used by other modules too.

Comment on lines +274 to +275
// TODO: Send data to server.
log( pageMetrics );
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to be done in another PR. See #878.

Base automatically changed from add/image-loading-optimization-module to feature/image-loading-optimization November 2, 2023 20:55
Copy link
Member

@joemcgill joemcgill left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couple of questions, but generally this looks like a good start. No reason to block this for the feature branch.

.eslintrc.js Outdated
@@ -8,7 +8,15 @@ const config = {
rules: {
...( wpConfig?.rules || {} ),
'jsdoc/valid-types': 'off',
'no-console': 'off',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if it's a good idea to turn this off for the whole plugin. Perhaps we can just disable it where a console.log is expected instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 912abef

Comment on lines +233 to +239
const pageMetrics = {
viewport: {
width: win.innerWidth,
height: win.innerHeight,
},
elements: [],
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious how you're thinking of using various viewport dimensions in the data when applying optimizations?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it turns out that that there are different LCP images for different viewports, then fetchpriority would not be added to the img elements themselves. Instead the viewport widths captured here would then be used to construct the media attribute of preload links so that only the relevant image URL is prioritized for the given viewport. In this way, the same markup can be served to both desktop and mobile, while both still get optimized.

@westonruter westonruter removed this from the PL Plugin 2.8.0 milestone Nov 7, 2023
@westonruter westonruter merged commit a3e8c10 into feature/image-loading-optimization Nov 7, 2023
8 checks passed
@westonruter westonruter deleted the add/image-loading-detection branch November 7, 2023 02:17
Comment on lines +59 to +62
* Allow this amount of milliseconds between when the page was first generated (and perhaps cached) and when the
* detect function on the page is allowed to perform its detection logic and submit the request to store the results.
* This avoids situations in which there is missing detection metrics in which case a site with page caching which
* also has a lot of traffic could result in a cache stampede.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

@westonruter westonruter added no milestone PRs that do not have a defined milestone for release [Plugin] Optimization Detective Issues for the Optimization Detective plugin labels Nov 17, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Focus] Images Issues related to the Images focus area no milestone PRs that do not have a defined milestone for release [Plugin] Optimization Detective Issues for the Optimization Detective plugin [Type] Feature A new feature within an existing module
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants