Skip to content

Commit 7e3aa09

Browse files
authoredSep 25, 2024
⚡️ perf: refactor pwa implement to have better performance (lobehub#4124)
* test remove serwist * 尝试优化 pwa install 实现 * Update next.config.mjs * fix lint * delay the service worker register * only enabled on prod * when isShowPWAGuide update, trigger guide too
1 parent bfb7675 commit 7e3aa09

File tree

13 files changed

+135
-77
lines changed

13 files changed

+135
-77
lines changed
 

‎.gitignore

+4
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,9 @@ bun.lockb
6161
sitemap*.xml
6262
robots.txt
6363

64+
# Serwist
65+
public/sw*
66+
public/swe-worker*
67+
6468
*.patch
6569
*.pdf

‎next.config.mjs

+5-7
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import nextPWA from '@ducanh2912/next-pwa';
21
import analyzer from '@next/bundle-analyzer';
32
import { withSentryConfig } from '@sentry/nextjs';
3+
import withSerwistInit from '@serwist/next';
44

55
const isProd = process.env.NODE_ENV === 'production';
66
const buildWithDocker = process.env.DOCKER === 'true';
@@ -192,12 +192,10 @@ const noWrapper = (config) => config;
192192
const withBundleAnalyzer = process.env.ANALYZE === 'true' ? analyzer() : noWrapper;
193193

194194
const withPWA = isProd
195-
? nextPWA({
196-
dest: 'public',
197-
register: true,
198-
workboxOptions: {
199-
skipWaiting: true,
200-
},
195+
? withSerwistInit({
196+
register: false,
197+
swDest: 'public/sw.js',
198+
swSrc: 'src/app/sw.ts',
201199
})
202200
: noWrapper;
203201

‎package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@
127127
"@next/third-parties": "^14.2.6",
128128
"@react-spring/web": "^9.7.3",
129129
"@sentry/nextjs": "^7.119.0",
130+
"@serwist/next": "^9.0.8",
130131
"@t3-oss/env-nextjs": "^0.11.0",
131132
"@tanstack/react-query": "^5.52.1",
132133
"@trpc/client": "next",
@@ -232,7 +233,6 @@
232233
},
233234
"devDependencies": {
234235
"@commitlint/cli": "^19.4.0",
235-
"@ducanh2912/next-pwa": "^10.2.8",
236236
"@edge-runtime/vm": "^4.0.2",
237237
"@lobehub/i18n-cli": "^1.19.1",
238238
"@lobehub/lint": "^1.24.4",
@@ -288,6 +288,7 @@
288288
"remark-cli": "^11.0.0",
289289
"remark-parse": "^10.0.2",
290290
"semantic-release": "^21.1.2",
291+
"serwist": "^9.0.8",
291292
"stylelint": "^15.11.0",
292293
"supports-color": "8",
293294
"tsx": "^4.17.0",

‎src/app/(main)/chat/(workspace)/@conversation/features/ChatInput/Desktop/TextArea.test.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ describe('<InputArea />', () => {
150150
const beforeUnloadHandler = vi.fn();
151151

152152
addEventListenerSpy.mockImplementation((event, handler) => {
153+
// @ts-ignore
153154
if (event === 'beforeunload') {
154155
beforeUnloadHandler.mockImplementation(handler as any);
155156
}

‎src/app/sw.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { defaultCache } from '@serwist/next/worker';
2+
import type { PrecacheEntry, SerwistGlobalConfig } from 'serwist';
3+
import { Serwist } from 'serwist';
4+
5+
// This declares the value of `injectionPoint` to TypeScript.
6+
// `injectionPoint` is the string that will be replaced by the
7+
// actual precache manifest. By default, this string is set to
8+
// `"self.__SW_MANIFEST"`.
9+
declare global {
10+
interface WorkerGlobalScope extends SerwistGlobalConfig {
11+
__SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
12+
}
13+
}
14+
15+
// eslint-disable-next-line no-undef
16+
declare const self: ServiceWorkerGlobalScope;
17+
18+
const serwist = new Serwist({
19+
clientsClaim: true,
20+
navigationPreload: true,
21+
precacheEntries: self.__SW_MANIFEST,
22+
runtimeCaching: defaultCache,
23+
skipWaiting: true,
24+
});
25+
26+
serwist.addEventListeners();

‎src/features/FileViewer/Renderer/Image/index.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const ImageRenderer: DocRenderer = ({ mainState: { currentDocument } }) => {
66

77
return (
88
<Center height={'100%'} width={'100%'}>
9+
{/* eslint-disable-next-line @next/next/no-img-element */}
910
<img
1011
alt={fileName}
1112
height={'100%'}

‎src/features/PWAInstall/Install.tsx

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
'use client';
2+
3+
import dynamic from 'next/dynamic';
4+
import { memo, useEffect, useLayoutEffect } from 'react';
5+
import { useTranslation } from 'react-i18next';
6+
7+
import { BRANDING_NAME } from '@/const/branding';
8+
import { PWA_INSTALL_ID } from '@/const/layoutTokens';
9+
import { usePWAInstall } from '@/hooks/usePWAInstall';
10+
import { useGlobalStore } from '@/store/global';
11+
import { systemStatusSelectors } from '@/store/global/selectors';
12+
import { useUserStore } from '@/store/user';
13+
14+
// @ts-ignore
15+
const PWA: any = dynamic(() => import('@khmyznikov/pwa-install/dist/pwa-install.react.js'), {
16+
ssr: false,
17+
});
18+
19+
const PWAInstall = memo(() => {
20+
const { t } = useTranslation('metadata');
21+
22+
const { install, canInstall } = usePWAInstall();
23+
24+
const isShowPWAGuide = useUserStore((s) => s.isShowPWAGuide);
25+
const [hidePWAInstaller, updateSystemStatus] = useGlobalStore((s) => [
26+
systemStatusSelectors.hidePWAInstaller(s),
27+
s.updateSystemStatus,
28+
]);
29+
30+
// we need to make the pwa installer hidden by default
31+
useLayoutEffect(() => {
32+
sessionStorage.setItem('pwa-hide-install', 'true');
33+
}, []);
34+
35+
const pwaInstall =
36+
// eslint-disable-next-line unicorn/prefer-query-selector
37+
typeof window === 'undefined' ? undefined : document.getElementById(PWA_INSTALL_ID);
38+
39+
// add an event listener to control the user close installer action
40+
useEffect(() => {
41+
if (!pwaInstall) return;
42+
43+
const handler = (e: Event) => {
44+
const event = e as CustomEvent;
45+
46+
// it means user hide installer
47+
if (event.detail.message === 'dismissed') {
48+
updateSystemStatus({ hidePWAInstaller: true });
49+
}
50+
};
51+
52+
pwaInstall.addEventListener('pwa-user-choice-result-event', handler);
53+
return () => {
54+
pwaInstall.removeEventListener('pwa-user-choice-result-event', handler);
55+
};
56+
}, [pwaInstall]);
57+
58+
// trigger the PWA guide on demand
59+
useEffect(() => {
60+
if (!canInstall || hidePWAInstaller) return;
61+
62+
// trigger the pwa installer and register the service worker
63+
if (isShowPWAGuide) {
64+
install();
65+
if ('serviceWorker' in navigator && window.serwist !== undefined) {
66+
window.serwist.register();
67+
}
68+
}
69+
}, [canInstall, hidePWAInstaller, isShowPWAGuide]);
70+
71+
return (
72+
<PWA
73+
description={t('chat.description', { appName: BRANDING_NAME })}
74+
id={PWA_INSTALL_ID}
75+
manifest-url={'/manifest.webmanifest'}
76+
/>
77+
);
78+
});
79+
80+
export default PWAInstall;

‎src/features/PWAInstall/index.tsx

+6-61
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,24 @@
11
'use client';
22

33
import dynamic from 'next/dynamic';
4-
import { memo, useEffect, useLayoutEffect } from 'react';
5-
import { useTranslation } from 'react-i18next';
4+
import { memo } from 'react';
65

7-
import { BRANDING_NAME } from '@/const/branding';
8-
import { PWA_INSTALL_ID } from '@/const/layoutTokens';
9-
import { usePWAInstall } from '@/hooks/usePWAInstall';
106
import { usePlatform } from '@/hooks/usePlatform';
11-
import { useGlobalStore } from '@/store/global';
12-
import { systemStatusSelectors } from '@/store/global/selectors';
137
import { useUserStore } from '@/store/user';
148

15-
// @ts-ignore
16-
const PWA: any = dynamic(() => import('@khmyznikov/pwa-install/dist/pwa-install.react.js'), {
9+
const Install: any = dynamic(() => import('./Install'), {
1710
ssr: false,
1811
});
1912

2013
const PWAInstall = memo(() => {
21-
const { t } = useTranslation('metadata');
2214
const { isPWA } = usePlatform();
23-
24-
const { install, canInstall } = usePWAInstall();
25-
2615
const isShowPWAGuide = useUserStore((s) => s.isShowPWAGuide);
27-
const [hidePWAInstaller, updateSystemStatus] = useGlobalStore((s) => [
28-
systemStatusSelectors.hidePWAInstaller(s),
29-
s.updateSystemStatus,
30-
]);
31-
32-
// we need to make the pwa installer hidden by default
33-
useLayoutEffect(() => {
34-
sessionStorage.setItem('pwa-hide-install', 'true');
35-
}, []);
36-
37-
const pwaInstall =
38-
// eslint-disable-next-line unicorn/prefer-query-selector
39-
typeof window === 'undefined' ? undefined : document.getElementById(PWA_INSTALL_ID);
40-
41-
// add an event listener to control the user close installer action
42-
useEffect(() => {
43-
if (!pwaInstall) return;
44-
45-
const handler = (e: Event) => {
46-
const event = e as CustomEvent;
47-
48-
// it means user hide installer
49-
if (event.detail.message === 'dismissed') {
50-
updateSystemStatus({ hidePWAInstaller: true });
51-
}
52-
};
53-
54-
pwaInstall.addEventListener('pwa-user-choice-result-event', handler);
55-
return () => {
56-
pwaInstall.removeEventListener('pwa-user-choice-result-event', handler);
57-
};
58-
}, [pwaInstall]);
59-
60-
// trigger the PWA guide on demand
61-
useEffect(() => {
62-
if (!canInstall || hidePWAInstaller) return;
6316

64-
if (isShowPWAGuide) {
65-
install();
66-
}
67-
}, [canInstall, hidePWAInstaller, isShowPWAGuide]);
17+
if (isPWA || !isShowPWAGuide) return null;
6818

69-
if (isPWA) return null;
70-
return (
71-
<PWA
72-
description={t('chat.description', { appName: BRANDING_NAME })}
73-
id={PWA_INSTALL_ID}
74-
manifest-url={'/manifest.webmanifest'}
75-
/>
76-
);
19+
// only when the user is suitable for the pwa install and not install the pwa
20+
// then show the installation guide
21+
return <Install />;
7722
});
7823

7924
export default PWAInstall;

‎src/server/services/discover/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ export class DiscoverService {
212212
// Providers
213213

214214
// eslint-disable-next-line @typescript-eslint/no-unused-vars
215-
getProviderList = async (locale: Locales): Promise<DiscoverProviderItem[]> => {
215+
getProviderList = async (_locale: Locales): Promise<DiscoverProviderItem[]> => {
216216
const list = DEFAULT_MODEL_PROVIDER_LIST.filter((item) => item.chatModels.length > 0);
217217
return list.map((item) => {
218218
const provider = {

‎src/services/message/server.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export class ServerService implements IMessageService {
7979
return lambdaClient.message.updatePluginState.mutate({ id, value });
8080
}
8181

82-
bindMessagesToTopic(topicId: string, messageIds: string[]): Promise<any> {
82+
bindMessagesToTopic(_topicId: string, _messageIds: string[]): Promise<any> {
8383
throw new Error('Method not implemented.');
8484
}
8585

‎src/services/session/server.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export class ServerService implements ISessionService {
9191
return lambdaClient.session.updateSessionChatConfig.mutate({ id, value }, { signal });
9292
}
9393

94-
getSessionsByType(type: 'agent' | 'group' | 'all' = 'all'): Promise<LobeSessions> {
94+
getSessionsByType(_type: 'agent' | 'group' | 'all' = 'all'): Promise<LobeSessions> {
9595
// TODO: need be fixed
9696
// @ts-ignore
9797
return lambdaClient.session.getSessions.query({});
@@ -121,7 +121,7 @@ export class ServerService implements ISessionService {
121121
return lambdaClient.sessionGroup.getSessionGroup.query();
122122
}
123123

124-
batchCreateSessionGroups(groups: SessionGroups): Promise<BatchTaskResult> {
124+
batchCreateSessionGroups(_groups: SessionGroups): Promise<BatchTaskResult> {
125125
return Promise.resolve({ added: 0, ids: [], skips: [], success: true });
126126
}
127127

‎src/store/chat/slices/plugin/action.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,9 @@ export const chatPlugin: StateCreator<
141141

142142
try {
143143
content = JSON.parse(data);
144-
} catch {}
144+
} catch {
145+
/* empty block */
146+
}
145147

146148
if (!content) return;
147149

‎tsconfig.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "https://json.schemastore.org/tsconfig",
33
"compilerOptions": {
44
"target": "ESNext",
5-
"lib": ["dom", "dom.iterable", "esnext"],
5+
"lib": ["dom", "dom.iterable", "esnext", "webworker"],
66
"allowJs": true,
77
"skipLibCheck": true,
88
"strict": true,
@@ -16,7 +16,7 @@
1616
"jsx": "preserve",
1717
"incremental": true,
1818
"baseUrl": ".",
19-
"types": ["vitest/globals"],
19+
"types": ["vitest/globals", "@serwist/next/typings"],
2020
"paths": {
2121
"@/*": ["./src/*"],
2222
"~test-utils": ["./tests/utils.tsx"]
@@ -27,7 +27,7 @@
2727
}
2828
]
2929
},
30-
"exclude": ["node_modules"],
30+
"exclude": ["node_modules", "public/sw.js"],
3131
"include": [
3232
"next-env.d.ts",
3333
"vitest.config.ts",

0 commit comments

Comments
 (0)
Please sign in to comment.