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) {
|
_animateOpening(panel) {
|
||||||
|
window.requestAnimationFrame(
|
||||||
|
this._setAnimateOpeningProperties.bind(this, panel)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
_setAnimateOpeningProperties(panel) {
|
||||||
const headerCloak = document.querySelector(".header-cloak");
|
const headerCloak = document.querySelector(".header-cloak");
|
||||||
panel.classList.add("animate");
|
panel.classList.add("animate");
|
||||||
headerCloak.classList.add("animate");
|
headerCloak.classList.add("animate");
|
||||||
|
@ -67,13 +73,16 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||||
},
|
},
|
||||||
|
|
||||||
_animateClosing(panel, menuOrigin) {
|
_animateClosing(panel, menuOrigin) {
|
||||||
const windowWidth = document.body.offsetWidth;
|
|
||||||
this._animate = true;
|
this._animate = true;
|
||||||
const headerCloak = document.querySelector(".header-cloak");
|
const headerCloak = document.querySelector(".header-cloak");
|
||||||
panel.classList.add("animate");
|
panel.classList.add("animate");
|
||||||
headerCloak.classList.add("animate");
|
headerCloak.classList.add("animate");
|
||||||
const offsetDirection = menuOrigin === "left" ? -1 : 1;
|
if (menuOrigin === "left") {
|
||||||
panel.style.setProperty("--offset", `${offsetDirection * windowWidth}px`);
|
panel.style.setProperty("--offset", `-100vw`);
|
||||||
|
} else {
|
||||||
|
panel.style.setProperty("--offset", `100vw`);
|
||||||
|
}
|
||||||
|
|
||||||
headerCloak.style.setProperty("--opacity", 0);
|
headerCloak.style.setProperty("--opacity", 0);
|
||||||
this._scheduledRemoveAnimate = discourseLater(() => {
|
this._scheduledRemoveAnimate = discourseLater(() => {
|
||||||
panel.classList.remove("animate");
|
panel.classList.remove("animate");
|
||||||
|
@ -365,7 +374,6 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const windowWidth = document.body.offsetWidth;
|
|
||||||
const viewMode =
|
const viewMode =
|
||||||
this.site.mobileView || this.site.narrowDesktopView
|
this.site.mobileView || this.site.narrowDesktopView
|
||||||
? "slide-in"
|
? "slide-in"
|
||||||
|
@ -374,9 +382,6 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||||
menuPanels.forEach((panel) => {
|
menuPanels.forEach((panel) => {
|
||||||
const headerCloak = document.querySelector(".header-cloak");
|
const headerCloak = document.querySelector(".header-cloak");
|
||||||
let width = parseInt(panel.getAttribute("data-max-width"), 10) || 300;
|
let width = parseInt(panel.getAttribute("data-max-width"), 10) || 300;
|
||||||
if (windowWidth - width < 50) {
|
|
||||||
width = windowWidth - 50;
|
|
||||||
}
|
|
||||||
if (this._panMenuOffset) {
|
if (this._panMenuOffset) {
|
||||||
this._panMenuOffset = -width;
|
this._panMenuOffset = -width;
|
||||||
}
|
}
|
||||||
|
@ -384,77 +389,23 @@ const SiteHeaderComponent = MountWidget.extend(
|
||||||
panel.classList.remove("drop-down");
|
panel.classList.remove("drop-down");
|
||||||
panel.classList.remove("slide-in");
|
panel.classList.remove("slide-in");
|
||||||
panel.classList.add(viewMode);
|
panel.classList.add(viewMode);
|
||||||
|
|
||||||
if (this._animate || this._panMenuOffset !== 0) {
|
if (this._animate || this._panMenuOffset !== 0) {
|
||||||
if (
|
if (
|
||||||
(this.site.mobileView || this.site.narrowDesktopView) &&
|
(this.site.mobileView || this.site.narrowDesktopView) &&
|
||||||
panel.parentElement.classList.contains(this._leftMenuClass())
|
panel.parentElement.classList.contains(this._leftMenuClass())
|
||||||
) {
|
) {
|
||||||
this._panMenuOrigin = "left";
|
this._panMenuOrigin = "left";
|
||||||
panel.style.setProperty("--offset", `${-windowWidth}px`);
|
panel.style.setProperty("--offset", `-100vw`);
|
||||||
} else {
|
} else {
|
||||||
this._panMenuOrigin = "right";
|
this._panMenuOrigin = "right";
|
||||||
panel.style.setProperty("--offset", `${windowWidth}px`);
|
panel.style.setProperty("--offset", `100vw`);
|
||||||
}
|
}
|
||||||
headerCloak.style.setProperty("--opacity", 0);
|
headerCloak.style.setProperty("--opacity", 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const panelBody = panel.querySelector(".panel-body");
|
if (viewMode === "slide-in") {
|
||||||
|
|
||||||
// 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 {
|
|
||||||
headerCloak.style.display = "block";
|
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) {
|
if (this._animate) {
|
||||||
this._animateOpening(panel);
|
this._animateOpening(panel);
|
||||||
|
@ -488,12 +439,20 @@ export default SiteHeaderComponent.extend({
|
||||||
|
|
||||||
this.appEvents.on("site-header:force-refresh", this, "queueRerender");
|
this.appEvents.on("site-header:force-refresh", this, "queueRerender");
|
||||||
|
|
||||||
const header = document.querySelector(".d-header-wrap");
|
const headerWrap = document.querySelector(".d-header-wrap");
|
||||||
if (header) {
|
let header;
|
||||||
|
if (headerWrap) {
|
||||||
schedule("afterRender", () => {
|
schedule("afterRender", () => {
|
||||||
|
header = headerWrap.querySelector("header.d-header");
|
||||||
|
const headerOffset = headerWrap.offsetHeight;
|
||||||
|
const headerTop = header.offsetTop;
|
||||||
document.documentElement.style.setProperty(
|
document.documentElement.style.setProperty(
|
||||||
"--header-offset",
|
"--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) => {
|
this._resizeObserver = new ResizeObserver((entries) => {
|
||||||
for (let entry of entries) {
|
for (let entry of entries) {
|
||||||
if (entry.contentRect) {
|
if (entry.contentRect) {
|
||||||
|
const headerOffset = entry.contentRect.height;
|
||||||
|
const headerTop = header.offsetTop;
|
||||||
document.documentElement.style.setProperty(
|
document.documentElement.style.setProperty(
|
||||||
"--header-offset",
|
"--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");
|
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";
|
import { click, triggerEvent, visit } from "@ember/test-helpers";
|
||||||
|
|
||||||
async function triggerSwipeStart(touchTarget) {
|
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
|
// Other tests are shown in a transformed viewport, and this is a multiple for the offsets
|
||||||
let scale = parseFloat(
|
let scale = parseFloat(
|
||||||
window
|
window
|
||||||
|
|
|
@ -29,6 +29,8 @@
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
margin: 3px 0;
|
margin: 3px 0;
|
||||||
}
|
}
|
||||||
|
@ -689,7 +691,7 @@ body.footer-nav-ipad {
|
||||||
background-color: black;
|
background-color: black;
|
||||||
--opacity: 0.5;
|
--opacity: 0.5;
|
||||||
opacity: var(--opacity);
|
opacity: var(--opacity);
|
||||||
top: 0;
|
top: var(--header-top);
|
||||||
left: 0;
|
left: 0;
|
||||||
display: none;
|
display: none;
|
||||||
touch-action: pan-y pinch-zoom;
|
touch-action: pan-y pinch-zoom;
|
||||||
|
@ -702,6 +704,23 @@ body.footer-nav-ipad {
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-panel.slide-in {
|
.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));
|
transform: translateX(var(--offset));
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
&.animate {
|
&.animate {
|
||||||
|
|
Loading…
Reference in New Issue