diff --git a/.size-limit.js b/.size-limit.js
index c1725577c856..61fb027289d3 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -38,21 +38,21 @@ module.exports = [
     path: 'packages/browser/build/npm/esm/index.js',
     import: createImport('init', 'browserTracingIntegration'),
     gzip: true,
-    limit: '39 KB',
+    limit: '40 KB',
   },
   {
     name: '@sentry/browser (incl. Tracing, Replay)',
     path: 'packages/browser/build/npm/esm/index.js',
     import: createImport('init', 'browserTracingIntegration', 'replayIntegration'),
     gzip: true,
-    limit: '77 KB',
+    limit: '80 KB',
   },
   {
     name: '@sentry/browser (incl. Tracing, Replay) - with treeshaking flags',
     path: 'packages/browser/build/npm/esm/index.js',
     import: createImport('init', 'browserTracingIntegration', 'replayIntegration'),
     gzip: true,
-    limit: '70.1 KB',
+    limit: '75 KB',
     modifyWebpackConfig: function (config) {
       const webpack = require('webpack');
 
@@ -156,7 +156,7 @@ module.exports = [
     name: 'CDN Bundle (incl. Tracing)',
     path: createCDNPath('bundle.tracing.min.js'),
     gzip: true,
-    limit: '39 KB',
+    limit: '41 KB',
   },
   {
     name: 'CDN Bundle (incl. Tracing, Replay)',
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/assets/sentry-logo-600x179.png b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/assets/sentry-logo-600x179.png
new file mode 100644
index 000000000000..353b7233d6bf
Binary files /dev/null and b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/assets/sentry-logo-600x179.png differ
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/init.js b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/init.js
new file mode 100644
index 000000000000..8da426e106b8
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/init.js
@@ -0,0 +1,17 @@
+import * as Sentry from '@sentry/browser';
+
+window.Sentry = Sentry;
+
+Sentry.init({
+  dsn: 'https://public@dsn.ingest.sentry.io/1337',
+  integrations: [
+    Sentry.browserTracingIntegration({
+      idleTimeout: 9000,
+      _experiments: {
+        enableStandaloneLcpSpans: true,
+      },
+    }),
+  ],
+  tracesSampleRate: 1,
+  debug: true,
+});
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/template.html b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/template.html
new file mode 100644
index 000000000000..ef5d3bac0018
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/template.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8" />
+  </head>
+  <body>
+    <div id="content"></div>
+    <img src="https://sentry-test-site.example/my/image.png" />
+  </body>
+</html>
diff --git a/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/test.ts b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/test.ts
new file mode 100644
index 000000000000..d0fa133f9567
--- /dev/null
+++ b/dev-packages/browser-integration-tests/suites/tracing/metrics/web-vitals-lcp-standalone-spans/test.ts
@@ -0,0 +1,356 @@
+import type { Page, Route } from '@playwright/test';
+import { expect } from '@playwright/test';
+import type { Event as SentryEvent, EventEnvelope, SpanEnvelope } from '@sentry/core';
+import { sentryTest } from '../../../../utils/fixtures';
+import {
+  getFirstSentryEnvelopeRequest,
+  getMultipleSentryEnvelopeRequests,
+  properFullEnvelopeRequestParser,
+  shouldSkipTracingTest,
+} from '../../../../utils/helpers';
+
+sentryTest.beforeEach(async ({ browserName, page }) => {
+  if (shouldSkipTracingTest() || browserName !== 'chromium') {
+    sentryTest.skip();
+  }
+
+  await page.setViewportSize({ width: 800, height: 1200 });
+});
+
+function hidePage(page: Page): Promise<void> {
+  return page.evaluate(() => {
+    window.dispatchEvent(new Event('pagehide'));
+  });
+}
+
+sentryTest('captures LCP vital as a standalone span', async ({ getLocalTestUrl, page }) => {
+  const spanEnvelopePromise = getMultipleSentryEnvelopeRequests<SpanEnvelope>(
+    page,
+    1,
+    { envelopeType: 'span' },
+    properFullEnvelopeRequestParser,
+  );
+
+  page.route('**', route => route.continue());
+  page.route('**/my/image.png', async (route: Route) => {
+    return route.fulfill({
+      path: `${__dirname}/assets/sentry-logo-600x179.png`,
+    });
+  });
+
+  const url = await getLocalTestUrl({ testDir: __dirname });
+  await page.goto(url);
+
+  // Wait for LCP to be captured
+  await page.waitForTimeout(1000);
+
+  await hidePage(page);
+
+  const spanEnvelope = (await spanEnvelopePromise)[0];
+
+  const spanEnvelopeHeaders = spanEnvelope[0];
+  const spanEnvelopeItem = spanEnvelope[1][0][1];
+
+  expect(spanEnvelopeItem).toEqual({
+    data: {
+      'sentry.exclusive_time': 0,
+      'sentry.op': 'ui.webvital.lcp',
+      'sentry.origin': 'auto.http.browser.lcp',
+      transaction: expect.stringContaining('index.html'),
+      'user_agent.original': expect.stringContaining('Chrome'),
+      'sentry.pageload.span_id': expect.stringMatching(/[a-f0-9]{16}/),
+      'lcp.element': 'body > img',
+      'lcp.id': '',
+      'lcp.loadTime': expect.any(Number),
+      'lcp.renderTime': expect.any(Number),
+      'lcp.size': expect.any(Number),
+      'lcp.url': 'https://sentry-test-site.example/my/image.png',
+    },
+    description: expect.stringContaining('body > img'),
+    exclusive_time: 0,
+    measurements: {
+      lcp: {
+        unit: 'millisecond',
+        value: expect.any(Number),
+      },
+    },
+    op: 'ui.webvital.lcp',
+    origin: 'auto.http.browser.lcp',
+    parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+    span_id: expect.stringMatching(/[a-f0-9]{16}/),
+    segment_id: expect.stringMatching(/[a-f0-9]{16}/),
+    start_timestamp: expect.any(Number),
+    timestamp: spanEnvelopeItem.start_timestamp, // LCP is a point-in-time metric
+    trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+  });
+
+  // LCP value should be greater than 0
+  expect(spanEnvelopeItem.measurements?.lcp?.value).toBeGreaterThan(0);
+
+  expect(spanEnvelopeHeaders).toEqual({
+    sent_at: expect.any(String),
+    trace: {
+      environment: 'production',
+      public_key: 'public',
+      sample_rate: '1',
+      sampled: 'true',
+      trace_id: spanEnvelopeItem.trace_id,
+      sample_rand: expect.any(String),
+      // no transaction, because span source is URL
+    },
+  });
+});
+
+sentryTest('LCP span is linked to pageload transaction', async ({ getLocalTestUrl, page }) => {
+  page.route('**', route => route.continue());
+  page.route('**/my/image.png', async (route: Route) => {
+    return route.fulfill({
+      path: `${__dirname}/assets/sentry-logo-600x179.png`,
+    });
+  });
+
+  const url = await getLocalTestUrl({ testDir: __dirname });
+
+  const eventData = await getFirstSentryEnvelopeRequest<SentryEvent>(page, url);
+
+  expect(eventData.type).toBe('transaction');
+  expect(eventData.contexts?.trace?.op).toBe('pageload');
+
+  const pageloadSpanId = eventData.contexts?.trace?.span_id;
+  const pageloadTraceId = eventData.contexts?.trace?.trace_id;
+
+  expect(pageloadSpanId).toMatch(/[a-f0-9]{16}/);
+  expect(pageloadTraceId).toMatch(/[a-f0-9]{32}/);
+
+  const spanEnvelopePromise = getMultipleSentryEnvelopeRequests<SpanEnvelope>(
+    page,
+    1,
+    { envelopeType: 'span' },
+    properFullEnvelopeRequestParser,
+  );
+
+  // Wait for LCP to be captured
+  await page.waitForTimeout(1000);
+
+  await hidePage(page);
+
+  const spanEnvelope = (await spanEnvelopePromise)[0];
+  const spanEnvelopeItem = spanEnvelope[1][0][1];
+
+  // Ensure the LCP span is connected to the pageload span and trace
+  expect(spanEnvelopeItem.data?.['sentry.pageload.span_id']).toBe(pageloadSpanId);
+  expect(spanEnvelopeItem.trace_id).toEqual(pageloadTraceId);
+  expect(spanEnvelopeItem.measurements?.lcp?.value).toBeGreaterThan(0);
+});
+
+sentryTest('sends LCP of the initial page when soft-navigating to a new page', async ({ getLocalTestUrl, page }) => {
+  page.route('**', route => route.continue());
+  page.route('**/my/image.png', async (route: Route) => {
+    return route.fulfill({
+      path: `${__dirname}/assets/sentry-logo-600x179.png`,
+    });
+  });
+
+  const url = await getLocalTestUrl({ testDir: __dirname });
+
+  const eventData = await getFirstSentryEnvelopeRequest<SentryEvent>(page, url);
+
+  expect(eventData.type).toBe('transaction');
+  expect(eventData.contexts?.trace?.op).toBe('pageload');
+
+  const spanEnvelopePromise = getMultipleSentryEnvelopeRequests<SpanEnvelope>(
+    page,
+    1,
+    { envelopeType: 'span' },
+    properFullEnvelopeRequestParser,
+  );
+
+  // Wait for LCP to be captured
+  await page.waitForTimeout(1000);
+
+  await page.goto(`${url}#soft-navigation`);
+
+  const spanEnvelope = (await spanEnvelopePromise)[0];
+  const spanEnvelopeItem = spanEnvelope[1][0][1];
+
+  expect(spanEnvelopeItem.measurements?.lcp?.value).toBeGreaterThan(0);
+  expect(spanEnvelopeItem.data?.['sentry.pageload.span_id']).toMatch(/[a-f0-9]{16}/);
+});
+
+sentryTest("doesn't send further LCP after the first navigation", async ({ getLocalTestUrl, page }) => {
+  page.route('**', route => route.continue());
+  page.route('**/my/image.png', async (route: Route) => {
+    return route.fulfill({
+      path: `${__dirname}/assets/sentry-logo-600x179.png`,
+    });
+  });
+
+  const url = await getLocalTestUrl({ testDir: __dirname });
+
+  const eventData = await getFirstSentryEnvelopeRequest<SentryEvent>(page, url);
+
+  expect(eventData.type).toBe('transaction');
+  expect(eventData.contexts?.trace?.op).toBe('pageload');
+
+  const spanEnvelopePromise = getMultipleSentryEnvelopeRequests<SpanEnvelope>(
+    page,
+    1,
+    { envelopeType: 'span' },
+    properFullEnvelopeRequestParser,
+  );
+
+  // Wait for LCP to be captured
+  await page.waitForTimeout(1000);
+
+  await page.goto(`${url}#soft-navigation`);
+
+  const spanEnvelope = (await spanEnvelopePromise)[0];
+  const spanEnvelopeItem = spanEnvelope[1][0][1];
+  expect(spanEnvelopeItem.measurements?.lcp?.value).toBeGreaterThan(0);
+
+  getMultipleSentryEnvelopeRequests<SpanEnvelope>(page, 1, { envelopeType: 'span' }, () => {
+    throw new Error('Unexpected span - This should not happen!');
+  });
+
+  const navigationTxnPromise = getMultipleSentryEnvelopeRequests<EventEnvelope>(
+    page,
+    1,
+    { envelopeType: 'transaction' },
+    properFullEnvelopeRequestParser,
+  );
+
+  // activate both LCP emission triggers:
+  await page.goto(`${url}#soft-navigation-2`);
+  await hidePage(page);
+
+  // assumption: If we would send another LCP span on the 2nd navigation, it would be sent before the navigation
+  // transaction ends. This isn't 100% safe to ensure we don't send something but otherwise we'd need to wait for
+  // a timeout or something similar.
+  await navigationTxnPromise;
+});
+
+sentryTest("doesn't send further LCP after the first page hide", async ({ getLocalTestUrl, page }) => {
+  page.route('**', route => route.continue());
+  page.route('**/my/image.png', async (route: Route) => {
+    return route.fulfill({
+      path: `${__dirname}/assets/sentry-logo-600x179.png`,
+    });
+  });
+
+  const url = await getLocalTestUrl({ testDir: __dirname });
+
+  const eventData = await getFirstSentryEnvelopeRequest<SentryEvent>(page, url);
+
+  expect(eventData.type).toBe('transaction');
+  expect(eventData.contexts?.trace?.op).toBe('pageload');
+
+  const spanEnvelopePromise = getMultipleSentryEnvelopeRequests<SpanEnvelope>(
+    page,
+    1,
+    { envelopeType: 'span' },
+    properFullEnvelopeRequestParser,
+  );
+
+  // Wait for LCP to be captured
+  await page.waitForTimeout(1000);
+
+  await hidePage(page);
+
+  const spanEnvelope = (await spanEnvelopePromise)[0];
+  const spanEnvelopeItem = spanEnvelope[1][0][1];
+  expect(spanEnvelopeItem.measurements?.lcp?.value).toBeGreaterThan(0);
+
+  getMultipleSentryEnvelopeRequests<SpanEnvelope>(page, 1, { envelopeType: 'span' }, () => {
+    throw new Error('Unexpected span - This should not happen!');
+  });
+
+  const navigationTxnPromise = getMultipleSentryEnvelopeRequests<EventEnvelope>(
+    page,
+    1,
+    { envelopeType: 'transaction' },
+    properFullEnvelopeRequestParser,
+  );
+
+  // activate both LCP emission triggers:
+  await page.goto(`${url}#soft-navigation-2`);
+  await hidePage(page);
+
+  // assumption: If we would send another LCP span on the 2nd navigation, it would be sent before the navigation
+  // transaction ends. This isn't 100% safe to ensure we don't send something but otherwise we'd need to wait for
+  // a timeout or something similar.
+  await navigationTxnPromise;
+});
+
+sentryTest('LCP span timestamps are set correctly', async ({ getLocalTestUrl, page }) => {
+  page.route('**', route => route.continue());
+  page.route('**/my/image.png', async (route: Route) => {
+    return route.fulfill({
+      path: `${__dirname}/assets/sentry-logo-600x179.png`,
+    });
+  });
+
+  const url = await getLocalTestUrl({ testDir: __dirname });
+
+  const eventData = await getFirstSentryEnvelopeRequest<SentryEvent>(page, url);
+
+  expect(eventData.type).toBe('transaction');
+  expect(eventData.contexts?.trace?.op).toBe('pageload');
+  expect(eventData.timestamp).toBeDefined();
+
+  const pageloadEndTimestamp = eventData.timestamp!;
+
+  const spanEnvelopePromise = getMultipleSentryEnvelopeRequests<SpanEnvelope>(
+    page,
+    1,
+    { envelopeType: 'span' },
+    properFullEnvelopeRequestParser,
+  );
+
+  // Wait for LCP to be captured
+  await page.waitForTimeout(1000);
+
+  await hidePage(page);
+
+  const spanEnvelope = (await spanEnvelopePromise)[0];
+  const spanEnvelopeItem = spanEnvelope[1][0][1];
+
+  expect(spanEnvelopeItem.start_timestamp).toBeDefined();
+  expect(spanEnvelopeItem.timestamp).toBeDefined();
+
+  const lcpSpanStartTimestamp = spanEnvelopeItem.start_timestamp!;
+  const lcpSpanEndTimestamp = spanEnvelopeItem.timestamp!;
+
+  // LCP is a point-in-time metric ==> start and end timestamp should be the same
+  expect(lcpSpanStartTimestamp).toEqual(lcpSpanEndTimestamp);
+
+  // We don't really care that they are very close together but rather about the order of magnitude
+  // Previously, we had a bug where the timestamps would be significantly off (by multiple hours)
+  // so we only ensure that this bug is fixed. 60 seconds should be more than enough.
+  expect(lcpSpanStartTimestamp - pageloadEndTimestamp).toBeLessThan(60);
+});
+
+sentryTest(
+  'pageload transaction does not contain LCP measurement when standalone spans are enabled',
+  async ({ getLocalTestUrl, page }) => {
+    page.route('**', route => route.continue());
+    page.route('**/my/image.png', async (route: Route) => {
+      return route.fulfill({
+        path: `${__dirname}/assets/sentry-logo-600x179.png`,
+      });
+    });
+
+    const url = await getLocalTestUrl({ testDir: __dirname });
+    const eventData = await getFirstSentryEnvelopeRequest<SentryEvent>(page, url);
+
+    expect(eventData.type).toBe('transaction');
+    expect(eventData.contexts?.trace?.op).toBe('pageload');
+
+    // LCP measurement should NOT be present on the pageload transaction when standalone spans are enabled
+    expect(eventData.measurements?.lcp).toBeUndefined();
+
+    // LCP attributes should also NOT be present on the pageload transaction when standalone spans are enabled
+    // because the LCP data is sent as a standalone span instead
+    expect(eventData.contexts?.trace?.data?.['lcp.element']).toBeUndefined();
+    expect(eventData.contexts?.trace?.data?.['lcp.size']).toBeUndefined();
+  },
+);
diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts
index 4c5e78899b29..e573ca441b03 100644
--- a/packages/browser-utils/src/metrics/browserMetrics.ts
+++ b/packages/browser-utils/src/metrics/browserMetrics.ts
@@ -22,6 +22,7 @@ import {
   addPerformanceInstrumentationHandler,
   addTtfbInstrumentationHandler,
 } from './instrument';
+import { trackLcpAsStandaloneSpan } from './lcp';
 import {
   extractNetworkProtocol,
   getBrowserPerformanceAPI,
@@ -81,6 +82,7 @@ let _clsEntry: LayoutShift | undefined;
 
 interface StartTrackingWebVitalsOptions {
   recordClsStandaloneSpans: boolean;
+  recordLcpStandaloneSpans: boolean;
 }
 
 /**
@@ -89,7 +91,10 @@ interface StartTrackingWebVitalsOptions {
  *
  * @returns A function that forces web vitals collection
  */
-export function startTrackingWebVitals({ recordClsStandaloneSpans }: StartTrackingWebVitalsOptions): () => void {
+export function startTrackingWebVitals({
+  recordClsStandaloneSpans,
+  recordLcpStandaloneSpans,
+}: StartTrackingWebVitalsOptions): () => void {
   const performance = getBrowserPerformanceAPI();
   if (performance && browserPerformanceTimeOrigin()) {
     // @ts-expect-error we want to make sure all of these are available, even if TS is sure they are
@@ -97,13 +102,13 @@ export function startTrackingWebVitals({ recordClsStandaloneSpans }: StartTracki
       WINDOW.performance.mark('sentry-tracing-init');
     }
     const fidCleanupCallback = _trackFID();
-    const lcpCleanupCallback = _trackLCP();
+    const lcpCleanupCallback = recordLcpStandaloneSpans ? trackLcpAsStandaloneSpan() : _trackLCP();
     const ttfbCleanupCallback = _trackTtfb();
     const clsCleanupCallback = recordClsStandaloneSpans ? trackClsAsStandaloneSpan() : _trackCLS();
 
     return (): void => {
       fidCleanupCallback();
-      lcpCleanupCallback();
+      lcpCleanupCallback?.();
       ttfbCleanupCallback();
       clsCleanupCallback?.();
     };
@@ -298,11 +303,23 @@ function _trackTtfb(): () => void {
 
 interface AddPerformanceEntriesOptions {
   /**
-   * Flag to determine if CLS should be recorded as a measurement on the span or
+   * Flag to determine if CLS should be recorded as a measurement on the pageload span or
    * sent as a standalone span instead.
+   * Sending it as a standalone span will yield more accurate LCP values.
+   *
+   * Default: `false` for backwards compatibility.
    */
   recordClsOnPageloadSpan: boolean;
 
+  /**
+   * Flag to determine if LCP should be recorded as a measurement on the pageload span or
+   * sent as a standalone span instead.
+   * Sending it as a standalone span will yield more accurate LCP values.
+   *
+   * Default: `false` for backwards compatibility.
+   */
+  recordLcpOnPageloadSpan: boolean;
+
   /**
    * Resource spans with `op`s matching strings in the array will not be emitted.
    *
@@ -418,6 +435,11 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries
       delete _measurements.cls;
     }
 
+    // If LCP standalone spans are enabled, don't record LCP as a measurement
+    if (!options.recordLcpOnPageloadSpan) {
+      delete _measurements.lcp;
+    }
+
     Object.entries(_measurements).forEach(([measurementName, measurement]) => {
       setMeasurement(measurementName, measurement.value, measurement.unit);
     });
@@ -433,7 +455,7 @@ export function addPerformanceEntries(span: Span, options: AddPerformanceEntries
     // the `activationStart` attribute of the "navigation" PerformanceEntry.
     span.setAttribute('performance.activationStart', getActivationStart());
 
-    _setWebVitalAttributes(span);
+    _setWebVitalAttributes(span, options);
   }
 
   _lcpEntry = undefined;
@@ -742,8 +764,9 @@ function _trackNavigator(span: Span): void {
 }
 
 /** Add LCP / CLS data to span to allow debugging */
-function _setWebVitalAttributes(span: Span): void {
-  if (_lcpEntry) {
+function _setWebVitalAttributes(span: Span, options: AddPerformanceEntriesOptions): void {
+  // Only add LCP attributes if LCP is being recorded on the pageload span
+  if (_lcpEntry && options.recordLcpOnPageloadSpan) {
     // Capture Properties of the LCP element that contributes to the LCP.
 
     if (_lcpEntry.element) {
@@ -774,8 +797,8 @@ function _setWebVitalAttributes(span: Span): void {
     span.setAttribute('lcp.size', _lcpEntry.size);
   }
 
-  // See: https://developer.mozilla.org/en-US/docs/Web/API/LayoutShift
-  if (_clsEntry?.sources) {
+  // Only add CLS attributes if CLS is being recorded on the pageload span
+  if (_clsEntry?.sources && options.recordClsOnPageloadSpan) {
     _clsEntry.sources.forEach((source, index) =>
       span.setAttribute(`cls.source.${index + 1}`, htmlTreeAsString(source.node)),
     );
diff --git a/packages/browser-utils/src/metrics/cls.ts b/packages/browser-utils/src/metrics/cls.ts
index 435a1b6f7a1b..5e9b646fde89 100644
--- a/packages/browser-utils/src/metrics/cls.ts
+++ b/packages/browser-utils/src/metrics/cls.ts
@@ -58,8 +58,6 @@ export function trackClsAsStandaloneSpan(): void {
     standaloneClsEntry = entry;
   }, true);
 
-  // TODO: Figure out if we can switch to using whenIdleOrHidden instead of onHidden
-  // use pagehide event from web-vitals
   onHidden(() => {
     _collectClsOnce();
   });
diff --git a/packages/browser-utils/src/metrics/lcp.ts b/packages/browser-utils/src/metrics/lcp.ts
new file mode 100644
index 000000000000..4d4fbda8f979
--- /dev/null
+++ b/packages/browser-utils/src/metrics/lcp.ts
@@ -0,0 +1,140 @@
+import type { SpanAttributes } from '@sentry/core';
+import {
+  browserPerformanceTimeOrigin,
+  getActiveSpan,
+  getClient,
+  getCurrentScope,
+  getRootSpan,
+  htmlTreeAsString,
+  logger,
+  SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME,
+  SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT,
+  SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE,
+  SEMANTIC_ATTRIBUTE_SENTRY_OP,
+  SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
+  spanToJSON,
+} from '@sentry/core';
+import { DEBUG_BUILD } from '../debug-build';
+import { addLcpInstrumentationHandler } from './instrument';
+import { msToSec, startStandaloneWebVitalSpan } from './utils';
+import { onHidden } from './web-vitals/lib/onHidden';
+
+/**
+ * Starts tracking the Largest Contentful Paint on the current page and collects the value once
+ *
+ * - the page visibility is hidden
+ * - a navigation span is started (to stop LCP measurement for SPA soft navigations)
+ *
+ * Once either of these events triggers, the LCP value is sent as a standalone span and we stop
+ * measuring LCP for subsequent routes.
+ */
+export function trackLcpAsStandaloneSpan(): void {
+  let standaloneLcpValue = 0;
+  let standaloneLcpEntry: LargestContentfulPaint | undefined;
+  let pageloadSpanId: string | undefined;
+
+  if (!supportsLargestContentfulPaint()) {
+    return;
+  }
+
+  let sentSpan = false;
+  function _collectLcpOnce() {
+    if (sentSpan) {
+      return;
+    }
+    sentSpan = true;
+    if (pageloadSpanId) {
+      sendStandaloneLcpSpan(standaloneLcpValue, standaloneLcpEntry, pageloadSpanId);
+    }
+    cleanupLcpHandler();
+  }
+
+  const cleanupLcpHandler = addLcpInstrumentationHandler(({ metric }) => {
+    const entry = metric.entries[metric.entries.length - 1] as LargestContentfulPaint | undefined;
+    if (!entry) {
+      return;
+    }
+    standaloneLcpValue = metric.value;
+    standaloneLcpEntry = entry;
+  }, true);
+
+  onHidden(() => {
+    _collectLcpOnce();
+  });
+
+  // Since the call chain of this function is synchronous and evaluates before the SDK client is created,
+  // we need to wait with subscribing to a client hook until the client is created. Therefore, we defer
+  // to the next tick after the SDK setup.
+  setTimeout(() => {
+    const client = getClient();
+
+    if (!client) {
+      return;
+    }
+
+    const unsubscribeStartNavigation = client.on('startNavigationSpan', () => {
+      _collectLcpOnce();
+      unsubscribeStartNavigation?.();
+    });
+
+    const activeSpan = getActiveSpan();
+    if (activeSpan) {
+      const rootSpan = getRootSpan(activeSpan);
+      const spanJSON = spanToJSON(rootSpan);
+      if (spanJSON.op === 'pageload') {
+        pageloadSpanId = rootSpan.spanContext().spanId;
+      }
+    }
+  }, 0);
+}
+
+function sendStandaloneLcpSpan(lcpValue: number, entry: LargestContentfulPaint | undefined, pageloadSpanId: string) {
+  DEBUG_BUILD && logger.log(`Sending LCP span (${lcpValue})`);
+
+  const startTime = msToSec((browserPerformanceTimeOrigin() || 0) + (entry?.startTime || 0));
+  const routeName = getCurrentScope().getScopeData().transactionName;
+
+  const name = entry ? htmlTreeAsString(entry.element) : 'Largest contentful paint';
+
+  const attributes: SpanAttributes = {
+    [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.browser.lcp',
+    [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.webvital.lcp',
+    [SEMANTIC_ATTRIBUTE_EXCLUSIVE_TIME]: 0, // LCP is a point-in-time metric
+    // attach the pageload span id to the LCP span so that we can link them in the UI
+    'sentry.pageload.span_id': pageloadSpanId,
+  };
+
+  if (entry) {
+    attributes['lcp.element'] = htmlTreeAsString(entry.element);
+    attributes['lcp.id'] = entry.id;
+    attributes['lcp.url'] = entry.url;
+    attributes['lcp.loadTime'] = entry.loadTime;
+    attributes['lcp.renderTime'] = entry.renderTime;
+    attributes['lcp.size'] = entry.size;
+  }
+
+  const span = startStandaloneWebVitalSpan({
+    name,
+    transaction: routeName,
+    attributes,
+    startTime,
+  });
+
+  if (span) {
+    span.addEvent('lcp', {
+      [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT]: 'millisecond',
+      [SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE]: lcpValue,
+    });
+
+    // LCP is a point-in-time metric, so we end the span immediately
+    span.end(startTime);
+  }
+}
+
+function supportsLargestContentfulPaint(): boolean {
+  try {
+    return PerformanceObserver.supportedEntryTypes.includes('largest-contentful-paint');
+  } catch {
+    return false;
+  }
+}
diff --git a/packages/browser/src/tracing/browserTracingIntegration.ts b/packages/browser/src/tracing/browserTracingIntegration.ts
index 1ba733ac4ca8..af742310c37f 100644
--- a/packages/browser/src/tracing/browserTracingIntegration.ts
+++ b/packages/browser/src/tracing/browserTracingIntegration.ts
@@ -236,6 +236,7 @@ export interface BrowserTracingOptions {
   _experiments: Partial<{
     enableInteractions: boolean;
     enableStandaloneClsSpans: boolean;
+    enableStandaloneLcpSpans: boolean;
   }>;
 
   /**
@@ -301,7 +302,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
     enableInp,
     enableLongTask,
     enableLongAnimationFrame,
-    _experiments: { enableInteractions, enableStandaloneClsSpans },
+    _experiments: { enableInteractions, enableStandaloneClsSpans, enableStandaloneLcpSpans },
     beforeStartSpan,
     idleTimeout,
     finalTimeout,
@@ -358,6 +359,7 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
         _collectWebVitals?.();
         addPerformanceEntries(span, {
           recordClsOnPageloadSpan: !enableStandaloneClsSpans,
+          recordLcpOnPageloadSpan: !enableStandaloneLcpSpans,
           ignoreResourceSpans,
           ignorePerformanceApiSpans,
         });
@@ -400,7 +402,10 @@ export const browserTracingIntegration = ((_options: Partial<BrowserTracingOptio
     setup(client) {
       registerSpanErrorInstrumentation();
 
-      _collectWebVitals = startTrackingWebVitals({ recordClsStandaloneSpans: enableStandaloneClsSpans || false });
+      _collectWebVitals = startTrackingWebVitals({
+        recordClsStandaloneSpans: enableStandaloneClsSpans || false,
+        recordLcpStandaloneSpans: enableStandaloneLcpSpans || false,
+      });
 
       if (enableInp) {
         startTrackingINP();