Compare commits

..

No commits in common. "88ff705d685c8f5ebcbe9482d4d0cdd4296dbc7b" and "d55a117aa40e27398d728e3a9809d114b0979052" have entirely different histories.

8 changed files with 54 additions and 199 deletions

View file

@ -5,7 +5,7 @@ Topic Index:
- Time to first byte - Time to first byte
- Time to first draw: [Load size](#load-size) - Time to first draw: [Load size](#load-size)
- CLS - CLS
- Framerate: [Algorithm](#algorithm), [CSS containment](#css-containment) - Framerate: [Algorithm](#algorithm)
## Load size ## 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. - Think in Map-Reduce framework if you don't have any idea.
- On the worker thread: balance the speed and the memory usage. - On the worker thread: balance the speed and the memory usage.
- Arrays are usually faster and use less memory. - 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 - Worker is always available on our target platforms, but workers introduce latency in the starting and the communication.
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.

View file

@ -10,7 +10,7 @@ import {
lazy, lazy,
onCleanup, onCleanup,
} from "solid-js"; } from "solid-js";
import { createRootTheme } from "./material/theme.js"; import { useRootTheme } from "./material/theme.js";
import { import {
Provider as ClientProvider, Provider as ClientProvider,
createMastoClientFor, createMastoClientFor,
@ -70,7 +70,7 @@ const Routing: Component = () => {
}; };
const App: Component = () => { const App: Component = () => {
const theme = createRootTheme(); const theme = useRootTheme();
const accts = useStore($accounts); const accts = useStore($accounts);
const lang = createCurrentLanguage(); const lang = createCurrentLanguage();
const region = createCurrentRegion(); const region = createCurrentRegion();

View file

@ -2,7 +2,6 @@ import { createElementSize } from "@solid-primitives/resize-observer";
import { import {
JSX, JSX,
Show, Show,
children,
createRenderEffect, createRenderEffect,
createSignal, createSignal,
splitProps, splitProps,
@ -22,8 +21,8 @@ type ScaffoldProps = ParentProps<
/** /**
* The passthrough props are passed to the content container. * The passthrough props are passed to the content container.
*/ */
const Scaffold: Component<ScaffoldProps> = (oprops) => { const Scaffold: Component<ScaffoldProps> = (props) => {
const [props, rest] = splitProps(oprops, [ const [managed, rest] = splitProps(props, [
"topbar", "topbar",
"fab", "fab",
"bottom", "bottom",
@ -35,13 +34,9 @@ const Scaffold: Component<ScaffoldProps> = (oprops) => {
const topbarSize = createElementSize(topbarElement); const topbarSize = createElementSize(topbarElement);
const topbar = children(() => props.topbar)
const fab = children(() => props.fab)
const bottom = children(() => props.bottom)
return ( return (
<div <div
class={`Scaffold ${props.class || ""}`} class={`Scaffold ${managed.class || ""}`}
ref={(e) => { ref={(e) => {
createRenderEffect(() => { createRenderEffect(() => {
e.style.setProperty( e.style.setProperty(
@ -50,28 +45,28 @@ const Scaffold: Component<ScaffoldProps> = (oprops) => {
); );
}); });
if (props.ref) { if (managed.ref) {
(props.ref as (val: typeof e) => void)(e); (managed.ref as (val: typeof e) => void)(e);
} }
}} }}
{...rest} {...rest}
> >
<Show when={topbar()}> <Show when={props.topbar}>
<div class="topbar" ref={setTopbarElement} role="presentation"> <div class="topbar" ref={setTopbarElement} role="presentation">
{topbar()} {props.topbar}
</div> </div>
</Show> </Show>
<Show when={fab()}> <Show when={props.fab}>
<div class="fab-dock" role="presentation"> <div class="fab-dock" role="presentation">
{fab()} {props.fab}
</div> </div>
</Show> </Show>
{props.children} {managed.children}
<Show when={bottom()}> <Show when={props.bottom}>
<div class="bottom-dock" role="presentation"> <div class="bottom-dock" role="presentation">
{bottom()} {props.bottom}
</div> </div>
</Show> </Show>
</div> </div>

View file

@ -5,22 +5,21 @@ import { Accessor } from "solid-js";
/** /**
* The MUI theme. * The MUI theme.
*/ */
export function createRootTheme(): Accessor<Theme> { export function useRootTheme(): Accessor<Theme> {
const theme = createTheme({ return () =>
palette: { createTheme({
primary: { palette: {
main: deepPurple[500], 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)"; export const ANIM_CURVE_STD = "cubic-bezier(0.4, 0, 0.2, 1)";

View file

@ -10,7 +10,6 @@ import {
Show, Show,
untrack, untrack,
useContext, useContext,
onCleanup,
type Accessor, type Accessor,
} from "solid-js"; } from "solid-js";
import { createStore, unwrap } from "solid-js/store"; 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. * The router that stacks the pages.
* *
@ -442,29 +417,6 @@ const StackedRouter: Component<StackedRouterProps> = (oprops) => {
const [stack, mutStack] = createStore([] as StackFrame[], { name: "stack" }); const [stack, mutStack] = createStore([] as StackFrame[], { name: "stack" });
const windowSize = useWindowSize(); 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<NewFrameOptions<any>>) => const pushFrame = (path: string, opts?: Readonly<NewFrameOptions<any>>) =>
untrack(() => { untrack(() => {
const frame = { const frame = {
@ -492,35 +444,11 @@ const StackedRouter: Component<StackedRouterProps> = (oprops) => {
return frame; return frame;
}); });
const onlyPopFrameOnStack = (depth: number) => {
mutStack((o) => o.toSpliced(o.length - depth, depth));
};
const onlyPopFrame = (depth: number) => { const onlyPopFrame = (depth: number) => {
onlyPopFrameOnStack(depth); mutStack((o) => o.toSpliced(o.length - depth, depth));
window.history.go(-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) => const popFrame = (depth: number = 1) =>
untrack(() => { untrack(() => {
if (import.meta.env.DEV) { if (import.meta.env.DEV) {
@ -528,27 +456,30 @@ const StackedRouter: Component<StackedRouterProps> = (oprops) => {
console.warn("the depth to pop should not < 0, now is", depth); console.warn("the depth to pop should not < 0, now is", depth);
} }
} }
if (stack.length > 1) { if (stack.length > 1) {
let count = depth; const lastFrame = stack[stack.length - 1];
animateUntil((created) => { const element = document.getElementById(
if (count > 0) { lastFrame.rootId,
animatePopOneFrame((a) => { )! as HTMLDialogElement;
a.addEventListener("finish", () => onlyPopFrame(1)); const createAnimation = lastFrame.animateClose ?? animateClose;
created(a); requestAnimationFrame(() => {
}); element.classList.add("animating");
} const animation = createAnimation(element);
count--; animation.addEventListener("finish", () => {
element.classList.remove("animating");
onlyPopFrame(depth);
});
}); });
} else { } else {
onlyPopFrame(1); onlyPopFrame(depth);
} }
}); });
createRenderEffect(() => { createRenderEffect(() => {
if (stack.length === 0) { if (stack.length === 0) {
pushFrame(window.location.pathname, { mutStack(0, {
replace: "all", path: window.location.pathname,
rootId: createUniqueId(),
}); });
} }
}); });
@ -557,23 +488,10 @@ const StackedRouter: Component<StackedRouterProps> = (oprops) => {
makeEventListener(window, "popstate", (event) => { makeEventListener(window, "popstate", (event) => {
if (!event.state) return; if (!event.state) return;
// TODO: verify the stack in state and handling forwards
if (stack.length === 0) { if (stack.length === 0) {
mutStack(event.state || []); mutStack(event.state);
} else if (stack.length > event.state.length) { } else if (stack.length > event.state.length) {
let count = stack.length - event.state.length; popFrame(stack.length - event.state.length);
animateUntil((created) => {
if (count > 0) {
animatePopOneFrame((a) => {
a.addEventListener("finish", () => {
onlyPopFrameOnStack(1);
created(a);
});
});
}
count--;
});
} }
}); });
}); });

View file

@ -25,22 +25,12 @@ const DEFAULT_LANG = "en";
/** /**
* Decide the using language for the user. * 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 * @returns the selected language tag
*/ */
export function autoMatchLangTag() { export function autoMatchLangTag() {
return match(Array.from(navigator.languages), SUPPORTED_LANGS, DEFAULT_LANG); 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() { export function autoMatchRegion() {
const specifiers = navigator.languages.map((x) => x.split("-")); const specifiers = navigator.languages.map((x) => x.split("-"));
@ -109,7 +99,7 @@ export function useDateFnLocale(): Accessor<Locale> {
export function createCurrentLanguage() { export function createCurrentLanguage() {
const settings = useStore($settings); const settings = useStore($settings);
return createMemo(() => settings().language || autoMatchLangTag()); return () => settings().language || autoMatchLangTag();
} }
type ImportFn<T> = (name: string) => Promise<{ default: T }>; type ImportFn<T> = (name: string) => Promise<{ default: T }>;
@ -124,30 +114,10 @@ type MergedImportedModule<T> = T extends []
? ImportedModule<I> & MergedImportedModule<J> ? ImportedModule<I> & MergedImportedModule<J>
: never; : 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< export function createStringResource<
T extends ImportFn<Record<string, string | Template<any> | undefined>>[], T extends ImportFn<Record<string, string | Template<any> | undefined>>[],
>(...importFns: T) { >(...importFns: T) {
const { language } = useAppLocale(); const language = createCurrentLanguage();
const cache: Record<string, MergedImportedModule<T>> = {}; const cache: Record<string, MergedImportedModule<T>> = {};
return createResource( 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< export function createTranslator<
T extends ImportFn<Record<string, string | Template<any> | undefined>>[], T extends ImportFn<Record<string, string | Template<any> | undefined>>[],
>(...importFns: T) { >(...importFns: T) {

View file

@ -16,7 +16,7 @@ import {
import { Close as CloseIcon, ContentCopy } from "@suid/icons-material"; import { Close as CloseIcon, ContentCopy } from "@suid/icons-material";
import { Title } from "~material/typography"; import { Title } from "~material/typography";
import { render } from "solid-js/web"; import { render } from "solid-js/web";
import { createRootTheme } from "~material/theme"; import { useRootTheme } from "~material/theme";
const ShareBottomSheet: Component<{ const ShareBottomSheet: Component<{
data?: ShareData; data?: ShareData;
@ -78,7 +78,7 @@ export async function share(data?: ShareData): Promise<void> {
const dispose = render(() => { const dispose = render(() => {
const [open, setOpen] = createSignal(true); const [open, setOpen] = createSignal(true);
const theme = createRootTheme(); const theme = useRootTheme();
onCleanup(() => { onCleanup(() => {
element.remove(); element.remove();
resolve(); resolve();

View file

@ -4,7 +4,7 @@
--toot-avatar-size: 40px; --toot-avatar-size: 40px;
margin-block: 0; margin-block: 0;
position: relative; position: relative;
contain: content; contain: layout style;
cursor: pointer; cursor: pointer;