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 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.
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)";
|
||||||
|
|
|
@ -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--;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue