bigcommerce / cornerstone

The BigCommerce Cornerstone theme
https://developer.bigcommerce.com/stencil-docs
288 stars 601 forks source link

Poor Core Web Vital - LCP #2137

Open Tiggerito opened 2 years ago

Tiggerito commented 2 years ago

Expected behavior

Google's Core Web Vitals are now a ranking factor as well as a measure of a good page experience. One of the metrics is Largest Contentful Paint (LCP) which google considers should be less than 2.5 seconds for a good experience.

Actual behavior

Our Tag Rocket app monitors the Web Vitals on BigCommerce stores. We found that they consistently failed on LCP for product, category, brand and home pages. With 75th percentile scores exceeding 3 or even 4 seconds.

This information can also bee seen in a more aggregated way via Page Speed Insights on a store. The origin section is the most accurate representation of user experiences for the whole site.

Steps to reproduce behavior

Use tools like Page Speed Insights or Web Page Test to check the Core Web Vitals scores for certain page types like product, category and home.

Solution

One way to improve LCP is to improve the initial server response time. That is out of the scope for this.

LCP is the time it takes for the main part of the visible page to be completed. An element is designated as the LCP element and a time given for when it is visible. It is often the main image of a page. Note that mobile and desktop views are quite different and so can have different LCP issues.

I used the data gathered by our app to determine the most common slow LCP elements and then made alterations to reduce the time it took for them to show. The main elements were:

I found that the biggest reason that these images had a poor LCP was because they were lazy loaded. Lazy loading deliberately delays them and causes a larger LCP. Lazy loading should not be applied to images that are visible as the page loads (reference).

I removed the lazy loading from those identified and saw a noticeable improvement in LCP. But not enough to pass.

So I went a step further and preloaded them so that they started to load as soon as the html was read. This got them to pass.

Google's Search Console shows what Google thinks about your Core Web Vitals. For the store I fixed you can see a period where they all became good urls (green). This client accidentally removed our fixes causing a reversal of the improvements, which is an accidental way to confirm it was those changes that improved the score.

gsc

I then worked out how to make my changes in a stock Cornerstone theme, and documented what I did in my LCP Quick Fix article.

Baking those changes into Cornerstone may help BigCommerce catch up with Shopify in the Core Web Vitals race.

tech

BC-krasnoshapka commented 2 years ago

hi @Tiggerito thanks for your input. Cornerstone performance optimisation and Core Web Vitals are already in BC roadmap for Q1 2022.

bookernath commented 2 years ago

One interesting aspect when it comes to optimizing lazy-loading is that it can be very unclear at HTML-rendering time what is actually going to be in the above-the-fold content. On a large screen on desktop, 8 product cards might fit in the above-the-fold content on a category page; on a mobile device with a small screen, there may only be 2. "How many should we eagerly load?" is not a question with a straightforward answer.

That being said, since our initial lazyloading implementation, the native browser lazyloading support has improved significantly, and the browser can make smart choices for us here. We may be able to eagerly load ~8 or so product cards safely and allow the browser to make the choice, or move to using the browser feature entirely.

One issue I do see with Google's metrics is that they currently punish the use of LQIP strategies even though these can be quite good for user experience. We use LQIP today, which we can prove works quite well for the "felt" performance of the page for most users, but Google's current metrics consider it a problem. Therefore, we must choose whether to optimize for synthetic metrics or user experience.

Tiggerito commented 2 years ago

Your right that this is chasing the metric, which will most likely change as Google gets their head around it.

At the moment it typically picks on the first large image in the viewport, so my solution just preloads the first product image. And that fixes LCP. This is valid for mobile but as you point out, on desktop the user may actually see 8 pages on load. So this solution is metric based and not user based.

I've also seen a LQIP scenario. From what I saw the metric picked up that the image was swapped out and set the LCP to the swap out time. So, even though the user already had a reasonable image the LQIP effect caused a slow LCP.

I'm sure in the long term the CWV team will improve how they set these metrics. They've already done some fixes in its first year.

bookernath commented 2 years ago

@Tiggerito I've created a WIP PR here to adjust the lazyloading methodology, I'm curious if you have any feedback?

I've applied this new theme version to this store if you wish to do any testing.

Tiggerito commented 2 years ago

I've just released an article related to my playing around with native lazy loading and LQIP. My findings are that it can improve LCP and perceived speed.

https://bigcommerce.websiteadvantage.com.au/tag-rocket/articles/improving-image-loading-without-javascript/

I'll have a play with your demo.

Tiggerito commented 2 years ago

Nice to work on a simple clean store. Test page:

https://test-store929.mybigcommerce.com/shop-all/

