mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
⚡️ perf: refactor pwa implement to have better performance (#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
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -61,5 +61,9 @@ bun.lockb
|
||||
sitemap*.xml
|
||||
robots.txt
|
||||
|
||||
# Serwist
|
||||
public/sw*
|
||||
public/swe-worker*
|
||||
|
||||
*.patch
|
||||
*.pdf
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import nextPWA from '@ducanh2912/next-pwa';
|
||||
import analyzer from '@next/bundle-analyzer';
|
||||
import { withSentryConfig } from '@sentry/nextjs';
|
||||
import withSerwistInit from '@serwist/next';
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
const buildWithDocker = process.env.DOCKER === 'true';
|
||||
@@ -192,12 +192,10 @@ const noWrapper = (config) => config;
|
||||
const withBundleAnalyzer = process.env.ANALYZE === 'true' ? analyzer() : noWrapper;
|
||||
|
||||
const withPWA = isProd
|
||||
? nextPWA({
|
||||
dest: 'public',
|
||||
register: true,
|
||||
workboxOptions: {
|
||||
skipWaiting: true,
|
||||
},
|
||||
? withSerwistInit({
|
||||
register: false,
|
||||
swDest: 'public/sw.js',
|
||||
swSrc: 'src/app/sw.ts',
|
||||
})
|
||||
: noWrapper;
|
||||
|
||||
|
||||
@@ -127,6 +127,7 @@
|
||||
"@next/third-parties": "^14.2.6",
|
||||
"@react-spring/web": "^9.7.3",
|
||||
"@sentry/nextjs": "^7.119.0",
|
||||
"@serwist/next": "^9.0.8",
|
||||
"@t3-oss/env-nextjs": "^0.11.0",
|
||||
"@tanstack/react-query": "^5.52.1",
|
||||
"@trpc/client": "next",
|
||||
@@ -232,7 +233,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^19.4.0",
|
||||
"@ducanh2912/next-pwa": "^10.2.8",
|
||||
"@edge-runtime/vm": "^4.0.2",
|
||||
"@lobehub/i18n-cli": "^1.19.1",
|
||||
"@lobehub/lint": "^1.24.4",
|
||||
@@ -288,6 +288,7 @@
|
||||
"remark-cli": "^11.0.0",
|
||||
"remark-parse": "^10.0.2",
|
||||
"semantic-release": "^21.1.2",
|
||||
"serwist": "^9.0.8",
|
||||
"stylelint": "^15.11.0",
|
||||
"supports-color": "8",
|
||||
"tsx": "^4.17.0",
|
||||
|
||||
@@ -150,6 +150,7 @@ describe('<InputArea />', () => {
|
||||
const beforeUnloadHandler = vi.fn();
|
||||
|
||||
addEventListenerSpy.mockImplementation((event, handler) => {
|
||||
// @ts-ignore
|
||||
if (event === 'beforeunload') {
|
||||
beforeUnloadHandler.mockImplementation(handler as any);
|
||||
}
|
||||
|
||||
26
src/app/sw.ts
Normal file
26
src/app/sw.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { defaultCache } from '@serwist/next/worker';
|
||||
import type { PrecacheEntry, SerwistGlobalConfig } from 'serwist';
|
||||
import { Serwist } from 'serwist';
|
||||
|
||||
// This declares the value of `injectionPoint` to TypeScript.
|
||||
// `injectionPoint` is the string that will be replaced by the
|
||||
// actual precache manifest. By default, this string is set to
|
||||
// `"self.__SW_MANIFEST"`.
|
||||
declare global {
|
||||
interface WorkerGlobalScope extends SerwistGlobalConfig {
|
||||
__SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
declare const self: ServiceWorkerGlobalScope;
|
||||
|
||||
const serwist = new Serwist({
|
||||
clientsClaim: true,
|
||||
navigationPreload: true,
|
||||
precacheEntries: self.__SW_MANIFEST,
|
||||
runtimeCaching: defaultCache,
|
||||
skipWaiting: true,
|
||||
});
|
||||
|
||||
serwist.addEventListeners();
|
||||
@@ -6,6 +6,7 @@ const ImageRenderer: DocRenderer = ({ mainState: { currentDocument } }) => {
|
||||
|
||||
return (
|
||||
<Center height={'100%'} width={'100%'}>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
alt={fileName}
|
||||
height={'100%'}
|
||||
|
||||
80
src/features/PWAInstall/Install.tsx
Normal file
80
src/features/PWAInstall/Install.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { memo, useEffect, useLayoutEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { BRANDING_NAME } from '@/const/branding';
|
||||
import { PWA_INSTALL_ID } from '@/const/layoutTokens';
|
||||
import { usePWAInstall } from '@/hooks/usePWAInstall';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { systemStatusSelectors } from '@/store/global/selectors';
|
||||
import { useUserStore } from '@/store/user';
|
||||
|
||||
// @ts-ignore
|
||||
const PWA: any = dynamic(() => import('@khmyznikov/pwa-install/dist/pwa-install.react.js'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const PWAInstall = memo(() => {
|
||||
const { t } = useTranslation('metadata');
|
||||
|
||||
const { install, canInstall } = usePWAInstall();
|
||||
|
||||
const isShowPWAGuide = useUserStore((s) => s.isShowPWAGuide);
|
||||
const [hidePWAInstaller, updateSystemStatus] = useGlobalStore((s) => [
|
||||
systemStatusSelectors.hidePWAInstaller(s),
|
||||
s.updateSystemStatus,
|
||||
]);
|
||||
|
||||
// we need to make the pwa installer hidden by default
|
||||
useLayoutEffect(() => {
|
||||
sessionStorage.setItem('pwa-hide-install', 'true');
|
||||
}, []);
|
||||
|
||||
const pwaInstall =
|
||||
// eslint-disable-next-line unicorn/prefer-query-selector
|
||||
typeof window === 'undefined' ? undefined : document.getElementById(PWA_INSTALL_ID);
|
||||
|
||||
// add an event listener to control the user close installer action
|
||||
useEffect(() => {
|
||||
if (!pwaInstall) return;
|
||||
|
||||
const handler = (e: Event) => {
|
||||
const event = e as CustomEvent;
|
||||
|
||||
// it means user hide installer
|
||||
if (event.detail.message === 'dismissed') {
|
||||
updateSystemStatus({ hidePWAInstaller: true });
|
||||
}
|
||||
};
|
||||
|
||||
pwaInstall.addEventListener('pwa-user-choice-result-event', handler);
|
||||
return () => {
|
||||
pwaInstall.removeEventListener('pwa-user-choice-result-event', handler);
|
||||
};
|
||||
}, [pwaInstall]);
|
||||
|
||||
// trigger the PWA guide on demand
|
||||
useEffect(() => {
|
||||
if (!canInstall || hidePWAInstaller) return;
|
||||
|
||||
// trigger the pwa installer and register the service worker
|
||||
if (isShowPWAGuide) {
|
||||
install();
|
||||
if ('serviceWorker' in navigator && window.serwist !== undefined) {
|
||||
window.serwist.register();
|
||||
}
|
||||
}
|
||||
}, [canInstall, hidePWAInstaller, isShowPWAGuide]);
|
||||
|
||||
return (
|
||||
<PWA
|
||||
description={t('chat.description', { appName: BRANDING_NAME })}
|
||||
id={PWA_INSTALL_ID}
|
||||
manifest-url={'/manifest.webmanifest'}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default PWAInstall;
|
||||
@@ -1,79 +1,24 @@
|
||||
'use client';
|
||||
|
||||
import dynamic from 'next/dynamic';
|
||||
import { memo, useEffect, useLayoutEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { memo } from 'react';
|
||||
|
||||
import { BRANDING_NAME } from '@/const/branding';
|
||||
import { PWA_INSTALL_ID } from '@/const/layoutTokens';
|
||||
import { usePWAInstall } from '@/hooks/usePWAInstall';
|
||||
import { usePlatform } from '@/hooks/usePlatform';
|
||||
import { useGlobalStore } from '@/store/global';
|
||||
import { systemStatusSelectors } from '@/store/global/selectors';
|
||||
import { useUserStore } from '@/store/user';
|
||||
|
||||
// @ts-ignore
|
||||
const PWA: any = dynamic(() => import('@khmyznikov/pwa-install/dist/pwa-install.react.js'), {
|
||||
const Install: any = dynamic(() => import('./Install'), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const PWAInstall = memo(() => {
|
||||
const { t } = useTranslation('metadata');
|
||||
const { isPWA } = usePlatform();
|
||||
|
||||
const { install, canInstall } = usePWAInstall();
|
||||
|
||||
const isShowPWAGuide = useUserStore((s) => s.isShowPWAGuide);
|
||||
const [hidePWAInstaller, updateSystemStatus] = useGlobalStore((s) => [
|
||||
systemStatusSelectors.hidePWAInstaller(s),
|
||||
s.updateSystemStatus,
|
||||
]);
|
||||
|
||||
// we need to make the pwa installer hidden by default
|
||||
useLayoutEffect(() => {
|
||||
sessionStorage.setItem('pwa-hide-install', 'true');
|
||||
}, []);
|
||||
if (isPWA || !isShowPWAGuide) return null;
|
||||
|
||||
const pwaInstall =
|
||||
// eslint-disable-next-line unicorn/prefer-query-selector
|
||||
typeof window === 'undefined' ? undefined : document.getElementById(PWA_INSTALL_ID);
|
||||
|
||||
// add an event listener to control the user close installer action
|
||||
useEffect(() => {
|
||||
if (!pwaInstall) return;
|
||||
|
||||
const handler = (e: Event) => {
|
||||
const event = e as CustomEvent;
|
||||
|
||||
// it means user hide installer
|
||||
if (event.detail.message === 'dismissed') {
|
||||
updateSystemStatus({ hidePWAInstaller: true });
|
||||
}
|
||||
};
|
||||
|
||||
pwaInstall.addEventListener('pwa-user-choice-result-event', handler);
|
||||
return () => {
|
||||
pwaInstall.removeEventListener('pwa-user-choice-result-event', handler);
|
||||
};
|
||||
}, [pwaInstall]);
|
||||
|
||||
// trigger the PWA guide on demand
|
||||
useEffect(() => {
|
||||
if (!canInstall || hidePWAInstaller) return;
|
||||
|
||||
if (isShowPWAGuide) {
|
||||
install();
|
||||
}
|
||||
}, [canInstall, hidePWAInstaller, isShowPWAGuide]);
|
||||
|
||||
if (isPWA) return null;
|
||||
return (
|
||||
<PWA
|
||||
description={t('chat.description', { appName: BRANDING_NAME })}
|
||||
id={PWA_INSTALL_ID}
|
||||
manifest-url={'/manifest.webmanifest'}
|
||||
/>
|
||||
);
|
||||
// only when the user is suitable for the pwa install and not install the pwa
|
||||
// then show the installation guide
|
||||
return <Install />;
|
||||
});
|
||||
|
||||
export default PWAInstall;
|
||||
|
||||
@@ -212,7 +212,7 @@ export class DiscoverService {
|
||||
// Providers
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
getProviderList = async (locale: Locales): Promise<DiscoverProviderItem[]> => {
|
||||
getProviderList = async (_locale: Locales): Promise<DiscoverProviderItem[]> => {
|
||||
const list = DEFAULT_MODEL_PROVIDER_LIST.filter((item) => item.chatModels.length > 0);
|
||||
return list.map((item) => {
|
||||
const provider = {
|
||||
|
||||
@@ -79,7 +79,7 @@ export class ServerService implements IMessageService {
|
||||
return lambdaClient.message.updatePluginState.mutate({ id, value });
|
||||
}
|
||||
|
||||
bindMessagesToTopic(topicId: string, messageIds: string[]): Promise<any> {
|
||||
bindMessagesToTopic(_topicId: string, _messageIds: string[]): Promise<any> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ export class ServerService implements ISessionService {
|
||||
return lambdaClient.session.updateSessionChatConfig.mutate({ id, value }, { signal });
|
||||
}
|
||||
|
||||
getSessionsByType(type: 'agent' | 'group' | 'all' = 'all'): Promise<LobeSessions> {
|
||||
getSessionsByType(_type: 'agent' | 'group' | 'all' = 'all'): Promise<LobeSessions> {
|
||||
// TODO: need be fixed
|
||||
// @ts-ignore
|
||||
return lambdaClient.session.getSessions.query({});
|
||||
@@ -121,7 +121,7 @@ export class ServerService implements ISessionService {
|
||||
return lambdaClient.sessionGroup.getSessionGroup.query();
|
||||
}
|
||||
|
||||
batchCreateSessionGroups(groups: SessionGroups): Promise<BatchTaskResult> {
|
||||
batchCreateSessionGroups(_groups: SessionGroups): Promise<BatchTaskResult> {
|
||||
return Promise.resolve({ added: 0, ids: [], skips: [], success: true });
|
||||
}
|
||||
|
||||
|
||||
@@ -141,7 +141,9 @@ export const chatPlugin: StateCreator<
|
||||
|
||||
try {
|
||||
content = JSON.parse(data);
|
||||
} catch {}
|
||||
} catch {
|
||||
/* empty block */
|
||||
}
|
||||
|
||||
if (!content) return;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": ["dom", "dom.iterable", "esnext", "webworker"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -16,7 +16,7 @@
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"baseUrl": ".",
|
||||
"types": ["vitest/globals"],
|
||||
"types": ["vitest/globals", "@serwist/next/typings"],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"~test-utils": ["./tests/utils.tsx"]
|
||||
@@ -27,7 +27,7 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"exclude": ["node_modules", "public/sw.js"],
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"vitest.config.ts",
|
||||
|
||||
Reference in New Issue
Block a user