Compare commits
No commits in common. "88ff705d685c8f5ebcbe9482d4d0cdd4296dbc7b" and "d55a117aa40e27398d728e3a9809d114b0979052" have entirely different histories.
88ff705d68
...
d55a117aa4
8 changed files with 54 additions and 199 deletions
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -5,8 +5,9 @@ import { Accessor } from "solid-js";
|
|||
/**
|
||||
* The MUI theme.
|
||||
*/
|
||||
export function createRootTheme(): Accessor<Theme> {
|
||||
const theme = createTheme({
|
||||
export function useRootTheme(): Accessor<Theme> {
|
||||
return () =>
|
||||
createTheme({
|
||||
palette: {
|
||||
primary: {
|
||||
main: deepPurple[500],
|
||||
|
@ -19,8 +20,6 @@ export function createRootTheme(): Accessor<Theme> {
|
|||
},
|
||||
},
|
||||
});
|
||||
|
||||
return () => theme;
|
||||
}
|
||||
|
||||
export const ANIM_CURVE_STD = "cubic-bezier(0.4, 0, 0.2, 1)";
|
||||
|
|
|
@ -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);
|
||||
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);
|
||||
});
|
||||
}
|
||||
count--;
|
||||
});
|
||||
} 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
--toot-avatar-size: 40px;
|
||||
margin-block: 0;
|
||||
position: relative;
|
||||
contain: content;
|
||||
contain: layout style;
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue