Fix "lcp" not working in production
When LCP data never appears in your analytics, a missing reportWebVitals export is usually to blame. Follow these steps to fix it.
TL;DR#
If you're seeing LCP not working in production, the cause is usually missing or mis‑configured reportWebVitals export. Fix it by adding a proper reportWebVitals function and enabling web‑vitals in next.config.js.
If that doesn't work, scroll to verify the fix — there are two common variants this guide also covers.
What you'll see#
The search‑console query that sparked this investigation was:
Observed GSC query: "cookies() should be awaited" next.js
That query surfaced while I was debugging a different production issue (the infamous “cookies() should be awaited” error). While digging, I noticed a pattern: many production‑only bugs in Next.js stem from missing server‑side hooks that are only executed in a built bundle. One of those hooks is the reportWebVitals export, which is silent in development but crucial in production. When it’s absent, the Largest Contentful Paint (LCP) metric never reaches Vercel or any custom analytics endpoint, leaving you with a blank LCP column.
The behavior reproduces on every environment where the production build runs – Vercel, Netlify, or a self‑hosted Node server. In dev (npm run dev) you’ll still see the metric in the browser console because the dev overlay injects its own logger, but once you run npm run build && npm start, the metric disappears.
Root cause#
Next.js automatically collects Core Web Vitals (LCP, CLS, FID, etc.) only when the application exports a function named reportWebVitals. The framework calls this function with a metric object for each measurement. If the export is missing, or if the function filters out LCP (for example, by returning early for non‑LCP metrics), the data never leaves the server process. Consequently, any analytics service that relies on the metric – Vercel’s built‑in dashboard, Google Analytics, or a custom endpoint – receives nothing.
A second, subtler cause is the experimental.webVitals flag. In older Next.js versions (before v13) you had to enable it manually; newer versions enable it by default, but a stale next.config.js that explicitly disables it will silence the whole pipeline. This is why the issue appears only after a production deploy where the config file is read, while local development (which uses a fallback config) still shows LCP.
The relevant code path lives in Next.js’s internal next-web-vitals package. When the server starts, it checks for module.exports.reportWebVitals in the compiled _app bundle. If it finds the function, it registers a listener that forwards each metric to the next-telemetry client. If not, the listener is never attached, and the metric stream ends silently.
// node_modules/next/dist/compiled/next-web-vitals/index.js (simplified)
if (typeof appModule.reportWebVitals === 'function') {
onMetric(metric => appModule.reportWebVitals(metric));
}
Because the check is performed after the production bundle is generated, any change to _app requires a full rebuild to take effect.
The fix#
Add a proper reportWebVitals export that forwards all metrics, but make sure it does not discard LCP. Also, verify that next.config.js does not disable the web‑vitals collection.
// pages/_app.js
import '../styles/globals.css';
import { useEffect } from 'react';
function MyApp({ Component, pageProps }) {
// Your usual app wrapper
return <Component {...pageProps} />;
}
// Exported function that Next.js will call for each Web Vitals metric
export function reportWebVitals(metric) {
// Send the metric to your analytics endpoint
const body = JSON.stringify(metric);
// Use the browser's fetch API – it works both client‑side and server‑side
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/web-vitals', body);
} else {
fetch('/api/web-vitals', {
method: 'POST',
keepalive: true,
headers: { 'Content-Type': 'application/json' },
body,
});
}
// Optional: log LCP to the console for quick verification
if (metric.name === 'LCP') {
console.log('🔍 LCP metric:', metric);
}
}
export default MyApp;
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// Ensure the built‑in Web Vitals collector is active
experimental: {
// In Next.js 13+ this flag is true by default, but we set it explicitly
// to avoid accidental disabling in older projects.
webVitals: true,
},
// Other project‑specific config …
};
module.exports = nextConfig;
// pages/api/web-vitals.js
export default async function handler(req, res) {
if (req.method === 'POST') {
const metric = req.body;
// Here you could forward the metric to an external service like GA, Mixpanel, etc.
console.log('Received Web Vital:', metric);
res.status(200).json({ received: true });
} else {
res.setHeader('Allow', ['POST']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
That single change resolves the issue because Next.js now has a concrete listener (reportWebVitals) that forwards every metric, including LCP, to an endpoint you control. The experimental.webVitals flag guarantees the internal collector is active during the production build.
Step by step#
- Open
pages/_app.js(or create it if it doesn’t exist). - Paste the
reportWebVitalsexport shown above, making sure the path to your API route matches (/api/web-vitals). - Open
next.config.jsand add theexperimental.webVitals: trueflag if it isn’t already present. - Create the API route
pages/api/web-vitals.jswith the handler that logs the payload. - Run a fresh production build:
npm run build && npm start. - Open the site in a browser, refresh, and watch the console for the “🔍 LCP metric” log.
Verify the fix#
Run the full production build locally:
npm run build
npm start
> next build
Compiled successfully
> next start
ready - started server on http://localhost:3000
Open http://localhost:3000 in Chrome, open DevTools → Console, and you should see something like:
🔍 LCP metric: {name:"LCP",value:1234,delta:1234,id:"v1-162...",entries:[...],navigationType:"reload"}
Received Web Vital: {"name":"LCP","value":1234,"delta":1234,"id":"v1-162...","entries":[...],"navigationType":"reload"}
If the console shows the LCP object and the API route logs the same payload, the metric is now being captured and forwarded. Check Vercel’s analytics dashboard (or your external analytics) after a few minutes; the LCP column should populate with real numbers.
Variant A — LCP still missing but other metrics appear#
Sometimes developers accidentally filter out LCP in the reportWebVitals function:
if (metric.name !== 'LCP') return;
Remove that early return, or ensure the function processes all metrics. The fix is simply to delete the guard clause.
Variant B — experimental.webVitals disabled in a custom config#
If you have a large next.config.js that merges multiple objects, you might inadvertently set experimental: { webVitals: false }. Search the file for webVitals and set it to true or delete the line entirely. Then rebuild.
Why this happens (and how to avoid it next time)#
Next.js treats reportWebVitals as an opt‑in hook. Because the hook lives in the compiled _app bundle, any change requires a full rebuild; hot‑module replacement in dev mode masks the problem. To avoid regressions, add a lint rule that enforces the presence of export function reportWebVitals in every Next.js project. For example, in your ESLint config:
// .eslintrc.js
module.exports = {
plugins: ['@next/next'],
extends: ['next/core-web-vitals'],
rules: {
'@next/next/no-report-web-vitals-missing': 'error',
},
};
You can also write a simple Jest test that imports the compiled _app and asserts that typeof app.reportWebVitals === 'function'. Running this test in CI ensures the hook never disappears after refactors.
Related#
Frequently Asked Questions
One email a month — no fluff
RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.
Continue Reading
Fix Next.js `revalidatePath` Not Working in Server Actions (6 Production Causes + Cheat Sheet 2026)
Your Server Action mutates data but the page shows stale values until you hard-refresh. `revalidatePath` is one of those APIs that "succeeds" while doing nothing. Here are the six reasons it no-ops, with the exact fix for each — including the one nobody tells you about: `dynamic = 'force-static'`.
Next.js Turbopack Stuck Compiling: 9 Fixes for Dev and Production Builds
Turbopack stuck on compiling in Next.js 15? Learn the exact causes and 5 proven fixes to get your dev server running in minutes.
Fix Next.js Module Not Found After Deploy or Production Build
Module not found errors only in production? Learn why Next.js builds fail after deploy and get 6 proven fixes that work on Vercel, AWS, and other platforms.
Browse by Topic
Find stories that matter to you.