This became a bit of an epic. TL;DR; I reduced LCP on a fast 3G network from 4.5 seconds down to 2.7 seconds.

I hacked in sizes attributes as per my comment in the WIP and it looks reasonably good for LCP. But there is a bit of a delay before the images start loading. And nothing to do with the lazy loading setting (I tested) but due to the web fonts loading script and css resources.

I changed the web fonts loader to an async version based on their instruction and that stopped that from blocking. Assuming the async version works.

Next was the css file. There does seem to be a hack combined with preload. As a test I added a preload and moved the link to the end of the page (so images got in first, but the css was not really delayed).

https://stackoverflow.com/questions/32759272/how-to-load-css-asynchronously

This left the css font which I solved in the same way, preload and move to the end.

At this point all critical resources started loading at once and the LCP was the css file. i.e. the images completed loading before the css file so it had become the critical resource for LCP.

However this caused a brief moment when rendering was done without the css. Ugly.

Back to the usual solution. Instead of preloading the css files I preloaded the first two images. And that worked. All critical resources loaded at once and no css flicker.

The down side to the preload of images is that we don't know how many to preload. For mobile devices one or two would be right, but for desktops it may be more.

So the solution I liked consisting of fixing the sizes attribute and preloading a few of the images. The async of the web fonts loader would not make much difference.

I then noticed DCL was a bit delayed. A bit of playing and changing the defer scripts to async fixed that. DCL was now when theme-bundle.main.js was loaded. So I made that async and DCL was at LCP. Not sure if that would break anything?

Finally on to the Load event. I preloaded all the async scripts and that made the critical path for Load as loading the css. Then I preloaded the fonts in the css.

The load event now fires once theme-bundle.main.js is loaded. That is the single critical path to page load.

Here's the timings before the changes in my Chrome Canary Browser throttled as a fast 3G network. LCP was 4.5 seconds and Load was 5.9 seconds.

before

And after. LCP is 2.7 seconds and Load is 3.2 seconds.

after

I've attached the final html. Search for 'Tony' to see the edits.

index.txt

Tiggerito commented 2 years ago

I was doing some more testing and research.

Web fonts are complex

It seems it would be best not to async the web fonts loader as fonts are important. If they load late page repaints with the new font (ugly) and it can cause Layout Shift.

Unfortunately, This font system first loads some CSS to identify the font, which then loads the font. Fonts in CSS are low priority so tend to be delayed.

One solution is to preload the actual fonts, but there is an issue. If you look at the font CSS the fonts URL changes with version updates. Something hard or impossible to work out for a preload.

One solution is to store the fonts locally so you control their URL, which also avoids the extra connection.

I did a test hard coding the preload of fonts and it worked well. The font can load before the first paint and avoid the layout shifts.

LCP LCP is mainly down to the slowest of CSS and main image loading.

Preloading the image is a big win . My tests show that this alone can turn a page from a poor LCP to a good one for many users. Responsive images (srcset) would improve this for smaller devices.

CSS is high priority and early loaded already. Splitting the CSS into essential and other can reduce the size of that critical CSS file. It's even suggested to inline the essential stuff. But I'd not consider that in the critical path for most pages, especially as it can be site wide cached.

DCL/Load This is more about how fast all the JS runs to make the page fully functional.

Blocking scripts can cause other script loading to be delayed. And delay DCL.

Delayed DCL delays the execution of other scripts, which delays Load.

Real World Consequence: I know of a Reviews widget that waits on DCL to add their widget (as it needs the DOM). Often sites are so slow to get to DCL that the review widget takes over 5 seconds to show. Googlebot gives a page about 5 seconds, so often Google doesn't to see the widget and its structured data, and therefore they don't get reviews stars in the search results.

So there is value in preloading scripts that block or are delayed for some other reason (e.g. scripts loading scripts).

I've set up a demo store based on the latest Cornerstone and added a bunch of preloads to the home page.

https://store-d70rbxs9pw.mybigcommerce.com/

Emulating fast 3G and the preloaded image is the critical path to LCP and the preloaded theme-bundle.main.js is the critical path to DCL. In my tests both LCP and DCL were reduced by 1 second and no font flicker or layout shift. LCP was inside the 2.5 second limit for a good experience.

The time between DCL and load is for BCs Facebook and GA tracking code, not stuff users care about. But anything waiting on Load would still get delayed. It would be nice to work out how to make that tracking code not delay the loaded event.

Tiggerito commented 10 months ago

@AdiMakk s comment is generic and not specific to BigCommerce. It also looks like it could be AI written. And it ends with a promo for a tool that probably created this disposable user and comment. It should be deleted.

BC-krasnoshapka commented 10 months ago

Promo comment deleted.