diff --git a/.size-limit.js b/.size-limit.js
index 2e7899cb934a..5a2ff44cc32d 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -48,7 +48,7 @@ module.exports = [
     path: 'packages/browser/build/npm/esm/index.js',
     import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'),
     gzip: true,
-    limit: '76 KB',
+    limit: '77 KB',
   },
   {
     name: '@sentry/browser (incl. Tracing, Replay, Feedback)',
diff --git a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts
index 7bacf5a8ae17..2e4b0bdf61bd 100644
--- a/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts
+++ b/dev-packages/browser-integration-tests/suites/replay/multiple-pages/test.ts
@@ -13,6 +13,7 @@ import {
   expectedNavigationPerformanceSpan,
   expectedNavigationPushPerformanceSpan,
   expectedReloadPerformanceSpan,
+  expectedTTFBPerformanceSpan,
   getExpectedReplayEvent,
 } from '../../../utils/replayEventTemplates';
 import {
@@ -80,13 +81,14 @@ sentryTest(
     const collectedPerformanceSpans = [...recording0.performanceSpans, ...recording1.performanceSpans];
     const collectedBreadcrumbs = [...recording0.breadcrumbs, ...recording1.breadcrumbs];
 
-    expect(collectedPerformanceSpans.length).toEqual(8);
+    expect(collectedPerformanceSpans.length).toEqual(9);
     expect(collectedPerformanceSpans).toEqual(
       expect.arrayContaining([
         expectedNavigationPerformanceSpan,
         expectedLCPPerformanceSpan,
         expectedCLSPerformanceSpan,
         expectedFIDPerformanceSpan,
+        expectedTTFBPerformanceSpan,
         expectedFPPerformanceSpan,
         expectedFCPPerformanceSpan,
         expectedMemoryPerformanceSpan, // two memory spans - once per flush
@@ -120,12 +122,13 @@ sentryTest(
     const collectedPerformanceSpansAfterReload = [...recording2.performanceSpans, ...recording3.performanceSpans];
     const collectedBreadcrumbsAdterReload = [...recording2.breadcrumbs, ...recording3.breadcrumbs];
 
-    expect(collectedPerformanceSpansAfterReload.length).toEqual(8);
+    expect(collectedPerformanceSpansAfterReload.length).toEqual(9);
     expect(collectedPerformanceSpansAfterReload).toEqual(
       expect.arrayContaining([
         expectedReloadPerformanceSpan,
         expectedLCPPerformanceSpan,
         expectedCLSPerformanceSpan,
+        expectedTTFBPerformanceSpan,
         expectedFIDPerformanceSpan,
         expectedFPPerformanceSpan,
         expectedFCPPerformanceSpan,
@@ -195,6 +198,7 @@ sentryTest(
         expectedNavigationPerformanceSpan,
         expectedLCPPerformanceSpan,
         expectedCLSPerformanceSpan,
+        expectedTTFBPerformanceSpan,
         expectedFIDPerformanceSpan,
         expectedFPPerformanceSpan,
         expectedFCPPerformanceSpan,
@@ -312,12 +316,13 @@ sentryTest(
     ];
     const collectedBreadcrumbsAfterIndexNavigation = [...recording8.breadcrumbs, ...recording9.breadcrumbs];
 
-    expect(collectedPerformanceSpansAfterIndexNavigation.length).toEqual(8);
+    expect(collectedPerformanceSpansAfterIndexNavigation.length).toEqual(9);
     expect(collectedPerformanceSpansAfterIndexNavigation).toEqual(
       expect.arrayContaining([
         expectedNavigationPerformanceSpan,
         expectedLCPPerformanceSpan,
         expectedCLSPerformanceSpan,
+        expectedTTFBPerformanceSpan,
         expectedFIDPerformanceSpan,
         expectedFPPerformanceSpan,
         expectedFCPPerformanceSpan,
diff --git a/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts b/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts
index 257c47fbfa9b..c15eb57be429 100644
--- a/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts
+++ b/dev-packages/browser-integration-tests/utils/replayEventTemplates.ts
@@ -127,7 +127,7 @@ export const expectedLCPPerformanceSpan = {
   endTimestamp: expect.any(Number),
   data: {
     value: expect.any(Number),
-    nodeId: expect.any(Number),
+    nodeId: expect.any(Array),
     rating: expect.any(String),
     size: expect.any(Number),
   },
@@ -142,6 +142,7 @@ export const expectedCLSPerformanceSpan = {
     value: expect.any(Number),
     rating: expect.any(String),
     size: expect.any(Number),
+    nodeId: expect.any(Array),
   },
 };
 
@@ -154,7 +155,7 @@ export const expectedFIDPerformanceSpan = {
     value: expect.any(Number),
     rating: expect.any(String),
     size: expect.any(Number),
-    nodeId: expect.any(Number),
+    nodeId: expect.any(Array),
   },
 };
 
@@ -167,7 +168,20 @@ export const expectedINPPerformanceSpan = {
     value: expect.any(Number),
     rating: expect.any(String),
     size: expect.any(Number),
-    nodeId: expect.any(Number),
+    nodeId: expect.any(Array),
+  },
+};
+
+export const expectedTTFBPerformanceSpan = {
+  op: 'web-vital',
+  description: 'time-to-first-byte',
+  startTimestamp: expect.any(Number),
+  endTimestamp: expect.any(Number),
+  data: {
+    value: expect.any(Number),
+    rating: expect.any(String),
+    size: expect.any(Number),
+    nodeId: expect.any(Array),
   },
 };
 
diff --git a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/fixtures/ReplayRecordingData.ts b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/fixtures/ReplayRecordingData.ts
index 156c2775f5ff..9f3a4427310c 100644
--- a/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/fixtures/ReplayRecordingData.ts
+++ b/dev-packages/e2e-tests/test-applications/react-send-to-sentry/tests/fixtures/ReplayRecordingData.ts
@@ -206,6 +206,25 @@ export const ReplayRecordingData = [
       },
     },
   },
+  {
+    type: 5,
+    timestamp: expect.any(Number),
+    data: {
+      tag: 'performanceSpan',
+      payload: {
+        op: 'web-vital',
+        description: 'time-to-first-byte',
+        startTimestamp: expect.any(Number),
+        endTimestamp: expect.any(Number),
+        data: {
+          value: expect.any(Number),
+          size: expect.any(Number),
+          rating: expect.any(String),
+          nodeId: [],
+        },
+      },
+    },
+  },
   {
     type: 5,
     timestamp: expect.any(Number),
@@ -220,7 +239,7 @@ export const ReplayRecordingData = [
           value: expect.any(Number),
           size: expect.any(Number),
           rating: expect.any(String),
-          nodeId: 16,
+          nodeId: [16],
         },
       },
     },
@@ -239,6 +258,7 @@ export const ReplayRecordingData = [
           value: expect.any(Number),
           size: expect.any(Number),
           rating: expect.any(String),
+          nodeId: [],
         },
       },
     },
@@ -257,7 +277,7 @@ export const ReplayRecordingData = [
           value: expect.any(Number),
           size: expect.any(Number),
           rating: expect.any(String),
-          nodeId: 10,
+          nodeId: [10],
         },
       },
     },
diff --git a/packages/replay-internal/src/coreHandlers/performanceObserver.ts b/packages/replay-internal/src/coreHandlers/performanceObserver.ts
index 638ef53b05fb..ea6ac6eed869 100644
--- a/packages/replay-internal/src/coreHandlers/performanceObserver.ts
+++ b/packages/replay-internal/src/coreHandlers/performanceObserver.ts
@@ -4,6 +4,7 @@ import {
   addInpInstrumentationHandler,
   addLcpInstrumentationHandler,
   addPerformanceInstrumentationHandler,
+  addTtfbInstrumentationHandler,
 } from '@sentry-internal/browser-utils';
 import type { ReplayContainer } from '../types';
 import {
@@ -11,6 +12,7 @@ import {
   getFirstInputDelay,
   getInteractionToNextPaint,
   getLargestContentfulPaint,
+  getTimeToFirstByte,
   webVitalHandler,
 } from '../util/createPerformanceEntries';
 
@@ -41,6 +43,7 @@ export function setupPerformanceObserver(replay: ReplayContainer): () => void {
     addClsInstrumentationHandler(webVitalHandler(getCumulativeLayoutShift, replay)),
     addFidInstrumentationHandler(webVitalHandler(getFirstInputDelay, replay)),
     addInpInstrumentationHandler(webVitalHandler(getInteractionToNextPaint, replay)),
+    addTtfbInstrumentationHandler(webVitalHandler(getTimeToFirstByte, replay)),
   );
 
   // A callback to cleanup all handlers
diff --git a/packages/replay-internal/src/types/performance.ts b/packages/replay-internal/src/types/performance.ts
index 5241c12d847a..a3a73cc0289e 100644
--- a/packages/replay-internal/src/types/performance.ts
+++ b/packages/replay-internal/src/types/performance.ts
@@ -110,7 +110,7 @@ export interface WebVitalData {
   /**
    * The recording id of the LCP node. -1 if not found
    */
-  nodeId?: number;
+  nodeId?: number | number[];
 }
 
 /**
diff --git a/packages/replay-internal/src/types/replayFrame.ts b/packages/replay-internal/src/types/replayFrame.ts
index 0fa43ff41eb2..07e6392d1fd0 100644
--- a/packages/replay-internal/src/types/replayFrame.ts
+++ b/packages/replay-internal/src/types/replayFrame.ts
@@ -173,7 +173,12 @@ interface ReplayHistoryFrame extends ReplayBaseSpanFrame {
 
 interface ReplayWebVitalFrame extends ReplayBaseSpanFrame {
   data: WebVitalData;
-  op: 'largest-contentful-paint' | 'cumulative-layout-shift' | 'first-input-delay' | 'interaction-to-next-paint';
+  op:
+    | 'largest-contentful-paint'
+    | 'cumulative-layout-shift'
+    | 'first-input-delay'
+    | 'interaction-to-next-paint'
+    | 'time-to-first-byte';
 }
 
 interface ReplayMemoryFrame extends ReplayBaseSpanFrame {
diff --git a/packages/replay-internal/src/util/createPerformanceEntries.ts b/packages/replay-internal/src/util/createPerformanceEntries.ts
index 28ccf60280e8..dfcc3ec08c6f 100644
--- a/packages/replay-internal/src/util/createPerformanceEntries.ts
+++ b/packages/replay-internal/src/util/createPerformanceEntries.ts
@@ -191,14 +191,18 @@ export function getLargestContentfulPaint(metric: Metric): ReplayPerformanceEntr
  * Add a CLS event to the replay based on a CLS metric.
  */
 export function getCumulativeLayoutShift(metric: Metric): ReplayPerformanceEntry<WebVitalData> {
-  // get first node that shifts
-  const firstEntry = metric.entries[0] as (PerformanceEntry & { sources?: LayoutShiftAttribution[] }) | undefined;
-  const node = firstEntry
-    ? firstEntry.sources && firstEntry.sources[0]
-      ? firstEntry.sources[0].node
-      : undefined
-    : undefined;
-  return getWebVital(metric, 'cumulative-layout-shift', node);
+  const lastEntry = metric.entries[metric.entries.length - 1] as
+    | (PerformanceEntry & { sources?: LayoutShiftAttribution[] })
+    | undefined;
+  const nodes: Node[] = [];
+  if (lastEntry && lastEntry.sources) {
+    for (const source of lastEntry.sources) {
+      if (source.node) {
+        nodes.push(source.node);
+      }
+    }
+  }
+  return getWebVital(metric, 'cumulative-layout-shift', nodes);
 }
 
 /**
@@ -219,19 +223,39 @@ export function getInteractionToNextPaint(metric: Metric): ReplayPerformanceEntr
   return getWebVital(metric, 'interaction-to-next-paint', node);
 }
 
+/**
+ * Add a TTFB event to the replay based on an INP metric.
+ */
+export function getTimeToFirstByte(metric: Metric): ReplayPerformanceEntry<WebVitalData> {
+  const lastEntry = metric.entries[metric.entries.length - 1] as (PerformanceEntry & { target?: Node }) | undefined;
+  const node = lastEntry ? lastEntry.target : undefined;
+  return getWebVital(metric, 'time-to-first-byte', node);
+}
+
 /**
  * Add an web vital event to the replay based on the web vital metric.
  */
 export function getWebVital(
   metric: Metric,
   name: string,
-  node: Node | undefined,
+  node: Node | Node[] | undefined,
 ): ReplayPerformanceEntry<WebVitalData> {
   const value = metric.value;
   const rating = metric.rating;
 
   const end = getAbsoluteTime(value);
 
+  const nodeIds: number[] = [];
+  if (Array.isArray(node)) {
+    for (const n of node) {
+      nodeIds.push(record.mirror.getId(n));
+    }
+  } else {
+    if (node) {
+      nodeIds.push(record.mirror.getId(node));
+    }
+  }
+
   const data: ReplayPerformanceEntry<WebVitalData> = {
     type: 'web-vital',
     name,
@@ -241,7 +265,7 @@ export function getWebVital(
       value,
       size: value,
       rating,
-      nodeId: node ? record.mirror.getId(node) : undefined,
+      nodeId: nodeIds ? nodeIds : undefined,
     },
   };
 
diff --git a/packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts b/packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts
index f13d72feecf4..f63bae23d1b2 100644
--- a/packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts
+++ b/packages/replay-internal/test/unit/util/createPerformanceEntry.test.ts
@@ -17,6 +17,7 @@ import {
   getFirstInputDelay,
   getInteractionToNextPaint,
   getLargestContentfulPaint,
+  getTimeToFirstByte,
 } from '../../../src/util/createPerformanceEntries';
 import { PerformanceEntryNavigation } from '../../fixtures/performanceEntry/navigation';
 
@@ -83,7 +84,7 @@ describe('Unit | util | createPerformanceEntries', () => {
         name: 'largest-contentful-paint',
         start: 1672531205.108299,
         end: 1672531205.108299,
-        data: { value: 5108.299, rating: 'good', size: 5108.299, nodeId: undefined },
+        data: { value: 5108.299, rating: 'good', size: 5108.299, nodeId: [] },
       });
     });
   });
@@ -103,7 +104,7 @@ describe('Unit | util | createPerformanceEntries', () => {
         name: 'cumulative-layout-shift',
         start: 1672531205.108299,
         end: 1672531205.108299,
-        data: { value: 5108.299, size: 5108.299, rating: 'good', nodeId: undefined },
+        data: { value: 5108.299, size: 5108.299, rating: 'good', nodeId: [] },
       });
     });
   });
