Compare commits
6 commits
d55a117aa4
...
88ff705d68
Author | SHA1 | Date | |
---|---|---|---|
|
88ff705d68 | ||
|
fe39675d0c | ||
|
62a80ddce2 | ||
|
b1812392cb | ||
|
6df5c2e216 | ||
|
62d3a5a3d0 |
8 changed files with 199 additions and 54 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)
|
- Framerate: [Algorithm](#algorithm), [CSS containment](#css-containment)
|
||||||
|
|
||||||
## Load size
|
## Load size
|
||||||
|
|
||||||
|
@ -23,4 +23,19 @@ 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.
|
- 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.
|
||||||
|
|
|
@ -10,7 +10,7 @@ import {
|
||||||
lazy,
|
lazy,
|
||||||
onCleanup,
|
onCleanup,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
import { useRootTheme } from "./material/theme.js";
|
import { createRootTheme } 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 = useRootTheme();
|
const theme = createRootTheme();
|
||||||
const accts = useStore($accounts);
|
const accts = useStore($accounts);
|
||||||
const lang = createCurrentLanguage();
|
const lang = createCurrentLanguage();
|
||||||
const region = createCurrentRegion();
|
const region = createCurrentRegion();
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { createElementSize } from "@solid-primitives/resize-observer";
|
||||||
import {
|
import {
|
||||||
JSX,
|
JSX,
|
||||||
Show,
|
Show,
|
||||||
|
children,
|
||||||
createRenderEffect,
|
createRenderEffect,
|
||||||
createSignal,
|
createSignal,
|
||||||
splitProps,
|
splitProps,
|
||||||
|
@ -21,8 +22,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> = (props) => {
|
const Scaffold: Component<ScaffoldProps> = (oprops) => {
|
||||||
const [managed, rest] = splitProps(props, [
|
const [props, rest] = splitProps(oprops, [
|
||||||
"topbar",
|
"topbar",
|
||||||
"fab",
|
"fab",
|
||||||
"bottom",
|
"bottom",
|
||||||
|
@ -34,9 +35,13 @@ const Scaffold: Component<ScaffoldProps> = (props) => {
|
||||||
|
|
||||||
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 ${managed.class || ""}`}
|
class={`Scaffold ${props.class || ""}`}
|
||||||
ref={(e) => {
|
ref={(e) => {
|
||||||
createRenderEffect(() => {
|
createRenderEffect(() => {
|
||||||
e.style.setProperty(
|
e.style.setProperty(
|
||||||
|
@ -45,28 +50,28 @@ const Scaffold: Component<ScaffoldProps> = (props) => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (managed.ref) {
|
if (props.ref) {
|
||||||
(managed.ref as (val: typeof e) => void)(e);
|
(props.ref as (val: typeof e) => void)(e);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
{...rest}
|
{...rest}
|
||||||
>
|
>
|
||||||
<Show when={props.topbar}>
|
<Show when={topbar()}>
|
||||||
<div class="topbar" ref={setTopbarElement} role="presentation">
|
<div class="topbar" ref={setTopbarElement} role="presentation">
|
||||||
{props.topbar}
|
{topbar()}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.fab}>
|
<Show when={fab()}>
|
||||||
<div class="fab-dock" role="presentation">
|
<div class="fab-dock" role="presentation">
|
||||||
{props.fab}
|
{fab()}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
{managed.children}
|
{props.children}
|
||||||
|
|
||||||
<Show when={props.bottom}>
|
<Show when={bottom()}>
|
||||||
<div class="bottom-dock" role="presentation">
|
<div class="bottom-dock" role="presentation">
|
||||||
{props.bottom}
|
{bottom()}
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,21 +5,22 @@ import { Accessor } from "solid-js";
|
||||||
/**
|
/**
|
||||||
* The MUI theme.
|
* The MUI theme.
|
||||||
*/
|
*/
|
||||||
export function useRootTheme(): Accessor<Theme> {
|
export function createRootTheme(): Accessor<Theme> {
|
||||||
return () =>
|
const theme = createTheme({
|
||||||
createTheme({
|
palette: {
|
||||||
palette: {
|
primary: {
|
||||||
primary: {
|
main: deepPurple[500],
|
||||||
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,6 +10,7 @@ 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";
|
||||||
|
@ -374,6 +375,30 @@ 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.
|
||||||
*
|
*
|
||||||
|
@ -417,6 +442,29 @@ 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 = {
|
||||||
|
@ -444,11 +492,35 @@ const StackedRouter: Component<StackedRouterProps> = (oprops) => {
|
||||||
return frame;
|
return frame;
|
||||||
});
|
});
|
||||||
|
|
||||||
const onlyPopFrame = (depth: number) => {
|
const onlyPopFrameOnStack = (depth: number) => {
|
||||||
mutStack((o) => o.toSpliced(o.length - depth, depth));
|
mutStack((o) => o.toSpliced(o.length - depth, depth));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onlyPopFrame = (depth: number) => {
|
||||||
|
onlyPopFrameOnStack(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) {
|
||||||
|
@ -456,30 +528,27 @@ 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) {
|
||||||
const lastFrame = stack[stack.length - 1];
|
let count = depth;
|
||||||
const element = document.getElementById(
|
animateUntil((created) => {
|
||||||
lastFrame.rootId,
|
if (count > 0) {
|
||||||
)! as HTMLDialogElement;
|
animatePopOneFrame((a) => {
|
||||||
const createAnimation = lastFrame.animateClose ?? animateClose;
|
a.addEventListener("finish", () => onlyPopFrame(1));
|
||||||
requestAnimationFrame(() => {
|
created(a);
|
||||||
element.classList.add("animating");
|
});
|
||||||
const animation = createAnimation(element);
|
}
|
||||||
animation.addEventListener("finish", () => {
|
count--;
|
||||||
element.classList.remove("animating");
|
|
||||||
onlyPopFrame(depth);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
onlyPopFrame(depth);
|
onlyPopFrame(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
createRenderEffect(() => {
|
createRenderEffect(() => {
|
||||||
if (stack.length === 0) {
|
if (stack.length === 0) {
|
||||||
mutStack(0, {
|
pushFrame(window.location.pathname, {
|
||||||
path: window.location.pathname,
|
replace: "all",
|
||||||
rootId: createUniqueId(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -488,10 +557,23 @@ 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) {
|
||||||
popFrame(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--;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -25,12 +25,22 @@ 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("-"));
|
||||||
|
|
||||||
|
@ -99,7 +109,7 @@ export function useDateFnLocale(): Accessor<Locale> {
|
||||||
|
|
||||||
export function createCurrentLanguage() {
|
export function createCurrentLanguage() {
|
||||||
const settings = useStore($settings);
|
const settings = useStore($settings);
|
||||||
return () => settings().language || autoMatchLangTag();
|
return createMemo(() => settings().language || autoMatchLangTag());
|
||||||
}
|
}
|
||||||
|
|
||||||
type ImportFn<T> = (name: string) => Promise<{ default: T }>;
|
type ImportFn<T> = (name: string) => Promise<{ default: T }>;
|
||||||
|
@ -114,10 +124,30 @@ 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 = createCurrentLanguage();
|
const { language } = useAppLocale();
|
||||||
const cache: Record<string, MergedImportedModule<T>> = {};
|
const cache: Record<string, MergedImportedModule<T>> = {};
|
||||||
|
|
||||||
return createResource(
|
return createResource(
|
||||||
|
@ -140,6 +170,18 @@ 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 { useRootTheme } from "~material/theme";
|
import { createRootTheme } 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 = useRootTheme();
|
const theme = createRootTheme();
|
||||||
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: layout style;
|
contain: content;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue