Fix "lcp" not working in production
Technology

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.

2026-06-01
7 min read
Fix "lcp" not working in production

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:

text
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.

js
// 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.

js
// 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;
js
// 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;
js
// 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#

  1. Open pages/_app.js (or create it if it doesn’t exist).
  2. Paste the reportWebVitals export shown above, making sure the path to your API route matches (/api/web-vitals).
  3. Open next.config.js and add the experimental.webVitals: true flag if it isn’t already present.
  4. Create the API route pages/api/web-vitals.js with the handler that logs the payload.
  5. Run a fresh production build: npm run build && npm start.
  6. 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:

bash
npm run build
npm start
text
> 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:

text
🔍 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:

js
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:

js
// .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.

Frequently Asked Questions

|

Have more questions? Contact us

Written by

Mahdi Br
Mahdi Br

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

One email a month — no fluff

RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.