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.
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 onLCP
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:
- Add the
web-vitals
package to your project using the Gadget command palette
terminalyarn add web-vitals
- Import and use the
onLCP
function in your app
1// additional imports...2import { onLCP } from "web-vitals";34const root = document.getElementById("root");5if (!root) throw new Error("#root element not found for booting react app");67// onLCP is a callback that receives the LCP score as a parameter8// you can also send the LCP score to your backend for further analysis9onLCP(console.log);1011ReactDOM.createRoot(root).render(12 <React.StrictMode>13 <AppProvider i18n={enTranslations}>14 <App />15 </AppProvider>16 </React.StrictMode>17);
1// additional imports...2import { onLCP } from "web-vitals";34const root = document.getElementById("root");5if (!root) throw new Error("#root element not found for booting react app");67// onLCP is a callback that receives the LCP score as a parameter8// you can also send the LCP score to your backend for further analysis9onLCP(console.log);1011ReactDOM.createRoot(root).render(12 <React.StrictMode>13 <AppProvider i18n={enTranslations}>14 <App />15 </AppProvider>16 </React.StrictMode>17);
The onLCP
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.
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 0: Use Gadget's pre-built performance optimizations
When you create a new Gadget app, some performance optimizations are already included to help improve your LCP score!
At this time, these benefits are primarily seen on your app's root route /
, for example sample.gadget.app/
. This route is optimized to not require a cold boot of your serverless frontend, which improves loading times and your LCP score.
To take advantage of this optimization, it is important not to change the default entry point for your app to a different route.
Shopify apps can take advantage of Shopify-managed installs which speeds up the initial render by eliminating browser redirects when loading your app frontend.
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 imageshtml<img src="{MyNeatImage}" loading="lazy" />
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 yourindex.html
file, rather than loading them from an external stylesheet- Use
@font-face
to declare your fonts in your CSS, with an appropriatefont-display
strategy, such asswap
, 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)
- Use
Example: inlining font declarationshtml1<!-- 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 }1011 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
- If you do use a third-party hosted font, you can use a
- 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:
terminalnpx 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 withReact.lazy
can help manage the loading state of these components and avoid layout shifts
- Using
1import { lazy } from "react";2import { OtherImportantContent } from "./OtherImportantContent";34const MyComponent = lazy(() => import("./MyComponent"));56function 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}
1import { lazy } from "react";2import { OtherImportantContent } from "./OtherImportantContent";34const MyComponent = lazy(() => import("./MyComponent"));56function 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: /api/shopify/install-or-render
app URL
Gadget apps using the Shopify plugin have a purpose-built, high-performance route for use as the Shopify App URL in Shopify's Partners dashboard: /api/shopify/install-or-render
.
This route is highly optimized to do the minimum number of redirects to render your app while still properly checking authentication and has no cold start times. Using this URL as your Shopify App URL in the Shopify Partners dashboard will improve your LCP when building embedded Shopify apps.
https://example-app.gadget.appapi/shopify/install-or-render
If you have an older Shopify app built with Gadget that uses a /shopify/install
HTTP route for installation, you should update your app to use this URL 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.