Largest contentful paint (LCP) optimization 

What is LCP? 

Largest Contentful Paint (LCP) is a stable Core Web Vital and key performance metric used in web development to measure the perceived loading speed of a web page. It specifically focuses on the time it takes for the largest content element, such as an image or text block, to become visible and fully rendered for the user. LCP is crucial for user experience as it reflects how quickly users can access and interact with the main content of a web page, influencing factors like bounce rates and overall user satisfaction.

A good LCP score is typically considered to be under 2.5 seconds, typically measured at the 75th percentile of page loads.

Built for Shopify

If you are building a public Shopify app on Gadget, one of the requirements to earn the Built for Shopify badge is to have an LCP score of less than 2.5 seconds.

For more information on what contributes to LCP and how LCP is reported, see the web.dev LCP documentation.

Measuring your LCP score 

For many web apps, you can measure your LCP score using tools like Google's PageSpeed Insights or Lighthouse. These tools provide detailed reports on your web page's performance, including LCP, and offer suggestions on how to improve your score.

If you are building an embedded Shopify app, which is rendered within an iframe, you can install the web-vitals package and use the getLCP function to measure your LCP score. For more information, see the web-vitals GitHub page and Shopify's docs for measuring your app's loading performance.

Steps to install the web-vitals package:

  1. Add the web-vitals package to your project using the Gadget command palette
terminal
yarn add web-vitals
  1. Import and use the getLCP function in your app
web/main.jsx
JavaScript
1// additional imports...
2import { onLCP } from "web-vitals";
3
4const root = document.getElementById("root");
5if (!root) throw new Error("#root element not found for booting react app");
6
7// onLCP is a callback that receives the LCP score as a parameter
8// you can also send the LCP score to your backend for further analysis
9onLCP(console.log);
10
11ReactDOM.createRoot(root).render(
12 <React.StrictMode>
13 <AppProvider i18n={enTranslations}>
14 <App />
15 </AppProvider>
16 </React.StrictMode>
17);

The getLCP function is a callback that receives the LCP score as a parameter. You could also send LCP scores to your Gadget backend or another service for further analysis.

Once you have determined that your LCP score needs improvement, you can start optimizing.

Development vs production LCP

In Gadget's development environments, Vite is used to serve your frontend assets, including JavaScript, CSS, and static files. These assets are not optimized for a better debugging experience, and the LCP score will be slower than in a production environment.
Assets in production environments are minified, optimized, and served through a globally distributed high-performance CDN. Make sure to test your LCP score in a production environment to get an accurate view of the LCP experienced by your users.

Optimizing LCP in Gadget 

Common strategies for improving LCP include optimizing static asset loading, deferring JavaScript execution, and minimizing render-blocking resources. Your app bundle size can also impact LCP depending on the amount of content being loaded initially, so it's important to keep your app bundle as small as possible.

Here are some tips for optimizing LCP in Gadget:

Tip 1: Update Gadget-provided packages 

Gadget provides a set of packages used to build your frontend. These packages are updated regularly to include the latest performance improvements and bug fixes.

You can keep these packages up to date to benefit from the latest optimizations:

  • Open the Gadget command palette
  • Select and run Update Gadget-provided packages

Note that some updates that include major version upgrades may require some manual work to resolve breaking changes.

Tip 2: Optimize image loading 

Large images are a common cause of slow LCP scores. You can optimize images by compressing them, using the correct image format, and lazy-loading images that are not immediately visible to the user so they do not affect your LCP score.

Here are some things that can be done to optimize images:

  • Pick the best image format for the job:
    • .webp is great for reducing image size while still allowing for transparency and is supported by all modern web browsers
    • .jpeg is great for detailed images that don't include text
    • .png is better for images with text or simple graphics
    • .svg is great when you need to scale the image to different sizes
  • Resize images to the correct dimensions and avoid using large images that are scaled down using CSS
  • Compress images using tools like TinyPNG
  • Lazy-load images that are not immediately visible to the user
Lazy loading images
html
<img src="{MyNeatImage}" loading="lazy" />
Need a large, high-definition image? Get creative!

Creative strategies can be used to load large, high-definition images as part of a page's main content without negatively impacting LCP.

For example, you can use a tiny version of an image scaled up and blurred as a placeholder while the larger detailed image loads.

Tip 3: Speed up font loading 

Optimizing font loading can also help improve LCP scores.

You can speed up font loading by:

  • Inlining font declarations in the <head> of your index.html file, rather than loading them from an external stylesheet
    • Use @font-face to declare your fonts in your CSS, with an appropriate font-display strategy, such as swap, which has a 0ms block period and will swap in a system font while the custom font is loading (note that this may cause some layout shifts if your fonts are significantly different in size or style)
Example: inlining font declarations
html
1<!-- example source: https://web.dev/articles/font-best-practices -->
2<!-- load woff2 font file from `web/assets` folder in Gadget -->
3<head>
4 <style>
5 @font-face {
6 font-family: "Open Sans";
7 src: url("./web/assets/OpenSans-Regular-webfont.woff2") format("woff2");
8 font-display: "swap";
9 }
10
11 body {
12 font-family: "Open Sans";
13 }
14 </style>
15</head>
  • Use WOFF2 font format for better compression and faster loading times
  • Self-host your fonts by including them as static assets in your Gadget project
    • If you do use a third-party hosted font, you can use a preconnect link to establish a connection to the font server before the browser requests the font
  • Don't use too many fonts on a single page, as each font requires an additional network request and can slow down your LCP score

More information on all of these strategies can be found on the web.dev guide to best practices for fonts.

Tip 4: Reduce bundle size and lazy load 

Large bundle sizes can slow down your LCP score because they take longer for the browser to download and parse. Gadget is already optimizing your bundle for production, but there are additional steps you can take to reduce your bundle size further.

Before you can reduce your bundle size, you need to know what's contributing to it. You can use tools like vite-bundle-visualizer or vite-bundle-analyzer to visualize your bundle size and identify areas for optimization.

  • Use ggt to pull a Gadget project down to your local system and run the following command in your project root to visualize your bundle size:
terminal
npx vite-bundle-visualizer

These tools will give you a visual representation of your bundle size and show you which dependencies are contributing to it. You can then take steps to reduce your bundle size, such as:

  • Removing unused dependencies and packages
  • Splitting your bundle into smaller chunks
    • Vite handles tree-shaking and lazy-loading automatically, so smaller ES modules can lead to better initial load times
  • Use React.lazy to lazy-load components that are not needed for the initial page render
    • Using Suspense along with React.lazy can help manage the loading state of these components and avoid layout shifts
web/components/MyPage.jsx
JavaScript
1import { lazy } from "react";
2import { OtherImportantContent } from "./OtherImportantContent";
3
4const MyComponent = lazy(() => import("./MyComponent"));
5
6function Page() {
7 return (
8 <div>
9 <OtherImportantContent />
10 <Suspense fallback={<div>Loading...</div>}>
11 <p>This will load... eventually</p>
12 <MyComponent />
13 </Suspense>
14 </div>
15 );
16}

Shopify tip: /install-or-render app URL 

All new Gadget apps provide a high-performance /install-or-render route used as the Shopify app URL when a Shopify connection is set up. This route is optimized for fast loading times and will improve your LCP when building embedded Shopify apps.

If you have an older Shopify app built with Gadget that uses a /shopify/install HTTP route for installation, you can update your app to use <your-app-domain>/api/shopify/install-or-render to improve LCP. More information can be found in our Shopify OAuth guide.

More info 

For more information on LCP and how to optimize it, see the Google Web Vitals documentation.