diff --git a/client/web/package.json b/client/web/package.json index 96b366db..9385306d 100644 --- a/client/web/package.json +++ b/client/web/package.json @@ -64,6 +64,7 @@ "tailchat-shared": "*", "tailwindcss": "^2.2.4", "url": "^0.11.0", + "web-vitals": "^3.1.0", "yup": "^0.32.9", "zustand": "^4.1.2" }, diff --git a/client/web/src/App.tsx b/client/web/src/App.tsx index 8db41972..e71ca2c3 100644 --- a/client/web/src/App.tsx +++ b/client/web/src/App.tsx @@ -77,7 +77,7 @@ const AppHeader: React.FC = React.memo(() => { AppHeader.displayName = 'AppHeader'; export const App: React.FC = React.memo(() => { - useRecordMeasure('AppRenderStart'); + useRecordMeasure('appRenderStart'); return ( diff --git a/client/web/src/components/modals/SettingsView/Performance.tsx b/client/web/src/components/modals/SettingsView/Performance.tsx index cb481e70..bf188a7e 100644 --- a/client/web/src/components/modals/SettingsView/Performance.tsx +++ b/client/web/src/components/modals/SettingsView/Performance.tsx @@ -2,8 +2,9 @@ import { measure } from '@/utils/measure-helper'; import React, { useMemo } from 'react'; export const SettingsPerformance: React.FC = React.memo(() => { - const { record, timeUsage } = useMemo( + const { vitals, record, timeUsage } = useMemo( () => ({ + vitals: measure.getVitals(), record: measure.getRecord(), timeUsage: measure.getTimeUsage(), }), @@ -12,22 +13,34 @@ export const SettingsPerformance: React.FC = React.memo(() => { return (
+
+
Vitals:
+
+ {Object.entries(vitals).map(([n, t]) => ( +
+ {n}: {t}ms +
+ ))} +
+
+
Record:
{Object.entries(record).map(([n, t]) => (
- {n}: {t} + {n}: {t}ms
))}
+
TimeUsage:
{Object.entries(timeUsage).map(([n, t]) => (
- {n}: {t} + {n}: {t}ms
))}
diff --git a/client/web/src/index.tsx b/client/web/src/index.tsx index 4bfb8745..da966f86 100644 --- a/client/web/src/index.tsx +++ b/client/web/src/index.tsx @@ -6,13 +6,16 @@ import { App } from './App'; import { initPlugins } from './plugin/loader'; import { installServiceWorker } from './utils/sw-helper'; import { showErrorToasts, t } from 'tailchat-shared'; +import { recordMeasure } from './utils/measure-helper'; import './styles'; installServiceWorker(); // 先加载插件再开启应用 +recordMeasure('initPluginsStart'); initPlugins() .then(() => { + recordMeasure('initPluginsEnd'); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const root = createRoot(document.querySelector('#app')!); root.render( diff --git a/client/web/src/init.tsx b/client/web/src/init.tsx index 44179646..87891064 100644 --- a/client/web/src/init.tsx +++ b/client/web/src/init.tsx @@ -18,6 +18,9 @@ import { import { getPopupContainer } from './utils/dom-helper'; import { getUserJWT } from './utils/jwt-helper'; import _get from 'lodash/get'; +import { recordMeasure } from './utils/measure-helper'; + +recordMeasure('init'); if (isDevelopment) { import('source-ref-runtime').then(({ start }) => start()); diff --git a/client/web/src/routes/Entry/index.tsx b/client/web/src/routes/Entry/index.tsx index 049f4e39..5e4980f9 100644 --- a/client/web/src/routes/Entry/index.tsx +++ b/client/web/src/routes/Entry/index.tsx @@ -10,7 +10,7 @@ import { GuestView } from './GuestView'; import { ForgetPasswordView } from './ForgetPasswordView'; const EntryRoute = React.memo(() => { - useRecordMeasure('AppEntryRenderStart'); + useRecordMeasure('appEntryRenderStart'); return (
diff --git a/client/web/src/routes/Invite/index.tsx b/client/web/src/routes/Invite/index.tsx index ce423cbe..935ef7a1 100644 --- a/client/web/src/routes/Invite/index.tsx +++ b/client/web/src/routes/Invite/index.tsx @@ -9,7 +9,7 @@ import { useRecordMeasure } from '@/utils/measure-helper'; */ const InviteRoute: React.FC = React.memo(() => { const { inviteCode = '' } = useParams<{ inviteCode: string }>(); - useRecordMeasure('AppInviteRenderStart'); + useRecordMeasure('appInviteRenderStart'); return ( diff --git a/client/web/src/routes/Main/index.tsx b/client/web/src/routes/Main/index.tsx index e8ce35fc..e0f5c1f7 100644 --- a/client/web/src/routes/Main/index.tsx +++ b/client/web/src/routes/Main/index.tsx @@ -7,7 +7,7 @@ import { MainProvider } from './Provider'; import { useShortcuts } from './useShortcuts'; const MainRoute: React.FC = React.memo(() => { - useRecordMeasure('AppMainRenderStart'); + useRecordMeasure('appMainRenderStart'); useShortcuts(); return ( diff --git a/client/web/src/routes/Panel/index.tsx b/client/web/src/routes/Panel/index.tsx index 4f7a967d..1012f032 100644 --- a/client/web/src/routes/Panel/index.tsx +++ b/client/web/src/routes/Panel/index.tsx @@ -21,7 +21,7 @@ const GroupDetailRoute = React.memo(() => { GroupDetailRoute.displayName = 'GroupDetailRoute'; const PanelRoute: React.FC = React.memo(() => { - useRecordMeasure('AppRouteRenderStart'); + useRecordMeasure('appRouteRenderStart'); return (
diff --git a/client/web/src/utils/measure-helper.ts b/client/web/src/utils/measure-helper.ts index 22c6bea4..472af1f1 100644 --- a/client/web/src/utils/measure-helper.ts +++ b/client/web/src/utils/measure-helper.ts @@ -1,7 +1,22 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { useLayoutEffect } from 'react'; +import { useEffect, useLayoutEffect } from 'react'; +import { Metric, onCLS, onFCP, onFID, onINP, onLCP, onTTFB } from 'web-vitals'; const records: Record = {}; +const vitals: Record = {}; + +const handleVitalsCb = (metric: Metric) => { + if (!vitals[metric.name]) { + vitals[metric.name] = metric.value; + } +}; + +onCLS(handleVitalsCb); +onFCP(handleVitalsCb); +onFID(handleVitalsCb); +onINP(handleVitalsCb); +onLCP(handleVitalsCb); +onTTFB(handleVitalsCb); /** * 记录测量点 @@ -9,6 +24,8 @@ const records: Record = {}; */ export function recordMeasure(name: string) { if (!records[name]) { + // 首次进入 + performance.mark(`tailchat:${name}`); records[name] = performance.now(); } } @@ -21,9 +38,14 @@ export function useRecordMeasure(name: string) { useLayoutEffect(() => { recordMeasure(name); }, []); + + useEffect(() => { + recordMeasure(name + 'Mounted'); + }, []); } export const measure = { + getVitals: () => ({ ...vitals }), getRecord: () => ({ ...records }), getTimeUsage() { let t = performance.timing; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b17188a..242fc544 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -421,6 +421,7 @@ importers: typescript: ^4.5.2 url: ^0.11.0 url-loader: ^4.1.1 + web-vitals: ^3.1.0 webpack: 5.73.0 webpack-bundle-analyzer: ^4.5.0 webpack-cli: ^4.9.1 @@ -471,6 +472,7 @@ importers: tailchat-shared: link:../shared tailwindcss: 2.2.19_ywsstkkounrjlah5ti55snp2aq url: 0.11.0 + web-vitals: 3.1.0 yup: 0.32.11 zustand: 4.1.2_react@18.2.0 devDependencies: @@ -11760,7 +11762,7 @@ packages: babel-plugin-syntax-jsx: 6.18.0 lodash: 4.17.21 picomatch: 2.3.1 - styled-components: 5.3.6_react@18.2.0 + styled-components: 5.3.6_7i5myeigehqah43i5u7wbekgba /babel-plugin-syntax-jsx/6.18.0: resolution: {integrity: sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==} @@ -19626,7 +19628,7 @@ packages: pretty-format: 27.5.1 slash: 3.0.0 strip-json-comments: 3.1.1 - ts-node: 10.9.1_k2dsl7zculo2nmh5s33pladmoa + ts-node: 10.9.1_bqee57coj3oib6dw4m24wknwqe transitivePeerDependencies: - bufferutil - canvas @@ -29104,7 +29106,6 @@ packages: react-is: 18.2.0 shallowequal: 1.1.0 supports-color: 5.5.0 - dev: false /styled-components/5.3.6_mdz3marskokvq6744hhidi3r5a: resolution: {integrity: sha512-hGTZquGAaTqhGWldX7hhfzjnIYBZ0IXQXkCYdvF1Sq3DsUaLx6+NTHC5Jj1ooM2F68sBiVz3lvhfwQs/S3l6qg==} @@ -29150,6 +29151,7 @@ packages: react: 18.2.0 shallowequal: 1.1.0 supports-color: 5.5.0 + dev: true /styled-system/5.1.5: resolution: {integrity: sha512-7VoD0o2R3RKzOzPK0jYrVnS8iJdfkKsQJNiLRDjikOpQVqQHns/DXWaPZOH4tIKkhAT7I6wIsy9FWTWh2X3q+A==} @@ -31540,6 +31542,10 @@ packages: /web-namespaces/1.1.4: resolution: {integrity: sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==} + /web-vitals/3.1.0: + resolution: {integrity: sha512-zCeQ+bOjWjJbXv5ZL0r8Py3XP2doCQMZXNKlBGfUjPAVZWokApdeF/kFlK1peuKlCt8sL9TFkKzyXE9/cmNJQA==} + dev: false + /webidl-conversions/3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}