PERF: Refactor slide-in menu sizing for improved performance ()

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
app/assets
javascripts/discourse
app/components
tests/acceptance
stylesheets/common/base

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