diff --git a/app/assets/javascripts/blame/streaming/index.js b/app/assets/javascripts/blame/streaming/index.js
index 935343cca2e3459c52bcc250c4f96801a957ecd3..a88ef1c3e2194f1ee32feaf917b7d571fb5493fb 100644
--- a/app/assets/javascripts/blame/streaming/index.js
+++ b/app/assets/javascripts/blame/streaming/index.js
@@ -1,5 +1,6 @@
 import { renderHtmlStreams } from '~/streaming/render_html_streams';
 import { handleStreamedAnchorLink } from '~/streaming/handle_streamed_anchor_link';
+import { handleStreamedRelativeTimestamps } from '~/streaming/handle_streamed_relative_timestamps';
 import { createAlert } from '~/alert';
 import { __ } from '~/locale';
 import { rateLimitStreamRequests } from '~/streaming/rate_limit_stream_requests';
@@ -11,6 +12,7 @@ export async function renderBlamePageStreams(firstStreamPromise) {
   if (!element || !firstStreamPromise) return;
 
   const stopAnchorObserver = handleStreamedAnchorLink(element);
+  const relativeTimestampsHandler = handleStreamedRelativeTimestamps(element);
   const { dataset } = document.querySelector('#blob-content-holder');
   const totalExtraPages = parseInt(dataset.totalExtraPages, 10);
   const { pagesUrl } = dataset;
@@ -50,6 +52,8 @@ export async function renderBlamePageStreams(firstStreamPromise) {
     });
     throw error;
   } finally {
+    const stopTimestampObserver = await relativeTimestampsHandler;
+    stopTimestampObserver();
     stopAnchorObserver();
     document.querySelector('#blame-stream-loading').remove();
   }
