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:
David Taylor 2023-02-21 13:55:38 +00:00 committed by GitHub
parent f7c57fbc19
commit c82094cd9d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 59 additions and 76 deletions

View File

@ -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;
}

View File

@ -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

View File

@ -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 {