frontend optimization: since icon-loading detection mechanism was introduced, performance were inadequate in case of thousands of files, hanging the browser

Massimo Melina committed Dec 21, 2021 at 11:54 UTC f4853e0e0bdb1f3d159ec9ebfaaee74de772e3cc
7 files changed +35 -67
frontend/src/App.tsx
+1 -1
@@ -10,4 +10,4 @@ function App() {
10 )
11 }
12
13 -export default App;
\ No newline at end of file
13 +export default App;
frontend/src/BrowseFiles.ts
+2 -2
@@ -2,7 +2,7 @@ import { Link, useLocation } from 'react-router-dom'
2 import { useApi } from './api'
3 import { createContext, createElement as h, Fragment, useContext, useEffect, useMemo, useState } from 'react'
4 import { formatBytes, hError, hIcon } from './misc'
5 -import { Loading, Spinner } from './components'
5 +import { Spinner } from './components'
6 import { Head } from './Head'
7 import { state, useSnapState } from './state'
8 import _ from 'lodash'
@@ -16,7 +16,7 @@ export const ListContext = createContext<{ list:DirList, unfinished:boolean }>({
16 export function BrowseFiles() {
17 const [list, unfinished] = useFetchList()
18 if (!list)
19 - return h(Loading)
19 + return h(Spinner)
20 if (list instanceof Error)
21 return hError(list)
22 return h(ListContext.Provider, { value:{ list, unfinished } },
frontend/src/components.ts
+2 -26
@@ -1,30 +1,6 @@
1 -import { createElement as h } from 'react'
2 -import { ICON_FONT_NAME, useIconsReady, usePromise } from './hooks'
3 -
4 -const SYS_ICONS: Record<string,string> = {
5 - login: 'person',
6 - user: 'account_circle',
7 - file: 'description',
8 - spinner: 'sports_baseball',
9 - filter: 'filter_alt',
10 -}
11 -
12 -const iconClass = ICON_FONT_NAME.then(v => v.replace(/ /g,'-').toLowerCase())
13 -export function Icon({ name, forced, ...props }: { name:string, forced?:boolean, style?:any }) {
14 - name = SYS_ICONS[name] || name
15 - const cl = usePromise(iconClass)
16 - return h('span',{
17 - className: cl+' icon',
18 - ...props
19 - }, useIconsReady() || forced ? name : '')
20 -}
21 -
22 -export function Loading() {
23 - return useIconsReady() ? h(Spinner)
24 - : h('span', {}, 'Loading...')
25 -}
1 +import { hIcon } from './misc'
2
3 export function Spinner() {
28 - return h(Icon, { name:'spinner', style: { animation:'1s spin infinite' } })
4 + return hIcon('spinner', { style: { animation:'1s spin infinite' } })
5 }
6
frontend/src/hooks.ts deleted
-37
@@ -1,37 +0,0 @@
1 -import { useCallback, useEffect, useRef, useState } from 'react'
2 -import { waitFor } from './misc'
3 -
4 -export function useIsMounted() {
5 - const ref = useRef(true)
6 -
7 - useEffect(() => () => {
8 - ref.current = false
9 - }, [])
10 -
11 - return useCallback(()=> ref.current, [ref])
12 -}
13 -
14 -export function useStateMounted(init?: any) {
15 - const isMounted = useIsMounted()
16 - const [v,set] = useState(init)
17 - const setIfMounted = useCallback(x => {
18 - if (isMounted())
19 - set(x)
20 - }, [isMounted,set])
21 - return [v, setIfMounted, isMounted]
22 -}
23 -
24 -export const ICON_FONT_NAME = waitFor(()=> document.getElementById('iconsFile')).then(el => decodeURIComponent((el as HTMLLinkElement).href.split('=')[1].replace(/\+/g, ' ')))
25 -const iconsReady = document.fonts.ready.then(()=> ICON_FONT_NAME).then(name => document.fonts.load(`9px "${name}"`))
26 -
27 -export function usePromise<T>(p:Promise<T>): T | undefined {
28 - const [v, setV] = useStateMounted()
29 - useEffect(()=>{
30 - p.then(setV)
31 - }, [])
32 - return v
33 -}
34 -
35 -export function useIconsReady() {
36 - return usePromise(iconsReady)
37 -}
frontend/src/icons.ts new
+28
@@ -0,0 +1,28 @@
1 +import { state, useSnapState } from './state'
2 +import { createElement as h } from 'react'
3 +import { waitFor } from './misc'
4 +
5 +const SYS_ICONS: Record<string,string> = {
6 + login: 'person',
7 + user: 'account_circle',
8 + file: 'description',
9 + spinner: 'sports_baseball',
10 + filter: 'filter_alt',
11 +}
12 +
13 +document.fonts.ready.then(async ()=> {
14 + const el = await waitFor(()=> document.getElementById('iconsFile'))
15 + const name = decodeURIComponent((el as HTMLLinkElement).href.split('=')[1].replace(/\+/g, ' '))
16 + await document.fonts.load(`9px "${name}"`) // force font to be loaded even if we didn't display anything with it yet
17 + state.iconsClass = name.replace(/ /g,'-').toLowerCase()
18 +})
19 +
20 +export function Icon({ name, ...props }: { name:string, style?:any }) {
21 + name = SYS_ICONS[name] || name
22 + const { iconsClass } = useSnapState()
23 + return h('span',{
24 + className: iconsClass+' icon',
25 + ...props
26 + }, iconsClass ? name : '#')
27 +}
28 +
frontend/src/misc.ts
+1 -1
@@ -1,5 +1,5 @@
1 import { createElement as h } from 'react'
2 -import { Icon } from './components'
2 +import { Icon } from './icons'
3
4 export type Falsy = false | null | undefined | '' | 0
5
frontend/src/state.ts
+1
@@ -1,6 +1,7 @@
1 import { proxy, useSnapshot } from 'valtio'
2
3 export const state = proxy({
4 + iconsClass: '',
5 username: '',
6 listFilter: '',
7 remoteSearch: '',