PERF: Refactor slide-in menu sizing for improved performance (#20377)
Using Javascript to read and recalculate sizing is prone to causing 'forced reflows', which are very expensive, especially on slower devices. This PR refactors the slide-in menus so that all of the height calculation is done using CSS. This is made possible by the new dvh (dynamic view height) units and env(safe-area-inset-bottom), both of which are supported on all of our target browsers. In tests on a moto g50, on a sidebar with 16 categories, 15 tags, and 2 chat channels, this improves the sidebar opening time by around 50ms (6%).
This commit is contained in:
parent
f7c57fbc19
commit
c82094cd9d
|
@ -54,6 +54,12 @@ const SiteHeaderComponent = MountWidget.extend(
|
|||
},
|
||||
|
||||
_animateOpening(panel) {
|
||||
window.requestAnimationFrame(
|
||||
this._setAnimateOpeningProperties.bind(this, panel)
|
||||
);
|
||||
},
|
||||
|
||||
_setAnimateOpeningProperties(panel) {
|
||||
const headerCloak = document.querySelector(".header-cloak");
|
||||
panel.classList.add("animate");
|
||||
headerCloak.classList.add("animate");
|
||||
|
@ -67,13 +73,16 @@ const SiteHeaderComponent = MountWidget.extend(
|
|||
},
|
||||
|
||||
_animateClosing(panel, menuOrigin) {
|
||||
const windowWidth = document.body.offsetWidth;
|
||||
this._animate = true;
|
||||
const headerCloak = document.querySelector(".header-cloak");
|
||||
panel.classList.add("animate");
|
||||
headerCloak.classList.add("animate");
|
||||
const offsetDirection = menuOrigin === "left" ? -1 : 1;
|
||||
panel.style.setProperty("--offset", `${offsetDirection * windowWidth}px`);
|
||||
if (menuOrigin === "left") {
|
||||
panel.style.setProperty("--offset", `-100vw`);
|
||||
} else {
|
||||
panel.style.setProperty("--offset", `100vw`);
|
||||
}
|
||||
|
||||
headerCloak.style.setProperty("--opacity", 0);
|
||||
this._scheduledRemoveAnimate = discourseLater(() => {
|
||||
panel.classList.remove("animate");
|
||||
|
@ -365,7 +374,6 @@ const SiteHeaderComponent = MountWidget.extend(
|
|||
return;
|
||||
}
|
||||
|
||||
const windowWidth = document.body.offsetWidth;
|
||||
const viewMode =
|
||||
this.site.mobileView || this.site.narrowDesktopView
|
||||
? "slide-in"
|
||||
|
@ -374,9 +382,6 @@ const SiteHeaderComponent = MountWidget.extend(
|
|||
menuPanels.forEach((panel) => {
|
||||
const headerCloak = document.querySelector(".header-cloak");
|
||||
let width = parseInt(panel.getAttribute("data-max-width"), 10) || 300;
|
||||
if (windowWidth - width < 50) {
|
||||
width = windowWidth - 50;
|
||||
}
|
||||
if (this._panMenuOffset) {
|
||||
this._panMenuOffset = -width;
|
||||
}
|
||||
|
@ -384,77 +389,23 @@ const SiteHeaderComponent = MountWidget.extend(
|
|||
panel.classList.remove("drop-down");
|
||||
panel.classList.remove("slide-in");
|
||||
panel.classList.add(viewMode);
|
||||
|
||||
if (this._animate || this._panMenuOffset !== 0) {
|
||||
if (
|
||||
(this.site.mobileView || this.site.narrowDesktopView) &&
|
||||
panel.parentElement.classList.contains(this._leftMenuClass())
|
||||
) {
|
||||
this._panMenuOrigin = "left";
|
||||
panel.style.setProperty("--offset", `${-windowWidth}px`);
|
||||
panel.style.setProperty("--offset", `-100vw`);
|
||||
} else {
|
||||
this._panMenuOrigin = "right";
|
||||
panel.style.setProperty("--offset", `${windowWidth}px`);
|
||||
panel.style.setProperty("--offset", `100vw`);
|
||||
}
|
||||
headerCloak.style.setProperty("--opacity", 0);
|
||||
}
|
||||
|
||||
const panelBody = panel.querySelector(".panel-body");
|
||||
|
||||
// We use a mutationObserver to check for style changes, so it's important
|
||||
// we don't set it if it doesn't change. Same goes for the panelBody!
|
||||
|
||||
if (!this.site.mobileView && !this.site.narrowDesktopView) {
|
||||
const buttonPanel = document.querySelectorAll("header ul.icons");
|
||||
if (buttonPanel.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// These values need to be set here, not in the css file - this is to deal with the
|
||||
// possibility of the window being resized and the menu changing from .slide-in to .drop-down.
|
||||
if (panel.style.top !== "100%" || panel.style.height !== "auto") {
|
||||
panel.style.setProperty("top", "100%");
|
||||
panel.style.setProperty("height", "auto");
|
||||
}
|
||||
} else {
|
||||
if (viewMode === "slide-in") {
|
||||
headerCloak.style.display = "block";
|
||||
|
||||
const menuTop = headerTop();
|
||||
|
||||
const winHeightOffset = this.currentUser?.redesigned_user_menu_enabled
|
||||
? 0
|
||||
: 16;
|
||||
let initialWinHeight = window.innerHeight;
|
||||
const winHeight = initialWinHeight - winHeightOffset;
|
||||
|
||||
let height = winHeight - menuTop;
|
||||
|
||||
const isIPadApp = document.body.classList.contains("footer-nav-ipad"),
|
||||
heightProp = isIPadApp ? "max-height" : "height",
|
||||
iPadOffset = 10;
|
||||
|
||||
if (isIPadApp) {
|
||||
height = winHeight - menuTop - iPadOffset;
|
||||
}
|
||||
|
||||
if (panelBody.style.height !== "100%") {
|
||||
panelBody.style.setProperty("height", "100%");
|
||||
}
|
||||
if (
|
||||
panel.style.top !== `${menuTop}px` ||
|
||||
panel.style[heightProp] !== `${height}px`
|
||||
) {
|
||||
panel.style.top = `${menuTop}px`;
|
||||
panel.style.setProperty(heightProp, `${height}px`);
|
||||
if (headerCloak) {
|
||||
headerCloak.style.top = `${menuTop}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove the if condition when redesigned_user_menu_enabled is
|
||||
// removed
|
||||
if (!panel.classList.contains("revamped")) {
|
||||
panel.style.setProperty("width", `${width}px`);
|
||||
}
|
||||
if (this._animate) {
|
||||
this._animateOpening(panel);
|
||||
|
@ -488,12 +439,20 @@ export default SiteHeaderComponent.extend({
|
|||
|
||||
this.appEvents.on("site-header:force-refresh", this, "queueRerender");
|
||||
|
||||
const header = document.querySelector(".d-header-wrap");
|
||||
if (header) {
|
||||
const headerWrap = document.querySelector(".d-header-wrap");
|
||||
let header;
|
||||
if (headerWrap) {
|
||||
schedule("afterRender", () => {
|
||||
header = headerWrap.querySelector("header.d-header");
|
||||
const headerOffset = headerWrap.offsetHeight;
|
||||
const headerTop = header.offsetTop;
|
||||
document.documentElement.style.setProperty(
|
||||
"--header-offset",
|
||||
`${header.offsetHeight}px`
|
||||
`${headerOffset}px`
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--header-top",
|
||||
`${headerTop}px`
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -502,15 +461,21 @@ export default SiteHeaderComponent.extend({
|
|||
this._resizeObserver = new ResizeObserver((entries) => {
|
||||
for (let entry of entries) {
|
||||
if (entry.contentRect) {
|
||||
const headerOffset = entry.contentRect.height;
|
||||
const headerTop = header.offsetTop;
|
||||
document.documentElement.style.setProperty(
|
||||
"--header-offset",
|
||||
entry.contentRect.height + "px"
|
||||
`${headerOffset}px`
|
||||
);
|
||||
document.documentElement.style.setProperty(
|
||||
"--header-top",
|
||||
`${headerTop}px`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this._resizeObserver.observe(header);
|
||||
this._resizeObserver.observe(headerWrap);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -521,8 +486,3 @@ export default SiteHeaderComponent.extend({
|
|||
this.appEvents.off("site-header:force-refresh", this, "queueRerender");
|
||||
},
|
||||
});
|
||||
|
||||
export function headerTop() {
|
||||
const header = document.querySelector("header.d-header");
|
||||
return header.offsetTop ? header.offsetTop : 0;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,10 @@ import {
|
|||
import { click, triggerEvent, visit } from "@ember/test-helpers";
|
||||
|
||||
async function triggerSwipeStart(touchTarget) {
|
||||
const emberTesting = document.querySelector("#ember-testing-container");
|
||||
emberTesting.scrollTop = 0;
|
||||
emberTesting.scrollLeft = 0;
|
||||
|
||||
// Other tests are shown in a transformed viewport, and this is a multiple for the offsets
|
||||
let scale = parseFloat(
|
||||
window
|
||||
|
|
|
@ -29,6 +29,8 @@
|
|||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
|
||||
hr {
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
@ -689,7 +691,7 @@ body.footer-nav-ipad {
|
|||
background-color: black;
|
||||
--opacity: 0.5;
|
||||
opacity: var(--opacity);
|
||||
top: 0;
|
||||
top: var(--header-top);
|
||||
left: 0;
|
||||
display: none;
|
||||
touch-action: pan-y pinch-zoom;
|
||||
|
@ -702,6 +704,23 @@ body.footer-nav-ipad {
|
|||
}
|
||||
|
||||
.menu-panel.slide-in {
|
||||
top: var(--header-top);
|
||||
box-sizing: border-box;
|
||||
|
||||
/* Use dvh where supported, with fallback to vh */
|
||||
--100dvh: 100vh;
|
||||
--100dvh: 100dvh;
|
||||
|
||||
--base-height: calc(
|
||||
var(--100dvh) - var(--header-top) - env(safe-area-inset-bottom, 0px)
|
||||
);
|
||||
|
||||
height: var(--base-height);
|
||||
|
||||
body.footer-nav-ipad & {
|
||||
height: calc(var(--base-height) - var(--footer-nav-height));
|
||||
}
|
||||
|
||||
transform: translateX(var(--offset));
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
&.animate {
|
||||
|
|
Loading…
Reference in New Issue