Largest Contentful Paint (LCP) is a critical Core Web Vital that measures how quickly the main content of your page becomes visible. In Remix applications, common issues include unoptimized images, render-blocking resources, and inefficient data loading patterns that delay the largest visible element.
This guide walks through five targeted fixes for LCP in Remix, with real code examples and before/after performance comparisons. Each step addresses a specific bottleneck in the Remix rendering pipeline.
Expected results
Following all five steps typically produces these improvements:
Remix loaders run on the server before any HTML is sent to the browser. Slow loaders directly increase TTFB and LCP. Parallelize database queries, use defer() for non-critical data, and cache frequently accessed data. Move slow API calls out of the critical rendering path.
Common mistake: Every millisecond spent in your loader directly delays LCP. If your loader makes sequential API calls, refactor to use Promise.all() for parallel execution. For data that is not needed for the initial render, use defer().
import type { LoaderFunctionArgs } from "@remix-run/node";
import { defer } from "@remix-run/node";
export async function loader({ params }: LoaderFunctionArgs) {
// Critical data: fetched immediately (blocks render)
const product = await db.product.findUnique({ where: { id: params.id } });
// Non-critical data: deferred (streams in later)
const reviewsPromise = db.review.findMany({ where: { productId: params.id } });
const relatedPromise = db.product.findMany({
where: { categoryId: product.categoryId }
});
return defer({
product, // Available immediately
reviews: reviewsPromise, // Streams in when ready
related: relatedPromise, // Streams in when ready
});
}
Use Remix streaming for faster first paint
Remix supports streaming SSR out of the box. When you use defer() in your loader, Remix sends the HTML shell and critical data immediately, then streams deferred data as it resolves. Use <Suspense> and <Await> in your component to render a shell while deferred data loads.
TSX -- Streaming component
import { Await, useLoaderData } from "@remix-run/react";
import { Suspense } from "react";
export default function ProductPage() {
const { product, reviews, related } = useLoaderData();
return (
{/* Critical content renders immediately -- this is the LCP */}
Remix's links export lets you define resource hints that are added to the HTML head. Use this to preload the LCP image, critical fonts, and any other resources needed for the initial render. Remix automatically includes these in the streamed HTML head before the body renders.
Remix does not include a built-in image optimization component like Next.js. Use the HTML <picture> element with multiple sources for AVIF/WebP fallback chains, and srcset with sizes for responsive images. For production, pair with an image CDN (Cloudinary, Imgix, or Cloudflare Images) for on-the-fly optimization.
Cache loader responses and static assets aggressively
Remix gives you full control over HTTP caching headers. Use Cache-Control headers in your loaders to enable CDN caching of page responses. For static assets, Remix's built-in fingerprinting enables immutable caching. Proper caching turns subsequent visits into near-instant loads.
LCP image has fetchPriority="high" and loading="eager"
Critical resources preloaded via links export
Images use <picture> with AVIF/WebP sources and srcset
Loader responses have Cache-Control headers
Streaming SSR enabled with <Suspense> and <Await>
Static assets fingerprinted for immutable caching
Frequently asked questions
A well-optimized Remix application should achieve LCP under 1.5 seconds. With streaming SSR and deferred data, Remix can send the initial HTML shell quickly while non-critical data loads in the background. CrUX data shows Remix sites have a 71% CWV pass rate, benefiting from its server-first architecture and efficient data loading patterns.
Remix streaming sends the HTML head and above-the-fold content immediately, without waiting for all data to resolve. When you use defer() for non-critical data, the browser starts rendering the page and loading resources (images, fonts, CSS) while the server continues processing. The LCP element (hero image, headline) renders before deferred data arrives.
Unlike Next.js, Remix does not include a built-in image component. For production sites, use an image CDN (Cloudinary, Imgix, Cloudflare Images) that handles format conversion, resizing, and CDN delivery. For simpler setups, use the HTML picture element with pre-generated image variants at build time. Libraries like remix-image or unpic provide image component abstractions.
Remix and Next.js take different approaches. Remix's streaming SSR and loader-based data fetching can achieve excellent LCP by sending the HTML shell immediately. Next.js offers more built-in optimizations (next/image, next/font) but requires more configuration. CrUX data shows Remix at 71% CWV pass rate versus Next.js at 68%.
Remix has merged with React Router as of v7. If you are starting a new project, use React Router 7 with the Remix-style file conventions and loaders. The performance characteristics are identical -- the same streaming SSR, loaders, and resource preloading. Existing Remix apps can upgrade to React Router 7 without performance regressions.
Google rates LCP as 'good' when it is under 2.5 seconds at the 75th percentile. For Remix applications specifically, aim for under 2.0 seconds. Measure with field data from Chrome User Experience Report (CrUX) through PageSpeed Insights, as lab tests may not reflect real-user experience with third-party scripts and varying network conditions.