diff --git a/app/assets/javascripts/streaming/handle_streamed_relative_timestamps.js b/app/assets/javascripts/streaming/handle_streamed_relative_timestamps.js
new file mode 100644
index 0000000000000000000000000000000000000000..fa5fe02878c9fa61d1d3ce7a5dea94479efba23e
--- /dev/null
+++ b/app/assets/javascripts/streaming/handle_streamed_relative_timestamps.js
@@ -0,0 +1,80 @@
+import { localTimeAgo } from '~/lib/utils/datetime_utility';
+
+const STREAMING_ELEMENT_NAME = 'streaming-element';
+const TIME_AGO_CLASS_NAME = 'js-timeago';
+
+// Callback handler for intersections observed on timestamps.
+const handleTimestampsIntersecting = (entries, observer) => {
+  entries.forEach((entry) => {
+    const { isIntersecting, target: timestamp } = entry;
+    if (isIntersecting) {
+      localTimeAgo([timestamp]);
+      observer.unobserve(timestamp);
+    }
+  });
+};
+
+// Finds nodes containing the `js-timeago` class within a mutation list.
+const findTimeAgoNodes = (mutationList) => {
+  return mutationList.reduce((acc, mutation) => {
+    [...mutation.addedNodes].forEach((node) => {
+      if (node.classList?.contains(TIME_AGO_CLASS_NAME)) {
+        acc.push(node);
+      }
+    });
+
+    return acc;
+  }, []);
+};
+
+// Callback handler for mutations observed on the streaming element.
+const handleStreamingElementMutation = (mutationList) => {
+  const timestamps = findTimeAgoNodes(mutationList);
+  const timestampIntersectionObserver = new IntersectionObserver(handleTimestampsIntersecting, {
+    rootMargin: `${window.innerHeight}px 0px`,
+  });
+
+  timestamps.forEach((timestamp) => timestampIntersectionObserver.observe(timestamp));
+};
+
+// Finds the streaming element within a mutation list.
+const findStreamingElement = (mutationList) =>
+  mutationList.find((mutation) =>
+    [...mutation.addedNodes].find((node) => node.localName === STREAMING_ELEMENT_NAME),
+  )?.target;
+
+// Waits for the streaming element to become available on the rootElement.
+const waitForStreamingElement = (rootElement) => {
+  return new Promise((resolve) => {
+    let element = document.querySelector(STREAMING_ELEMENT_NAME);
+
+    if (element) {
+      resolve(element);
+      return;
+    }
+
+    const rootElementObserver = new MutationObserver((mutations) => {
+      element = findStreamingElement(mutations);
+      if (element) {
+        resolve(element);
+        rootElementObserver.disconnect();
+      }
+    });
+
+    rootElementObserver.observe(rootElement, { childList: true, subtree: true });
+  });
+};
+
+/**
+ * Ensures relative (timeago) timestamps that are streamed are formatted correctly.
+ *
+ * Example: `May 12, 2020` → `3 years ago`
+ */
+export const handleStreamedRelativeTimestamps = async (rootElement) => {
+  const streamingElement = await waitForStreamingElement(rootElement); // wait for streaming to start
+  const streamingElementObserver = new MutationObserver(handleStreamingElementMutation);
+
+  streamingElementObserver.observe(streamingElement, { childList: true, subtree: true });
+
+  return () => streamingElementObserver.disconnect();
+};
diff --git a/spec/frontend/__helpers__/mock_dom_observer.js b/spec/frontend/__helpers__/mock_dom_observer.js
index 8c9c435041e097c8a041a4741b96a04719775476..fd3945adfd89475989bcda847e19789f5d4bcf34 100644
--- a/spec/frontend/__helpers__/mock_dom_observer.js
+++ b/spec/frontend/__helpers__/mock_dom_observer.js
@@ -22,9 +22,9 @@ class MockObserver {
 
   takeRecords() {}
 
-  $_triggerObserve(node, { entry = {}, options = {} } = {}) {
+  $_triggerObserve(node, { entry = {}, observer = {}, options = {} } = {}) {
     if (this.$_hasObserver(node, options)) {
-      this.$_cb([{ target: node, ...entry }]);
+      this.$_cb([{ target: node, ...entry }], observer);
     }
   }
 
diff --git a/spec/frontend/blame/streaming/index_spec.js b/spec/frontend/blame/streaming/index_spec.js
index e048ce3f70e62234732face38163ec2494025002..29beb6beffa693ec5b8c0f1d86c7373312dacc3e 100644
--- a/spec/frontend/blame/streaming/index_spec.js
+++ b/spec/frontend/blame/streaming/index_spec.js
@@ -4,12 +4,14 @@ import { setHTMLFixture } from 'helpers/fixtures';
 import { renderHtmlStreams } from '~/streaming/render_html_streams';
 import { rateLimitStreamRequests } from '~/streaming/rate_limit_stream_requests';
 import { handleStreamedAnchorLink } from '~/streaming/handle_streamed_anchor_link';
+import { handleStreamedRelativeTimestamps } from '~/streaming/handle_streamed_relative_timestamps';
 import { toPolyfillReadable } from '~/streaming/polyfills';
 import { createAlert } from '~/alert';
 
 jest.mock('~/streaming/render_html_streams');
 jest.mock('~/streaming/rate_limit_stream_requests');
 jest.mock('~/streaming/handle_streamed_anchor_link');
+jest.mock('~/streaming/handle_streamed_relative_timestamps');
 jest.mock('~/streaming/polyfills');
 jest.mock('~/sentry');
 jest.mock('~/alert');
@@ -18,6 +20,7 @@ global.fetch = jest.fn();
 
 describe('renderBlamePageStreams', () => {
   let stopAnchor;
+  let stopTimetamps;
   const PAGES_URL = 'https://example.com/';
   const findStreamContainer = () => document.querySelector('#blame-stream-container');
   const findStreamLoadingIndicator = () => document.querySelector('#blame-stream-loading');
@@ -34,6 +37,7 @@ describe('renderBlamePageStreams', () => {
   };
 
   handleStreamedAnchorLink.mockImplementation(() => stopAnchor);
+  handleStreamedRelativeTimestamps.mockImplementation(() => Promise.resolve(stopTimetamps));
   rateLimitStreamRequests.mockImplementation(({ factory, total }) => {
     return Array.from({ length: total }, (_, i) => {
       return Promise.resolve(factory(i));
@@ -43,6 +47,7 @@ describe('renderBlamePageStreams', () => {
 
   beforeEach(() => {
     stopAnchor = jest.fn();
+    stopTimetamps = jest.fn();
     fetch.mockClear();
   });
 
@@ -50,6 +55,7 @@ describe('renderBlamePageStreams', () => {
     await renderBlamePageStreams();
 
     expect(handleStreamedAnchorLink).not.toHaveBeenCalled();
+    expect(handleStreamedRelativeTimestamps).not.toHaveBeenCalled();
     expect(renderHtmlStreams).not.toHaveBeenCalled();
   });
 
@@ -64,7 +70,9 @@ describe('renderBlamePageStreams', () => {
     renderBlamePageStreams(stream);
 
     expect(handleStreamedAnchorLink).toHaveBeenCalledTimes(1);
+    expect(handleStreamedRelativeTimestamps).toHaveBeenCalledTimes(1);
     expect(stopAnchor).toHaveBeenCalledTimes(0);
+    expect(stopTimetamps).toHaveBeenCalledTimes(0);
     expect(renderHtmlStreams).toHaveBeenCalledWith([stream], findStreamContainer());
     expect(findStreamLoadingIndicator()).not.toBe(null);
 
@@ -72,6 +80,7 @@ describe('renderBlamePageStreams', () => {
     await waitForPromises();
 
     expect(stopAnchor).toHaveBeenCalledTimes(1);
+    expect(stopTimetamps).toHaveBeenCalledTimes(1);
     expect(findStreamLoadingIndicator()).toBe(null);
   });
 
diff --git a/spec/frontend/streaming/handle_streamed_relative_timestamps_spec.js b/spec/frontend/streaming/handle_streamed_relative_timestamps_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..12bd27488b1aee8aadf6335688ecc27c21fe33ec
--- /dev/null
+++ b/spec/frontend/streaming/handle_streamed_relative_timestamps_spec.js
@@ -0,0 +1,94 @@
+import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures';
+import waitForPromises from 'helpers/wait_for_promises';
+import { handleStreamedRelativeTimestamps } from '~/streaming/handle_streamed_relative_timestamps';
+import { localTimeAgo } from '~/lib/utils/datetime_utility';
+import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
+
+jest.mock('~/lib/utils/datetime_utility');
+
+const TIMESTAMP_MOCK = `<div class="js-timeago">Oct 2, 2019</div>`;
+
+describe('handleStreamedRelativeTimestamps', () => {
+  const findRoot = () => document.querySelector('#root');
+  const findStreamingElement = () => document.querySelector('streaming-element');
+  const findTimestamp = () => document.querySelector('.js-timeago');
+
+  afterEach(() => {
+    resetHTMLFixture();
+  });
+
+  describe('when element is present', () => {
+    beforeEach(() => {
+      setHTMLFixture(`<div id="root">${TIMESTAMP_MOCK}</div>`);
+      handleStreamedRelativeTimestamps(findRoot());
+    });
+
+    it('does nothing', async () => {
+      await waitForPromises();
+      expect(localTimeAgo).not.toHaveBeenCalled();
+    });
+  });
+
+  describe('when element is streamed', () => {
+    let relativeTimestampsHandler;
+    const { trigger: triggerIntersection } = useMockIntersectionObserver();
+
+    const insertStreamingElement = () =>
+      findRoot().insertAdjacentHTML('afterbegin', `<streaming-element></streaming-element>`);
+
+    beforeEach(() => {
+      setHTMLFixture('<div id="root"></div>');
+      relativeTimestampsHandler = handleStreamedRelativeTimestamps(findRoot());
+    });
+
+    it('formats and unobserved the timestamp when inserted and intersecting', async () => {
+      insertStreamingElement();
+      await waitForPromises();
+      findStreamingElement().insertAdjacentHTML('afterbegin', TIMESTAMP_MOCK);
+      await waitForPromises();
+
+      const timestamp = findTimestamp();
+      const unobserveMock = jest.fn();
+
+      triggerIntersection(findTimestamp(), {
+        entry: { isIntersecting: true },
+        observer: { unobserve: unobserveMock },
+      });
+
+      expect(unobserveMock).toHaveBeenCalled();
+      expect(localTimeAgo).toHaveBeenCalledWith([timestamp]);
+    });
+
+    it('does not format the timestamp when inserted but not intersecting', async () => {
+      insertStreamingElement();
+      await waitForPromises();
+      findStreamingElement().insertAdjacentHTML('afterbegin', TIMESTAMP_MOCK);
+      await waitForPromises();
+
+      const unobserveMock = jest.fn();
+
+      triggerIntersection(findTimestamp(), {
+        entry: { isIntersecting: false },
+        observer: { unobserve: unobserveMock },
+      });
+
+      expect(unobserveMock).not.toHaveBeenCalled();
+      expect(localTimeAgo).not.toHaveBeenCalled();
+    });
+
+    it('does not format the time when destroyed', async () => {
+      insertStreamingElement();
+
+      const stop = await relativeTimestampsHandler;
+      stop();
+
+      await waitForPromises();
+      findStreamingElement().insertAdjacentHTML('afterbegin', TIMESTAMP_MOCK);
+      await waitForPromises();
+
+      triggerIntersection(findTimestamp(), { entry: { isIntersecting: true } });
+
+      expect(localTimeAgo).not.toHaveBeenCalled();
+    });
+  });
+});