diff --git a/src/core/frames/frame_controller.js b/src/core/frames/frame_controller.js
index 1cab2902e..424bef7df 100644
--- a/src/core/frames/frame_controller.js
+++ b/src/core/frames/frame_controller.js
@@ -258,7 +258,7 @@ export class FrameController {
 
   // View delegate
 
-  allowsImmediateRender({ element: newFrame }, _isPreview, options) {
+  allowsImmediateRender({ element: newFrame }, _renderer, options) {
     const event = dispatch("turbo:before-frame-render", {
       target: this.element,
       detail: { newFrame, ...options },
@@ -276,7 +276,13 @@ export class FrameController {
     return !defaultPrevented
   }
 
-  viewRenderedSnapshot(_snapshot, _isPreview) {}
+  viewRenderedSnapshot({ currentSnapshot: { element: frame } }) {
+    return dispatch("turbo:frame-render", {
+      detail: {},
+      target: frame,
+      cancelable: true
+    })
+  }
 
   preloadOnLoadLinksForView(element) {
     session.preloadOnLoadLinksForView(element)
@@ -311,9 +317,11 @@ export class FrameController {
       if (this.view.renderPromise) await this.view.renderPromise
       this.changeHistory()
 
-      await this.view.render(renderer)
+      await mergeIntoNext("turbo:frame-render", { on: this.element, detail: { fetchResponse } }, async () => {
+        await this.view.render(renderer)
+      })
+
       this.complete = true
-      session.frameRendered(fetchResponse, this.element)
       session.frameLoaded(this.element)
       await this.fetchResponseLoaded(fetchResponse)
     } else if (this.#willHandleFrameMissingFromResponse(fetchResponse)) {
@@ -590,3 +598,15 @@ function activateElement(element, currentURL) {
     }
   }
 }
+
+async function mergeIntoNext(eventName, { on: target, detail }, callback) {
+  const listener = (event) => Object.assign(event.detail, detail)
+  const listenerOptions = { once: true, capture: true }
+  target.addEventListener(eventName, listener, listenerOptions)
+
+  try {
+    await callback()
+  } finally {
+    target.removeEventListener(eventName, listener, listenerOptions)
+  }
+}
diff --git a/src/core/session.js b/src/core/session.js
index ea8ad0f7d..dc2b5c8db 100644
--- a/src/core/session.js
+++ b/src/core/session.js
@@ -251,8 +251,8 @@ export class Session {
     }
   }
 
-  allowsImmediateRender({ element }, isPreview, options) {
-    const event = this.notifyApplicationBeforeRender(element, isPreview, options)
+  allowsImmediateRender({ element }, renderer, options) {
+    const event = this.notifyApplicationBeforeRender(element, renderer, options)
     const {
       defaultPrevented,
       detail: { render }
@@ -265,9 +265,9 @@ export class Session {
     return !defaultPrevented
   }
 
-  viewRenderedSnapshot(_snapshot, isPreview) {
+  viewRenderedSnapshot(renderer) {
     this.view.lastRenderedLocation = this.history.location
-    this.notifyApplicationAfterRender(isPreview)
+    this.notifyApplicationAfterRender(renderer)
   }
 
   preloadOnLoadLinksForView(element) {
@@ -284,10 +284,6 @@ export class Session {
     this.notifyApplicationAfterFrameLoad(frame)
   }
 
-  frameRendered(fetchResponse, frame) {
-    this.notifyApplicationAfterFrameRender(fetchResponse, frame)
-  }
-
   // Application events
 
   applicationAllowsFollowingLinkToLocation(link, location, ev) {
@@ -323,14 +319,14 @@ export class Session {
     return dispatch("turbo:before-cache")
   }
 
-  notifyApplicationBeforeRender(newBody, isPreview, options) {
+  notifyApplicationBeforeRender(newBody, { isPreview }, options) {
     return dispatch("turbo:before-render", {
       detail: { newBody, isPreview, ...options },
       cancelable: true
     })
   }
 
-  notifyApplicationAfterRender(isPreview) {
+  notifyApplicationAfterRender({ isPreview }) {
     return dispatch("turbo:render", { detail: { isPreview } })
   }
 
@@ -353,14 +349,6 @@ export class Session {
     return dispatch("turbo:frame-load", { target: frame })
   }
 
-  notifyApplicationAfterFrameRender(fetchResponse, frame) {
-    return dispatch("turbo:frame-render", {
-      detail: { fetchResponse },
-      target: frame,
-      cancelable: true
-    })
-  }
-
   // Helpers
 
   submissionIsNavigatable(form, submitter) {
diff --git a/src/core/view.js b/src/core/view.js
index ca81e8bdb..13d6b71c4 100644
--- a/src/core/view.js
+++ b/src/core/view.js
@@ -56,7 +56,7 @@ export class View {
   // Rendering
 
   async render(renderer) {
-    const { isPreview, shouldRender, newSnapshot: snapshot } = renderer
+    const { shouldRender, newSnapshot: snapshot } = renderer
     if (shouldRender) {
       try {
         this.renderPromise = new Promise((resolve) => (this.#resolveRenderPromise = resolve))
@@ -65,11 +65,11 @@ export class View {
 
         const renderInterception = new Promise((resolve) => (this.#resolveInterceptionPromise = resolve))
         const options = { resume: this.#resolveInterceptionPromise, render: this.renderer.renderElement }
-        const immediateRender = this.delegate.allowsImmediateRender(snapshot, isPreview, options)
+        const immediateRender = this.delegate.allowsImmediateRender(snapshot, renderer, options)
         if (!immediateRender) await renderInterception
 
         await this.renderSnapshot(renderer)
-        this.delegate.viewRenderedSnapshot(snapshot, isPreview)
+        this.delegate.viewRenderedSnapshot(renderer)
         this.delegate.preloadOnLoadLinksForView(this.element)
         this.finishRenderingSnapshot(renderer)
       } finally {