@@ -123,7 +124,7 @@ describe('Unit | util | createPerformanceEntries', () => {
         name: 'first-input-delay',
         start: 1672531205.108299,
         end: 1672531205.108299,
-        data: { value: 5108.299, size: 5108.299, rating: 'good', nodeId: undefined },
+        data: { value: 5108.299, size: 5108.299, rating: 'good', nodeId: [] },
       });
     });
   });
@@ -143,7 +144,27 @@ describe('Unit | util | createPerformanceEntries', () => {
         name: 'interaction-to-next-paint',
         start: 1672531205.108299,
         end: 1672531205.108299,
-        data: { value: 5108.299, size: 5108.299, rating: 'good', nodeId: undefined },
+        data: { value: 5108.299, size: 5108.299, rating: 'good', nodeId: [] },
+      });
+    });
+  });
+
+  describe('getTimeToFirstByte', () => {
+    it('works with an TTFB metric', async () => {
+      const metric = {
+        value: 5108.299,
+        rating: 'good' as const,
+        entries: [],
+      };
+
+      const event = getTimeToFirstByte(metric);
+
+      expect(event).toEqual({
+        type: 'web-vital',
+        name: 'time-to-first-byte',
+        start: 1672531205.108299,
+        end: 1672531205.108299,
+        data: { value: 5108.299, size: 5108.299, rating: 'good', nodeId: [] },
       });
     });
   });