-
Notifications
You must be signed in to change notification settings - Fork 434
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
replace nextAnimationFrame by nextMicroTask #1042
Conversation
This fixes #920. Hopefully this will be taken into account in #1019, as mentioned by @seanpdoyle in this comment. |
This is subtly changing behavior because To illustrate this point see this code: <!DOCTYPE html>
<html lang="en">
<head>
<link rel="icon" href="data:,">
<style>
#box {
width: 100px;
height: 100px;
background-color: blue;
transition: all 1s;
}
</style>
<script type="text/javascript">
function nextAnimationFrame() {
return new Promise((resolve) => requestAnimationFrame(() => resolve()))
}
function nextEventLoopTick() {
return new Promise((resolve) => setTimeout(() => resolve(), 0))
}
function nextMicrotask() {
return Promise.resolve()
}
document.addEventListener("DOMContentLoaded", async () => {
const box = document.getElementById("box")
box.style.transform = "translateX(500px)"
await nextAnimationFrame()
box.style.transform = "translateX(0px)"
})
</script>
</head>
<body>
<div id="box"></div>
</body>
</html> This animates a box moving from right to left. animation.movHowever replacing The current uses of However, I'd be interested to see an easy way to reproduce |
Hi @afcapel , Thanks for your answer. I've tried your example and put it in codesandbox. I've implemented this in this PR by replacing the nextMicroTask with nextEventLoopTick. Unit tests are passing. But note that I had to keep the nextMicroTask on the stream_element.js; not sure why it failed when used with the nextEventLoopTick. |
@michelson sorry, I'm a wary of changing this behaviour because I find the possible repercussions very difficult to foresee. Turbo already works well simplifying most simple use cases, but this one is particular enough that you may have to reach to lower level APIs. I think you're best chance is to reach to ActionCable to deliver the new message notifications. You can reuse the same WebSocket connection that Turbo opens with the server, and do anything you want when a new message arrives, even if your tab is in the background. This is how we do it in in one of our apps: // consumer.js
import { cable } from "@hotwired/turbo-rails"
export default await cable.getConsumer() // this reuses the same websocket that Turbo uses for stream updates
// typing_notifications_channel.js
import consumer from "channels/consumer"
// Subscribe to a custom channel
export function subscribeToTypingNotificationsChannel(roomId, callbacks) {
const channel = { channel: "TypingNotificationsChannel", room_id: roomId }
return consumer.subscriptions.create(channel, callbacks)
}
// typing_notifications_controller.js
import { Controller } from "@hotwired/stimulus"
import { subscribeToTypingNotificationsChannel } from "channels/typing_notifications_channel"
import { pageIsTurboPreview } from "helpers/turbo_helpers"
// Reacts to new messages on the `TypingNotificationsChannel`
export default class extends Controller {
connect() {
if (!pageIsTurboPreview()) {
this.channel = subscribeToTypingNotificationsChannel(this.roomIdValue, { received: this.#received.bind(this) })
}
}
...
#received(data) {
... // handle received data
}
} Hope that helps and you can migrate your app away from React. |
This are certain use cases might be affected or limited by this approach:
Chat applications have unique challenges and requirements that might be influenced by the use of
Considering these points, while what if this could be an opt in feature? would you consider a new PR that implements that? This is an idea to provide an optional configuration setting for Hotwire's Turbo to control whether it uses Here's a high-level overview of how we could implement this:
Turbo.config = {
...,
useNextAnimationFrame: true // default is true to maintain current behavior
};
if (Turbo.config.useNextAnimationFrame) {
requestAnimationFrame(() => {
// current rendering logic
});
} else {
// immediate rendering logic
} |
Doing more tests, I believe that the only change needed is in the render function of |
Not interested in configuration options, but I'm open to a more surgical change. We can try limiting the change to the // util.js
export function nextRepaint() {
if (document.visibilityState === "hidden") {
return nextEventLoopTick()
} else {
return nextAnimationFrame()
}
} |
Great solution, @afcapel . On it. |
src/elements/stream_element.js
Outdated
@@ -43,7 +43,7 @@ export class StreamElement extends HTMLElement { | |||
const event = this.beforeRenderEvent | |||
|
|||
if (this.dispatchEvent(event)) { | |||
await nextEventLoopTick() | |||
nextRepaint() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to await
this? Should nextRepaint()
return the next
-prefixed Promise utilities instead of await
-ing them?
Hi @seanpdoyle, thanks for the suggestions. I've applied those already. Also, CI passing at https://github.com/michelson/turbo/actions/runs/6721616946/job/18267725557 |
Thanks @michelson & @seanpdoyle! |
DOM operations on turbostream are queued when the user is not in the browser tab or the tab is not active. This fixes that.
the problem resides when we call
await nextAnimationFrame()
because browsers pause the execution to save resources on the inactive browsers tab. The problem is that for chat applications or other kinds of notifications having a way to update things in the foreground is key.more context at:
ref hotwired/turbo-rails#338