PERF: Defer button actions to improve interaction-next-paint (INP) (#28019)

This is a variation on bc3e8a9963cf9a64d114ec751c875025af169690, which was reverted due to issues on iOS. Safari's "in response to user action" check cannot follow the `runAfterFramePaint` chain of interaction -> requestAnimationFrame -> messageChannel, and so some sensitive browser APIs (e.g. clipboard, upload, etc.) were blocked.

This commit is similar, but uses `next()` instead of `runAfterFramePaint()`. The result seems the same, but doesn't have the same issue on iOS.

The chat-emoji-picker change was required to resolve a test failure. The emoji picker has never closed-on-scroll on desktop, so there is no user-facing change in behavior.
This commit is contained in:
David Taylor 2024-08-20 03:11:34 +01:00 committed by GitHub
parent 1446596089
commit dfc947a97d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 24 additions and 18 deletions

View File

@ -1,6 +1,7 @@
import { on } from "@ember/modifier";
import { action } from "@ember/object";
import { empty, equal, notEmpty } from "@ember/object/computed";
import { next } from "@ember/runloop";
import { service } from "@ember/service";
import { htmlSafe } from "@ember/template";
import { or } from "truth-helpers";
@ -118,17 +119,19 @@ export default class DButton extends GlimmerComponentWithDeprecatedParentView {
);
}
} else if (typeof actionVal === "object" && actionVal.value) {
if (forwardEvent) {
actionVal.value(actionParam, event);
} else {
actionVal.value(actionParam);
}
// Using `next()` to optimise INP
next(() =>
forwardEvent
? actionVal.value(actionParam, event)
: actionVal.value(actionParam)
);
} else if (typeof actionVal === "function") {
if (forwardEvent) {
actionVal(actionParam, event);
} else {
actionVal(actionParam);
}
// Using `next()` to optimise INP
next(() =>
forwardEvent
? actionVal(actionParam, event)
: actionVal(actionParam)
);
}
} else if (route) {
this.router.transitionTo(route);

View File

@ -1,6 +1,6 @@
import { alias, match } from "@ember/object/computed";
import Mixin from "@ember/object/mixin";
import { schedule, throttle } from "@ember/runloop";
import { next, schedule, throttle } from "@ember/runloop";
import { service } from "@ember/service";
import { wantsNewWindow } from "discourse/lib/intercept-click";
import { headerOffset } from "discourse/lib/offset-calculator";
@ -86,9 +86,12 @@ export default Mixin.create({
document.querySelector(".card-cloak")?.classList.remove("hidden");
this.appEvents.trigger("user-card:show", { username });
this._positionCard(target, event);
this._showCallback(username).then((user) => {
this.appEvents.trigger("user-card:after-show", { user });
// Using `next()` to optimise INP
next(() => {
this._positionCard(target, event);
this._showCallback(username).then((user) => {
this.appEvents.trigger("user-card:after-show", { user });
});
});
// We bind scrolling on mobile after cards are shown to hide them if user scrolls

View File

@ -13,6 +13,10 @@ export default class ChatChannelMessageEmojiPicker extends Component {
context = "chat-channel-message";
listenToBodyScroll = modifier(() => {
if (!this.site.mobileView) {
return;
}
const handler = () => {
this.chatEmojiPickerManager.close();
};
@ -43,10 +47,6 @@ export default class ChatChannelMessageEmojiPicker extends Component {
{
placement: "top",
modifiers: [
{
name: "eventListeners",
options: { scroll: false, resize: false },
},
{
name: "flip",
options: { padding: { top: headerOffset() } },