diff --git a/docs/optimizing.md b/docs/optimizing.md index 920c157..92f3005 100644 --- a/docs/optimizing.md +++ b/docs/optimizing.md @@ -5,7 +5,7 @@ Topic Index: - Time to first byte - Time to first draw: [Load size](#load-size) - CLS -- Framerate: [Algorithm](#algorithm), [CSS containment](#css-containment) +- Framerate: [Algorithm](#algorithm) ## Load size @@ -23,19 +23,4 @@ Don't choose algorithm solely on the time complexity. GUI app needs smooth, not - Think in Map-Reduce framework if you don't have any idea. - On the worker thread: balance the speed and the memory usage. - Arrays are usually faster and use less memory. - - Worker is always available on our target platforms, but workers introduce latency in the starting and the communication. And, even it's available on targets, it's not always - available on user's platform, so fallback is always required. - -## CSS containment - -`contain` property is a powerful tool that hints user agents to utilise specific conditions can only be easily identified by developers. [This article from MDN can help you undetstand this property](https://developer.mozilla.org/en-US/docs/Web/Performance/How_browsers_work). - -But it comes with cost. Modern browsers are already very smart on rendering. Using `contain`, you are trading onething off for another: - -- `layout` affects the reflow. This property usually won't make large change: mainline browsers already can incrementally reflow. -- `style` affacts the style computation, is automatically enabled when using `container` property. Usually won't make large change too, unless you frequently change the styles (and/or your stylesheet is large and/or with complex selectors). -- `paint` affects the shading, the pixel-filling process. This is useful - the shading is resource-heavy - but the browser may need more buffers and more time to compose the final frame. - - This containment may increase memory usage. -- `size` says the size is not affected by outside elements and is defined. It hints the user agent can use the pre-defined size and/or cache the computed size (with `auto` keyword). - - Must be used with `contain-intrinsic-size`. - - You can use `content-visibility: auto`, a stonger hint for browsers to skip elements if possible. You can see this like built-in "virtual list", which is used for rendering infinite size of dataset. + - Worker is always available on our target platforms, but workers introduce latency in the starting and the communication. diff --git a/src/App.tsx b/src/App.tsx index 5e69ff3..e7ca588 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,7 +10,7 @@ import { lazy, onCleanup, } from "solid-js"; -import { createRootTheme } from "./material/theme.js"; +import { useRootTheme } from "./material/theme.js"; import { Provider as ClientProvider, createMastoClientFor, @@ -70,7 +70,7 @@ const Routing: Component = () => { }; const App: Component = () => { - const theme = createRootTheme(); + const theme = useRootTheme(); const accts = useStore($accounts); const lang = createCurrentLanguage(); const region = createCurrentRegion(); diff --git a/src/material/Scaffold.tsx b/src/material/Scaffold.tsx index a2c7b69..4e905c3 100644 --- a/src/material/Scaffold.tsx +++ b/src/material/Scaffold.tsx @@ -2,7 +2,6 @@ import { createElementSize } from "@solid-primitives/resize-observer"; import { JSX, Show, - children, createRenderEffect, createSignal, splitProps, @@ -22,8 +21,8 @@ type ScaffoldProps = ParentProps< /** * The passthrough props are passed to the content container. */ -const Scaffold: Component = (oprops) => { - const [props, rest] = splitProps(oprops, [ +const Scaffold: Component = (props) => { + const [managed, rest] = splitProps(props, [ "topbar", "fab", "bottom", @@ -35,13 +34,9 @@ const Scaffold: Component = (oprops) => { const topbarSize = createElementSize(topbarElement); - const topbar = children(() => props.topbar) - const fab = children(() => props.fab) - const bottom = children(() => props.bottom) - return (
{ createRenderEffect(() => { e.style.setProperty( @@ -50,28 +45,28 @@ const Scaffold: Component = (oprops) => { ); }); - if (props.ref) { - (props.ref as (val: typeof e) => void)(e); + if (managed.ref) { + (managed.ref as (val: typeof e) => void)(e); } }} {...rest} > - + - + - {props.children} + {managed.children} - +
diff --git a/src/material/theme.ts b/src/material/theme.ts index a7a68a8..0eb4eaf 100644 --- a/src/material/theme.ts +++ b/src/material/theme.ts @@ -5,22 +5,21 @@ import { Accessor } from "solid-js"; /** * The MUI theme. */ -export function createRootTheme(): Accessor { - const theme = createTheme({ - palette: { - primary: { - main: deepPurple[500], +export function useRootTheme(): Accessor { + return () => + createTheme({ + palette: { + primary: { + main: deepPurple[500], + }, + error: { + main: red[900], + }, + secondary: { + main: amber.A200, + }, }, - error: { - main: red[900], - }, - secondary: { - main: amber.A200, - }, - }, - }); - - return () => theme; + }); } export const ANIM_CURVE_STD = "cubic-bezier(0.4, 0, 0.2, 1)"; diff --git a/src/platform/StackedRouter.tsx b/src/platform/StackedRouter.tsx index 975d66f..b20106f 100644 --- a/src/platform/StackedRouter.tsx +++ b/src/platform/StackedRouter.tsx @@ -10,7 +10,6 @@ import { Show, untrack, useContext, - onCleanup, type Accessor, } from "solid-js"; import { createStore, unwrap } from "solid-js/store"; @@ -375,30 +374,6 @@ function createManagedSwipeToBack( }; } -function animateUntil( - stepfn: (onCreated: (animation: Animation) => void) => void, -) { - const execStep = () => { - requestAnimationFrame(() => { - stepfn((step) => { - step.addEventListener("finish", () => { - execStep(); - }); - }); - }); - }; - - execStep(); -} - -/** - * The cache key of saved stack for hot reload. - * - * We could not use symbols because every time the hot reload the `Symbol()` - * call creates a new symbol. - */ -const $StackedRouterSavedStack = "$StackedRouterSavedStack"; - /** * The router that stacks the pages. * @@ -442,29 +417,6 @@ const StackedRouter: Component = (oprops) => { const [stack, mutStack] = createStore([] as StackFrame[], { name: "stack" }); const windowSize = useWindowSize(); - if (import.meta.hot) { - const saveStack = () => { - import.meta.hot!.data[$StackedRouterSavedStack] = unwrap(stack); - console.debug("stack saved"); - }; - - import.meta.hot.on("vite:beforeUpdate", saveStack); - onCleanup(() => import.meta.hot!.off("vite:beforeUpdate", saveStack)); - - const loadStack = () => { - const savedStack = import.meta.hot!.data[$StackedRouterSavedStack]; - if (savedStack) { - mutStack(savedStack); - console.debug("stack loaded"); - } - delete import.meta.hot!.data[$StackedRouterSavedStack]; - }; - - createRenderEffect(() => { - loadStack() - }); - } - const pushFrame = (path: string, opts?: Readonly>) => untrack(() => { const frame = { @@ -492,35 +444,11 @@ const StackedRouter: Component = (oprops) => { return frame; }); - const onlyPopFrameOnStack = (depth: number) => { - mutStack((o) => o.toSpliced(o.length - depth, depth)); - }; - const onlyPopFrame = (depth: number) => { - onlyPopFrameOnStack(depth); + mutStack((o) => o.toSpliced(o.length - depth, depth)); window.history.go(-depth); }; - const animatePopOneFrame = (onCreated: (animation: Animation) => void) => { - const lastFrame = stack[stack.length - 1]; - const element = document.getElementById( - lastFrame.rootId, - )! as HTMLDialogElement; - const createAnimation = lastFrame.animateClose ?? animateClose; - element.classList.add("animating"); - - const onNavAnimEnd = () => { - element.classList.remove("animating"); - }; - - requestAnimationFrame(() => { - const animation = createAnimation(element); - animation.addEventListener("finish", onNavAnimEnd); - animation.addEventListener("cancel", onNavAnimEnd); - onCreated(animation); - }); - }; - const popFrame = (depth: number = 1) => untrack(() => { if (import.meta.env.DEV) { @@ -528,27 +456,30 @@ const StackedRouter: Component = (oprops) => { console.warn("the depth to pop should not < 0, now is", depth); } } - if (stack.length > 1) { - let count = depth; - animateUntil((created) => { - if (count > 0) { - animatePopOneFrame((a) => { - a.addEventListener("finish", () => onlyPopFrame(1)); - created(a); - }); - } - count--; + const lastFrame = stack[stack.length - 1]; + const element = document.getElementById( + lastFrame.rootId, + )! as HTMLDialogElement; + const createAnimation = lastFrame.animateClose ?? animateClose; + requestAnimationFrame(() => { + element.classList.add("animating"); + const animation = createAnimation(element); + animation.addEventListener("finish", () => { + element.classList.remove("animating"); + onlyPopFrame(depth); + }); }); } else { - onlyPopFrame(1); + onlyPopFrame(depth); } }); createRenderEffect(() => { if (stack.length === 0) { - pushFrame(window.location.pathname, { - replace: "all", + mutStack(0, { + path: window.location.pathname, + rootId: createUniqueId(), }); } }); @@ -557,23 +488,10 @@ const StackedRouter: Component = (oprops) => { makeEventListener(window, "popstate", (event) => { if (!event.state) return; - // TODO: verify the stack in state and handling forwards - if (stack.length === 0) { - mutStack(event.state || []); + mutStack(event.state); } else if (stack.length > event.state.length) { - let count = stack.length - event.state.length; - animateUntil((created) => { - if (count > 0) { - animatePopOneFrame((a) => { - a.addEventListener("finish", () => { - onlyPopFrameOnStack(1); - created(a); - }); - }); - } - count--; - }); + popFrame(stack.length - event.state.length); } }); }); diff --git a/src/platform/i18n.tsx b/src/platform/i18n.tsx index 69bae99..b64b920 100644 --- a/src/platform/i18n.tsx +++ b/src/platform/i18n.tsx @@ -25,22 +25,12 @@ const DEFAULT_LANG = "en"; /** * Decide the using language for the user. - * - * **Performance**: This function is costy, make sure you cache the result. - * In the app, you should use {@link useAppLocale} instead. - * * @returns the selected language tag */ export function autoMatchLangTag() { return match(Array.from(navigator.languages), SUPPORTED_LANGS, DEFAULT_LANG); } -/** - * Decide the using region for the user. - * - * **Performance**: This function is costy, make sure you cache the result. - * In the app, you should use {@link useAppLocale} instead. - */ export function autoMatchRegion() { const specifiers = navigator.languages.map((x) => x.split("-")); @@ -109,7 +99,7 @@ export function useDateFnLocale(): Accessor { export function createCurrentLanguage() { const settings = useStore($settings); - return createMemo(() => settings().language || autoMatchLangTag()); + return () => settings().language || autoMatchLangTag(); } type ImportFn = (name: string) => Promise<{ default: T }>; @@ -124,30 +114,10 @@ type MergedImportedModule = T extends [] ? ImportedModule & MergedImportedModule : never; -/** - * Create a resource that combines all I18N strings into one object. - * - * The result is combined in the order of the argument functions. - * The formers will be overrided by the latter. - * - * @param importFns a series of functions imports the string modules - * based on the specified language code. - * - * **Context**: This function must be used under {@link AppLocaleProvider}. - * - * @example ````ts - * const [strings] = createStringResource( - * async (code) => await import(`./i18n/${code}.json`), // Vite can handle the bundling - * async () => import("./i18n/generic.json"), // You can also ignore the code. - * ); - * ```` - * - * @see {@link createTranslator} if you need a Translator from "@solid-primitives/i18n" - */ export function createStringResource< T extends ImportFn | undefined>>[], >(...importFns: T) { - const { language } = useAppLocale(); + const language = createCurrentLanguage(); const cache: Record> = {}; return createResource( @@ -170,18 +140,6 @@ export function createStringResource< ); } -/** - * Create the Translator from "@solid-primitives/i18n" based on - * the {@link createStringResource}. - * - * @param importFns same to {@link createStringResource} - * - * @returns the first element is the translator, the second is the result from - * {@link createStringResource}. - * - * @see {@link translator} for the translator usage - * @see {@link createStringResource} for the raw strings - */ export function createTranslator< T extends ImportFn | undefined>>[], >(...importFns: T) { diff --git a/src/platform/share.tsx b/src/platform/share.tsx index f135736..3f1d870 100644 --- a/src/platform/share.tsx +++ b/src/platform/share.tsx @@ -16,7 +16,7 @@ import { import { Close as CloseIcon, ContentCopy } from "@suid/icons-material"; import { Title } from "~material/typography"; import { render } from "solid-js/web"; -import { createRootTheme } from "~material/theme"; +import { useRootTheme } from "~material/theme"; const ShareBottomSheet: Component<{ data?: ShareData; @@ -78,7 +78,7 @@ export async function share(data?: ShareData): Promise { const dispose = render(() => { const [open, setOpen] = createSignal(true); - const theme = createRootTheme(); + const theme = useRootTheme(); onCleanup(() => { element.remove(); resolve(); diff --git a/src/timelines/RegularToot.css b/src/timelines/RegularToot.css index e00535e..d7a50c3 100644 --- a/src/timelines/RegularToot.css +++ b/src/timelines/RegularToot.css @@ -4,7 +4,7 @@ --toot-avatar-size: 40px; margin-block: 0; position: relative; - contain: content; + contain: layout style; cursor: pointer;