From 7b622d797d595ee6c3d4be05f11361557ce6d299 Mon Sep 17 00:00:00 2001 From: fly_mc Date: Thu, 21 Nov 2024 00:47:34 +0800 Subject: [PATCH 01/24] frontend: add same router --- packages/frontend/src/router/main.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/frontend/src/router/main.ts b/packages/frontend/src/router/main.ts index 3c25e80d1..8307df115 100644 --- a/packages/frontend/src/router/main.ts +++ b/packages/frontend/src/router/main.ts @@ -25,6 +25,10 @@ export function setupRouter(app: App, routerFactory: ((path: string) => IRouter) window.history.pushState({ key: ctx.key }, '', ctx.path); }); + mainRouter.addListener('same', () => { + window.scroll({ top: 0, behavior: 'smooth' }); + }); + mainRouter.addListener('replace', ctx => { window.history.replaceState({ key: ctx.key }, '', ctx.path); }); From f0c3a4cc0b47b392dd155ebabea0d5587df2753d Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 21 Nov 2024 07:58:34 +0900 Subject: [PATCH 02/24] perf(frontend): reduce api requests for non-logged-in enviroment (#15001) * wip * Update CHANGELOG.md * wip --- CHANGELOG.md | 1 + .../src/server/web/ClientServerService.ts | 14 ++++++++++- .../backend/src/server/web/views/base.pug | 3 +++ packages/frontend/src/pages/clip.vue | 14 ++++++++--- packages/frontend/src/pages/note.vue | 11 ++++++++- packages/frontend/src/pages/user/index.vue | 18 +++++++++++++-- packages/frontend/src/server-context.ts | 23 +++++++++++++++++++ 7 files changed, 77 insertions(+), 7 deletions(-) create mode 100644 packages/frontend/src/server-context.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index befe237b0..58b9c4d0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ (Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/663) - Enhance: サイドバーを簡単に展開・折りたたみできるように ( #14981 ) - Enhance: リノートメニューに「リノートの詳細」を追加 +- Enhance: 非ログイン状態でMisskeyを開いた際のパフォーマンスを向上 - Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正 - Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正 (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768) diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 5ebec4ffd..1b8873214 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -559,7 +559,7 @@ export class ClientServerService { } }); - //#region SSR (for crawlers) + //#region SSR // User fastify.get<{ Params: { user: string; sub?: string; } }>('/@:user/:sub?', async (request, reply) => { const { username, host } = Acct.parse(request.params.user); @@ -584,11 +584,17 @@ export class ClientServerService { reply.header('X-Robots-Tag', 'noimageai'); reply.header('X-Robots-Tag', 'noai'); } + + const _user = await this.userEntityService.pack(user); + return await reply.view('user', { user, profile, me, avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user), sub: request.params.sub, ...await this.generateCommonPugData(this.meta), + clientCtx: htmlSafeJsonStringify({ + user: _user, + }), }); } else { // リモートユーザーなので @@ -641,6 +647,9 @@ export class ClientServerService { // TODO: Let locale changeable by instance setting summary: getNoteSummary(_note), ...await this.generateCommonPugData(this.meta), + clientCtx: htmlSafeJsonStringify({ + note: _note, + }), }); } else { return await renderBase(reply); @@ -729,6 +738,9 @@ export class ClientServerService { profile, avatarUrl: _clip.user.avatarUrl, ...await this.generateCommonPugData(this.meta), + clientCtx: htmlSafeJsonStringify({ + clip: _clip, + }), }); } else { return await renderBase(reply); diff --git a/packages/backend/src/server/web/views/base.pug b/packages/backend/src/server/web/views/base.pug index 280a5923c..3883b5e5a 100644 --- a/packages/backend/src/server/web/views/base.pug +++ b/packages/backend/src/server/web/views/base.pug @@ -74,6 +74,9 @@ html script(type='application/json' id='misskey_meta' data-generated-at=now) != metaJson + script(type='application/json' id='misskey_clientCtx' data-generated-at=now) + != clientCtx + script include ../boot.js diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index 7b1737fec..891d59d60 100644 --- a/packages/frontend/src/pages/clip.vue +++ b/packages/frontend/src/pages/clip.vue @@ -33,25 +33,28 @@ SPDX-License-Identifier: AGPL-3.0-only `; + return `${messages.title}
${messages.header}
v${_VERSION_}
`; } -globalThis.addEventListener('fetch', ev => { - const shouldCache = PATHS_TO_CACHE.some(path => ev.request.url.includes(path)); +globalThis.addEventListener('fetch', (ev) => { + const shouldCache = PATHS_TO_CACHE.some(path => ev.request.url.includes(path)); - if (shouldCache) { - ev.respondWith( - caches.match(ev.request) - .then(async response => { - if (response) return response; - - const fetchResponse = await fetch(ev.request); - if (!fetchResponse || fetchResponse.status !== 200 || fetchResponse.type !== 'basic') { - return fetchResponse; - } - - const blob = await fetchResponse.clone().blob(); - await manageStorageSpace(blob.size); - - const responseToCache = fetchResponse.clone(); - const cache = await caches.open(STATIC_CACHE_NAME); - await cache.put(ev.request, responseToCache); - - return fetchResponse; - }) - ); - return; - } - - let isHTMLRequest = false; - if (ev.request.headers.get('sec-fetch-dest') === 'document') { - isHTMLRequest = true; - } else if (ev.request.headers.get('accept')?.includes('/html')) { - isHTMLRequest = true; - } else if (ev.request.url.endsWith('/')) { - isHTMLRequest = true; - } - - if (!isHTMLRequest) return; + if (shouldCache) { ev.respondWith( - fetch(ev.request) - .catch(async () => { - const html = await offlineContentHTML(); - return new Response(html, { - status: 200, - headers: { - 'content-type': 'text/html', - }, - }); - }), + caches.match(ev.request) + .then(async (response) => { + if (response) return response; + + const fetchResponse = await fetch(ev.request); + if (!fetchResponse || fetchResponse.status !== 200 || fetchResponse.type !== 'basic') { + return fetchResponse; + } + + const blob = await fetchResponse.clone().blob(); + await manageStorageSpace(blob.size); + + const responseToCache = fetchResponse.clone(); + const cache = await caches.open(STATIC_CACHE_NAME); + await cache.put(ev.request, responseToCache); + + return fetchResponse; + }), ); + return; + } + + let isHTMLRequest = false; + if (ev.request.headers.get('sec-fetch-dest') === 'document') { + isHTMLRequest = true; + } else if (ev.request.headers.get('accept')?.includes('/html')) { + isHTMLRequest = true; + } else if (ev.request.url.endsWith('index.html')) { + isHTMLRequest = true; + } + + if (isHTMLRequest) { + ev.respondWith( + caches.match(ev.request) + .then(async (response) => { + if (response) return response; + const offlineHTML = await offlineContentHTML(); + return new Response(offlineHTML, { + headers: { 'Content-Type': 'text/html' }, + }); + }), + ); + } }); -globalThis.addEventListener('push', ev => { - ev.waitUntil(globalThis.clients.matchAll({ - includeUncontrolled: true, - type: 'window', +globalThis.addEventListener('push', (ev) => { + ev.waitUntil( + globalThis.clients.matchAll({ + includeUncontrolled: true, + type: 'window', }).then(async () => { - const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.data?.json(); + const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.data?.json(); - switch (data.type) { - case 'notification': - case 'unreadAntennaNote': - if (Date.now() - data.dateTime > 1000 * 60 * 60 * 24) break; + switch (data.type) { + case 'notification': + case 'unreadAntennaNote': + if (Date.now() - data.dateTime > 1000 * 60 * 60 * 24) break; - return createNotification(data); - case 'readAllNotifications': - await globalThis.registration.getNotifications() - .then(notifications => notifications.forEach(n => n.tag !== 'read_notification' && n.close())); - break; - } + return createNotification(data); - await createEmptyNotification(); - return; - })); + case 'readAllNotifications': + await globalThis.registration.getNotifications() + .then(notifications => { + notifications.forEach(n => { + if (n.tag !== 'read_notification') n.close(); + }); + }); + break; + } + + await createEmptyNotification(); + }), + ); }); globalThis.addEventListener('notificationclick', (ev: ServiceWorkerGlobalScopeEventMap['notificationclick']) => { - ev.waitUntil((async (): Promise => { - if (_DEV_) { - console.log('notificationclick', ev.action, ev.notification.data); - } + ev.waitUntil((async (): Promise => { + if (_DEV_) { + console.log('notificationclick', ev.action, ev.notification.data); + } - const { action, notification } = ev; - const data: PushNotificationDataMap[keyof PushNotificationDataMap] = notification.data ?? {}; - const { userId: loginId } = data; - let client: WindowClient | null = null; + const { action, notification } = ev; + const data: PushNotificationDataMap[keyof PushNotificationDataMap] = notification.data ?? {}; + const { userId: loginId } = data; + let client: WindowClient | null = null; - switch (data.type) { - case 'notification': - switch (action) { - case 'follow': - if ('userId' in data.body) await swos.api('following/create', loginId, { userId: data.body.userId }); - break; - case 'showUser': - if ('user' in data.body) client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId); - break; - case 'reply': - if ('note' in data.body) client = await swos.openPost({ reply: data.body.note }, loginId); - break; - case 'renote': - if ('note' in data.body) await swos.api('notes/create', loginId, { renoteId: data.body.note.id }); - break; - case 'accept': - switch (data.body.type) { - case 'receiveFollowRequest': - await swos.api('following/requests/accept', loginId, { userId: data.body.userId }); - break; - } - break; - case 'reject': - switch (data.body.type) { - case 'receiveFollowRequest': - await swos.api('following/requests/reject', loginId, { userId: data.body.userId }); - break; - } - break; - case 'showFollowRequests': - client = await swos.openClient('push', '/my/follow-requests', loginId); - break; - default: - switch (data.body.type) { - case 'receiveFollowRequest': - client = await swos.openClient('push', '/my/follow-requests', loginId); - break; - case 'reaction': - client = await swos.openNote(data.body.note.id, loginId); - break; - default: - if ('note' in data.body) { - client = await swos.openNote(data.body.note.id, loginId); - } else if ('user' in data.body) { - client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId); - } - break; - } - } - break; - case 'unreadAntennaNote': - client = await swos.openAntenna(data.body.antenna.id, loginId); - break; - default: - switch (action) { - case 'markAllAsRead': - await globalThis.registration.getNotifications() - .then(notifications => notifications.forEach(n => n.tag !== 'read_notification' && n.close())); - await get[]>('accounts').then(accounts => { - return Promise.all((accounts ?? []).map(async account => { - await swos.sendMarkAllAsRead(account.id); - })); - }); - break; - case 'settings': - client = await swos.openClient('push', '/settings/notifications', loginId); - break; - } + switch (data.type) { + case 'notification': + switch (action) { + case 'follow': + if ('userId' in data.body) { + await swos.api('following/create', loginId, { userId: data.body.userId }); + } + break; + case 'showUser': + if ('user' in data.body) { + client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId); + } + break; + case 'reply': + if ('note' in data.body) { + client = await swos.openPost({ reply: data.body.note }, loginId); + } + break; + case 'renote': + if ('note' in data.body) { + await swos.api('notes/create', loginId, { renoteId: data.body.note.id }); + } + break; + case 'accept': + if (data.body.type === 'receiveFollowRequest') { + await swos.api('following/requests/accept', loginId, { userId: data.body.userId }); + } + break; + case 'reject': + if (data.body.type === 'receiveFollowRequest') { + await swos.api('following/requests/reject', loginId, { userId: data.body.userId }); + } + break; + case 'showFollowRequests': + client = await swos.openClient('push', '/my/follow-requests', loginId); + break; + default: + if (data.body.type === 'receiveFollowRequest') { + client = await swos.openClient('push', '/my/follow-requests', loginId); + } else if (data.body.type === 'reaction') { + client = await swos.openNote(data.body.note.id, loginId); + } else if ('note' in data.body) { + client = await swos.openNote(data.body.note.id, loginId); + } else if ('user' in data.body) { + client = await swos.openUser(Misskey.acct.toString(data.body.user), loginId); + } + break; } + break; - if (client) { - client.focus(); - } - if (data.type === 'notification') { - await swos.sendMarkAllAsRead(loginId); - } + case 'unreadAntennaNote': + client = await swos.openAntenna(data.body.antenna.id, loginId); + break; - notification.close(); - })()); + default: + switch (action) { + case 'markAllAsRead': + await globalThis.registration.getNotifications() + .then(notifications => notifications.forEach(n => { + if (n.tag !== 'read_notification') n.close(); + })); + + await get[]>('accounts') + .then(accounts => { + return Promise.all((accounts ?? []).map(async account => { + await swos.sendMarkAllAsRead(account.id); + })); + }); + break; + + case 'settings': + client = await swos.openClient('push', '/settings/notifications', loginId); + break; + } + } + + if (client) { + client.focus(); + } + + if (data.type === 'notification') { + await swos.sendMarkAllAsRead(loginId); + } + + notification.close(); + })()); }); globalThis.addEventListener('notificationclose', (ev: ServiceWorkerGlobalScopeEventMap['notificationclose']) => { - const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.notification.data; + const data: PushNotificationDataMap[keyof PushNotificationDataMap] = ev.notification.data; - ev.waitUntil((async (): Promise => { - if (data.type === 'notification') { - await swos.sendMarkAllAsRead(data.userId); - } - return; - })()); + ev.waitUntil((async (): Promise => { + if (data.type === 'notification') { + await swos.sendMarkAllAsRead(data.userId); + } + })()); }); globalThis.addEventListener('message', (ev: ServiceWorkerGlobalScopeEventMap['message']) => { - ev.waitUntil((async (): Promise => { - if (ev.data === 'clear') { - await caches.keys() - .then(cacheNames => Promise.all( - cacheNames.map(name => caches.delete(name)), - )); - return; - } + ev.waitUntil((async (): Promise => { + if (ev.data === 'clear') { + await caches.keys() + .then(cacheNames => Promise.all( + cacheNames.map(name => caches.delete(name)), + )); + return; + } - if (typeof ev.data === 'object') { - const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase(); + if (typeof ev.data === 'object') { + const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase(); - if (otype === 'object') { - if (ev.data.msg === 'initialize') { - swLang.setLang(ev.data.lang); - } - } + if (otype === 'object') { + if (ev.data.msg === 'initialize') { + swLang.setLang(ev.data.lang); } - })()); + } + } + })()); });