Upgrade: react 18 && connection table scroll perf

This commit is contained in:
Dreamacro 2022-03-30 20:19:48 +08:00
parent fa98e692bd
commit 2866bafc6a
7 changed files with 324 additions and 484 deletions

View File

@ -33,10 +33,10 @@
"@types/react-table": "^7.7.10", "@types/react-table": "^7.7.10",
"@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-virtualized-auto-sizer": "^1.0.1",
"@types/react-window": "^1.8.5", "@types/react-window": "^1.8.5",
"@typescript-eslint/eslint-plugin": "^5.16.0", "@typescript-eslint/eslint-plugin": "^5.17.0",
"@typescript-eslint/parser": "^5.16.0", "@typescript-eslint/parser": "^5.17.0",
"@vitejs/plugin-react": "^1.2.0", "@vitejs/plugin-react": "^1.2.0",
"eslint": "^8.11.0", "eslint": "^8.12.0",
"eslint-config-airbnb": "^19.0.4", "eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^16.1.4", "eslint-config-airbnb-typescript": "^16.1.4",
"eslint-config-standard-with-typescript": "^21.0.1", "eslint-config-standard-with-typescript": "^21.0.1",
@ -44,18 +44,19 @@
"eslint-plugin-import": "^2.25.4", "eslint-plugin-import": "^2.25.4",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^6.0.0", "eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react-hooks": "^4.3.0", "eslint-plugin-react-hooks": "^4.4.0",
"sass": "^1.49.9", "sass": "^1.49.9",
"type-fest": "^2.12.1", "type-fest": "^2.12.1",
"typescript": "^4.6.3", "typescript": "^4.6.3",
"vite": "^2.8.6", "vite": "^2.9.0",
"vite-plugin-pwa": "^0.11.13", "vite-plugin-pwa": "^0.11.13",
"vite-plugin-windicss": "^1.8.3", "vite-plugin-windicss": "^1.8.3",
"vite-tsconfig-paths": "^3.4.1", "vite-tsconfig-paths": "^3.4.1",
"windicss": "^3.5.1" "windicss": "^3.5.1"
}, },
"dependencies": { "dependencies": {
"@tanstack/react-table": "^8.0.0-alpha.8", "@react-hookz/web": "^13.1.0",
"@tanstack/react-table": "^8.0.0-alpha.11",
"axios": "^0.26.1", "axios": "^0.26.1",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"dayjs": "^1.11.0", "dayjs": "^1.11.0",
@ -64,10 +65,9 @@
"jotai": "^1.6.1", "jotai": "^1.6.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"neverthrow": "^4.3.1", "neverthrow": "^4.3.1",
"react": "^18.0.0-rc.3", "react": "^18.0.0",
"react-dom": "^18.0.0-rc.3", "react-dom": "^18.0.0",
"react-router-dom": "^6.2.2", "react-router-dom": "^6.2.2",
"react-use": "^17.3.2",
"react-virtualized-auto-sizer": "^1.0.6", "react-virtualized-auto-sizer": "^1.0.6",
"react-window": "^1.8.6", "react-window": "^1.8.6",
"swr": "^1.2.2", "swr": "^1.2.2",

721
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,9 @@
import { columnFilterRowsFn, createTable, sortRowsFn } from '@tanstack/react-table' import { useIntersectionObserver, useSyncedRef } from '@react-hookz/web/esm'
import { type ColumnSort } from '@tanstack/react-table/build/types/features/Sorting' import { useTable, columnFilterRowsFn, createTable, sortRowsFn } from '@tanstack/react-table'
import classnames from 'classnames' import classnames from 'classnames'
import produce from 'immer' import produce from 'immer'
import { groupBy } from 'lodash-es' import { groupBy } from 'lodash-es'
import { useMemo, useLayoutEffect, useRef, useState, useEffect } from 'react' import { useMemo, useLayoutEffect, useRef, useState, useEffect } from 'react'
import { useLatest, useScroll } from 'react-use'
import { Header, Checkbox, Modal, Icon, Drawer, Card, Button } from '@components' import { Header, Checkbox, Modal, Icon, Drawer, Card, Button } from '@components'
import { fromNow } from '@lib/date' import { fromNow } from '@lib/date'
@ -47,7 +46,7 @@ function formatSpeed (upload: number, download: number) {
} }
} }
const table = createTable().RowType<FormatConnection>() const table = createTable<FormatConnection>()
export default function Connections () { export default function Connections () {
const { translation, lang } = useI18n() const { translation, lang } = useI18n()
@ -95,8 +94,8 @@ export default function Connections () {
}, [connections]) }, [connections])
// table // table
const tableRef = useRef<HTMLDivElement>(null) const pinRef = useRef<HTMLTableCellElement>(null)
const { x: scrollX } = useScroll(tableRef) const intersection = useIntersectionObserver(pinRef, { threshold: [1] })
const columns = useMemo( const columns = useMemo(
() => table.createColumns([ () => table.createColumns([
table.createDataColumn(Columns.Host, { minWidth: 260, width: 260, header: t(`columns.${Columns.Host}`) }), table.createDataColumn(Columns.Host, { minWidth: 260, width: 260, header: t(`columns.${Columns.Host}`) }),
@ -113,28 +112,28 @@ export default function Connections () {
width: 200, width: 200,
sortDescFirst: true, sortDescFirst: true,
sortType (rowA, rowB) { sortType (rowA, rowB) {
const speedA = rowA.original.speed const speedA = rowA.original?.speed ?? { upload: 0, download: 0 }
const speedB = rowB.original.speed const speedB = rowB.original?.speed ?? { upload: 0, download: 0 }
return speedA.download === speedB.download return speedA.download === speedB.download
? speedA.upload - speedB.upload ? speedA.upload - speedB.upload
: speedA.download - speedB.download : speedA.download - speedB.download
}, },
cell: cell => formatSpeed(cell.value[0], cell.value[1]), cell: (cell: { value: [number, number] }) => formatSpeed(cell.value[0], cell.value[1]),
}, },
), ),
table.createDataColumn(Columns.Upload, { minWidth: 100, width: 100, header: t(`columns.${Columns.Upload}`), cell: cell => formatTraffic(cell.value) }), table.createDataColumn(Columns.Upload, { minWidth: 100, width: 100, header: t(`columns.${Columns.Upload}`), cell: cell => formatTraffic(cell.value as number) }),
table.createDataColumn(Columns.Download, { minWidth: 100, width: 100, header: t(`columns.${Columns.Download}`), cell: cell => formatTraffic(cell.value) }), table.createDataColumn(Columns.Download, { minWidth: 100, width: 100, header: t(`columns.${Columns.Download}`), cell: cell => formatTraffic(cell.value as number) }),
table.createDataColumn(Columns.SourceIP, { minWidth: 140, width: 140, header: t(`columns.${Columns.SourceIP}`), filterType: 'equals' }), table.createDataColumn(Columns.SourceIP, { minWidth: 140, width: 140, header: t(`columns.${Columns.SourceIP}`), filterType: 'equals' }),
table.createDataColumn( table.createDataColumn(
Columns.Time, Columns.Time,
{ {
minWidth: minWidth: 120,
120,
width: 120, width: 120,
header: t(`columns.${Columns.Time}`), header: t(`columns.${Columns.Time}`),
cell: cell => fromNow(new Date(cell.value), lang), cell: cell => fromNow(new Date(cell.value as string), lang),
sortType: (rowA, rowB) => rowB.original.time - rowA.original.time, sortType: (rowA, rowB) => (rowB.original as FormatConnection).time - (rowA.original as FormatConnection).time,
}), },
),
]), ]),
[lang, t], [lang, t],
) )
@ -158,7 +157,7 @@ export default function Connections () {
} }
}, [connStreamReader, feed, setTraffic]) }, [connStreamReader, feed, setTraffic])
const instance = table.useTable({ const instance = useTable(table, {
data, data,
columns, columns,
sortRowsFn, sortRowsFn,
@ -191,7 +190,7 @@ export default function Connections () {
setDrawerState(d => { d.connection.completed = true }) setDrawerState(d => { d.connection.completed = true })
client.closeConnection(drawerState.selectedID) client.closeConnection(drawerState.selectedID)
} }
const latestConntion = useLatest(drawerState.connection) const latestConntion = useSyncedRef(drawerState.connection)
useEffect(() => { useEffect(() => {
const conn = data.find(c => c.id === drawerState.selectedID)?.original const conn = data.find(c => c.id === drawerState.selectedID)?.original
if (conn) { if (conn) {
@ -206,9 +205,9 @@ export default function Connections () {
} }
}, [data, drawerState.selectedID, latestConntion, setDrawerState]) }, [data, drawerState.selectedID, latestConntion, setDrawerState])
const scrolled = useMemo(() => scrollX > 0, [scrollX]) const scrolled = useMemo(() => (intersection?.intersectionRatio ?? 0) < 1, [intersection])
const headers = headerGroup.headers.map((header, idx) => { const headers = headerGroup.headers.map((header, idx) => {
const column = header.column // as unknown as TableColumn<FormatConnection> const column = header.column
const id = column.id const id = column.id
return ( return (
<th <th
@ -223,6 +222,7 @@ export default function Connections () {
props.style.width = header.getWidth() props.style.width = header.getWidth()
}), }),
)} )}
ref={column.id === Columns.Host ? pinRef : undefined}
key={id}> key={id}>
<div {...column.getToggleSortingProps()}> <div {...column.getToggleSortingProps()}>
{header.renderHeader()} {header.renderHeader()}
@ -233,13 +233,13 @@ export default function Connections () {
} }
</div> </div>
{ idx !== headerGroup.headers.length - 1 && { idx !== headerGroup.headers.length - 1 &&
<div {...column.getResizerProps()} className="connections-resizer" /> <div {...header.getResizerProps()} className="connections-resizer" />
} }
</th> </th>
) )
}) })
const content = instance.getRows().map(row => { const content = instance.getRowModel().rows.map(row => {
return ( return (
<tr <tr
{...row.getRowProps()} {...row.getRowProps()}
@ -253,7 +253,7 @@ export default function Connections () {
{ 'text-center': shouldCenter.has(cell.column.id), completed: row.original?.completed }, { 'text-center': shouldCenter.has(cell.column.id), completed: row.original?.completed },
{ {
fixed: cell.column.id === Columns.Host, fixed: cell.column.id === Columns.Host,
shadow: scrollX > 0 && cell.column.id === Columns.Host, shadow: scrolled && cell.column.id === Columns.Host,
}, },
) )
return ( return (
@ -286,7 +286,7 @@ export default function Connections () {
</Header> </Header>
{ devices.length > 1 && <Devices devices={devices} selected={device} onChange={handleDeviceSelected} /> } { devices.length > 1 && <Devices devices={devices} selected={device} onChange={handleDeviceSelected} /> }
<Card ref={cardRef} className="connections-card relative"> <Card ref={cardRef} className="connections-card relative">
<div className="overflow-auto min-h-full" ref={tableRef}> <div className="overflow-auto min-h-full">
<table {...instance.getTableProps()} className="flex-1"> <table {...instance.getTableProps()} className="flex-1">
<thead> <thead>
<tr {...headerGroup.getHeaderGroupProps()} className="connections-header"> <tr {...headerGroup.getHeaderGroupProps()} className="connections-header">

View File

@ -28,7 +28,7 @@
&.fixed { &.fixed {
position: sticky !important; position: sticky !important;
left: 0; left: -0.1px;
z-index: 99; z-index: 99;
&.shadow { &.shadow {
box-shadow: inset -9px 0 8px -14px $color-black; box-shadow: inset -9px 0 8px -14px $color-black;

View File

@ -1,4 +1,4 @@
import { Suspense } from 'react' import { Suspense, StrictMode } from 'react'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import { HashRouter } from 'react-router-dom' import { HashRouter } from 'react-router-dom'
@ -9,11 +9,13 @@ import 'virtual:windi.css'
export default function renderApp () { export default function renderApp () {
const rootEl = document.getElementById('root') const rootEl = document.getElementById('root')
const AppInstance = ( const AppInstance = (
<HashRouter> <StrictMode>
<Suspense fallback={<Loading visible />}> <HashRouter>
<App /> <Suspense fallback={<Loading visible />}>
</Suspense> <App />
</HashRouter> </Suspense>
</HashRouter>
</StrictMode>
) )
const root = createRoot(rootEl!) const root = createRoot(rootEl!)

View File

@ -1,6 +1,6 @@
import { atom, useAtom, useAtomValue } from 'jotai' import { atom, useAtom, useAtomValue } from 'jotai'
import { atomWithStorage } from 'jotai/utils' import { atomWithStorage } from 'jotai/utils'
import { useLocation } from 'react-use' import { useLocation } from 'react-router-dom'
import { isClashX, jsBridge } from '@lib/jsBridge' import { isClashX, jsBridge } from '@lib/jsBridge'
import { Client } from '@lib/request' import { Client } from '@lib/request'

View File

@ -1,5 +1,5 @@
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite' import { defineConfig, splitVendorChunkPlugin } from 'vite'
import { VitePWA } from 'vite-plugin-pwa' import { VitePWA } from 'vite-plugin-pwa'
import windiCSS from 'vite-plugin-windicss' import windiCSS from 'vite-plugin-windicss'
import tsConfigPath from 'vite-tsconfig-paths' import tsConfigPath from 'vite-tsconfig-paths'
@ -24,6 +24,7 @@ export default defineConfig(
name: 'Clash Dashboard', name: 'Clash Dashboard',
}, },
}), }),
splitVendorChunkPlugin(),
], ],
base: './', base: './',
css: { css: {