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

View file

@ -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();

View file

@ -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<ScaffoldProps> = (oprops) => {
const [props, rest] = splitProps(oprops, [
const Scaffold: Component<ScaffoldProps> = (props) => {
const [managed, rest] = splitProps(props, [
"topbar",
"fab",
"bottom",
@ -35,13 +34,9 @@ const Scaffold: Component<ScaffoldProps> = (oprops) => {
const topbarSize = createElementSize(topbarElement);
const topbar = children(() => props.topbar)
const fab = children(() => props.fab)
const bottom = children(() => props.bottom)
return (
<div
class={`Scaffold ${props.class || ""}`}
class={`Scaffold ${managed.class || ""}`}
ref={(e) => {
createRenderEffect(() => {
e.style.setProperty(
@ -50,28 +45,28 @@ const Scaffold: Component<ScaffoldProps> = (oprops) => {
);
});
if (props.ref) {
(props.ref as (val: typeof e) => void)(e);
if (managed.ref) {
(managed.ref as (val: typeof e) => void)(e);
}
}}
{...rest}
>
<Show when={topbar()}>
<Show when={props.topbar}>
<div class="topbar" ref={setTopbarElement} role="presentation">
{topbar()}
{props.topbar}
</div>
</Show>
<Show when={fab()}>
<Show when={props.fab}>
<div class="fab-dock" role="presentation">
{fab()}
{props.fab}
</div>
</Show>
{props.children}
{managed.children}
<Show when={bottom()}>
<Show when={props.bottom}>
<div class="bottom-dock" role="presentation">
{bottom()}
{props.bottom}
</div>
</Show>
</div>

View file

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

View file

@ -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<StackedRouterProps> = (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<NewFrameOptions<any>>) =>
untrack(() => {
const frame = {
@ -492,35 +444,11 @@ const StackedRouter: Component<StackedRouterProps> = (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<StackedRouterProps> = (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<StackedRouterProps> = (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);
}
});
});

View file

@ -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<Locale> {
export function createCurrentLanguage() {
const settings = useStore($settings);
return createMemo(() => settings().language || autoMatchLangTag());
return () => settings().language || autoMatchLangTag();
}
type ImportFn<T> = (name: string) => Promise<{ default: T }>;
@ -124,30 +114,10 @@ type MergedImportedModule<T> = T extends []
? ImportedModule<I> & MergedImportedModule<J>
: 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<Record<string, string | Template<any> | undefined>>[],
>(...importFns: T) {
const { language } = useAppLocale();
const language = createCurrentLanguage();
const cache: Record<string, MergedImportedModule<T>> = {};
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<Record<string, string | Template<any> | undefined>>[],
>(...importFns: T) {

View file

@ -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<void> {
const dispose = render(() => {
const [open, setOpen] = createSignal(true);
const theme = createRootTheme();
const theme = useRootTheme();
onCleanup(() => {
element.remove();
resolve();

View file

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