Skip to content

Commit

Permalink
⚡️ perf: refactor pwa implement to have better performance (lobehub#4124
Browse files Browse the repository at this point in the history
)

* 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
  • Loading branch information
arvinxx authored Sep 25, 2024
1 parent bfb7675 commit 7e3aa09
Show file tree
Hide file tree
Showing 13 changed files with 135 additions and 77 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,9 @@ bun.lockb
sitemap*.xml
robots.txt

# Serwist
public/sw*
public/swe-worker*

*.patch
*.pdf
12 changes: 5 additions & 7 deletions next.config.mjs
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ describe('<InputArea />', () => {
const beforeUnloadHandler = vi.fn();

addEventListenerSpy.mockImplementation((event, handler) => {
// @ts-ignore
if (event === 'beforeunload') {
beforeUnloadHandler.mockImplementation(handler as any);
}
Expand Down
26 changes: 26 additions & 0 deletions src/app/sw.ts
Original file line number Diff line number Diff line change
@@ -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();
1 change: 1 addition & 0 deletions src/features/FileViewer/Renderer/Image/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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%'}
Expand Down
80 changes: 80 additions & 0 deletions src/features/PWAInstall/Install.tsx
Original file line number Diff line number Diff line change
@@ -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;
67 changes: 6 additions & 61 deletions src/features/PWAInstall/index.tsx
Original file line number Diff line number Diff line change
@@ -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');
}, []);

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 || !isShowPWAGuide) return null;

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;
2 changes: 1 addition & 1 deletion src/server/services/discover/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
2 changes: 1 addition & 1 deletion src/services/message/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.');
}

Expand Down
4 changes: 2 additions & 2 deletions src/services/session/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({});
Expand Down Expand Up @@ -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 });
}

Expand Down
4 changes: 3 additions & 1 deletion src/store/chat/slices/plugin/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,9 @@ export const chatPlugin: StateCreator<

try {
content = JSON.parse(data);
} catch {}
} catch {
/* empty block */
}

if (!content) return;

Expand Down
6 changes: 3 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"]
Expand All @@ -27,7 +27,7 @@
}
]
},
"exclude": ["node_modules"],
"exclude": ["node_modules", "public/sw.js"],
"include": [
"next-env.d.ts",
"vitest.config.ts",
Expand Down

0 comments on commit 7e3aa09

Please sign in to comment.