From 4bdee0362d4749ced52fd09d635cda4c5b5d36e6 Mon Sep 17 00:00:00 2001 From: plebeius Date: Thu, 26 Feb 2026 16:42:36 +0800 Subject: [PATCH] perf(bundle): replace plebbit-js imports with local utility, split chunks, fix CLS and rerenders Eliminate direct @plebbit/plebbit-js (8.7MB) imports from all UI components by replacing Plebbit.getShortAddress() with a local get-short-address utility. Add Vite manual chunks for plebbit-js, plebbit-react-hooks, and react-spring/use-gesture. Fix catalog CLS by matching skeleton to wrapper dimensions. Lazy-load ChallengeModal and ReplyModal. Add failed-URL cache to useFetchGifFirstFrame, optimize useHide selector, and memoize useFeed options in mod-queue. --- src/app.tsx | 30 ++++++++++-------- src/components/board-header/board-header.tsx | 4 +-- src/components/boardsbar/boardsbar.tsx | 4 +-- .../catalog-row/catalog-row.module.css | 4 +-- src/components/catalog-row/catalog-row.tsx | 8 ++--- .../comment-content/comment-content.tsx | 4 +-- src/components/post-desktop/post-desktop.tsx | 8 ++--- src/components/post-form/post-form.tsx | 4 +-- src/components/post-mobile/post-mobile.tsx | 6 ++-- .../subscriptions-setting.tsx | 4 +-- src/hooks/use-fetch-gif-first-frame.ts | 6 ++++ src/hooks/use-hide.ts | 8 ++--- src/lib/get-short-address.ts | 8 +++++ src/lib/react-scan.ts | 13 ++++---- src/views/mod-queue/mod-queue.tsx | 31 ++++++++++++------- vite.config.js | 9 ++++++ 16 files changed, 92 insertions(+), 59 deletions(-) create mode 100644 src/lib/get-short-address.ts diff --git a/src/app.tsx b/src/app.tsx index 0f26b4da..ae3dc652 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -27,9 +27,7 @@ import PendingPost from './views/pending-post'; import Post from './views/post'; import Rules from './views/rules'; import BoardHeader from './components/board-header'; -import ChallengeModal from './components/challenge-modal'; import FeedCacheContainer from './components/feed-cache-container'; -import ReplyModal from './components/reply-modal'; import PostForm from './components/post-form'; import BoardBlotter from './components/board-blotter'; import BoardsBar from './components/boardsbar'; @@ -37,8 +35,10 @@ import BoardsBar from './components/boardsbar'; const AccountDataEditor = lazy(() => import('./views/account-data-editor')); const BoardsBarEditModal = lazy(() => import('./components/boardsbar-edit-modal')); const CreateBoardModal = lazy(() => import('./components/create-board-modal')); +const ChallengeModal = lazy(() => import('./components/challenge-modal')); const DirectoryModal = lazy(() => import('./components/directory-modal')); const DisclaimerModal = lazy(() => import('./components/disclaimer-modal')); +const ReplyModal = lazy(() => import('./components/reply-modal')); const SettingsModal = lazy(() => import('./components/settings-modal')); // Preload all theme assets (buttons, backgrounds) immediately on app load @@ -134,18 +134,22 @@ const GlobalLayout = () => { return ( <> - + + + {activeCid && threadCid && subplebbitAddress && ( - + + + )} {isInSettingsView && ( diff --git a/src/components/board-header/board-header.tsx b/src/components/board-header/board-header.tsx index c60fd2c5..d6b24c7a 100644 --- a/src/components/board-header/board-header.tsx +++ b/src/components/board-header/board-header.tsx @@ -4,7 +4,7 @@ import { useLocation, useParams, useNavigate } from 'react-router-dom'; import { useAccountComment } from '@plebbit/plebbit-react-hooks'; import useAccountsStore from '@plebbit/plebbit-react-hooks/dist/stores/accounts'; import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits'; -import Plebbit from '@plebbit/plebbit-js'; +import getShortAddress from '../../lib/get-short-address'; import { useStableSubplebbit } from '../../hooks/use-stable-subplebbit'; import { isAllView, isSubscriptionsView, isModView } from '../../lib/utils/view-utils'; import styles from './board-header.module.css'; @@ -95,7 +95,7 @@ const BoardHeader = () => { ? shortAddress.endsWith('.eth') || shortAddress.endsWith('.sol') ? shortAddress.slice(0, -4) : shortAddress - : subplebbitAddress && Plebbit.getShortAddress({ address: subplebbitAddress }))} + : subplebbitAddress && getShortAddress(subplebbitAddress))} {!isInAllView && !isInSubscriptionsView && !isInModView && }
diff --git a/src/components/boardsbar/boardsbar.tsx b/src/components/boardsbar/boardsbar.tsx index b711eea9..e3d89235 100644 --- a/src/components/boardsbar/boardsbar.tsx +++ b/src/components/boardsbar/boardsbar.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import Plebbit from '@plebbit/plebbit-js'; +import getShortAddress from '../../lib/get-short-address'; import { useAccountComment } from '@plebbit/plebbit-react-hooks'; import useAccountsStore from '@plebbit/plebbit-react-hooks/dist/stores/accounts'; import { isAllView, isCatalogView, isSubscriptionsView } from '../../lib/utils/view-utils'; @@ -200,7 +200,7 @@ const BoardsBarDesktop = () => { // Render a subscription link const renderSubscription = (address: string, index: number, total: number) => { const boardPath = getBoardPath(address, directories); - const displayText = address.endsWith('.eth') || address.endsWith('.sol') ? address : Plebbit.getShortAddress({ address }); + const displayText = address.endsWith('.eth') || address.endsWith('.sol') ? address : getShortAddress(address); return ( diff --git a/src/components/catalog-row/catalog-row.module.css b/src/components/catalog-row/catalog-row.module.css index 597af362..68cced01 100644 --- a/src/components/catalog-row/catalog-row.module.css +++ b/src/components/catalog-row/catalog-row.module.css @@ -110,8 +110,8 @@ } .loadingSkeleton { - width: var(--maxWidth); - height: var(--maxHeight); + width: 100%; + height: 100%; display: inline-block; } diff --git a/src/components/catalog-row/catalog-row.tsx b/src/components/catalog-row/catalog-row.tsx index 9b0b9702..f3d6ae3e 100644 --- a/src/components/catalog-row/catalog-row.tsx +++ b/src/components/catalog-row/catalog-row.tsx @@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next'; import { Link, useLocation, useParams } from 'react-router-dom'; import { useFloating, offset, size, Placement } from '@floating-ui/react'; import { Comment, useReplies } from '@plebbit/plebbit-react-hooks'; -import Plebbit from '@plebbit/plebbit-js'; +import getShortAddress from '../../lib/get-short-address'; import { shouldShowSnow } from '../../lib/snow'; import { getHasThumbnail } from '../../lib/utils/media-utils'; import { getFormattedTimeAgo } from '../../lib/utils/time-utils'; @@ -57,8 +57,8 @@ export const CatalogPostMedia = ({ cid, commentMediaInfo, linkWidth, linkHeight } if (type === 'audio') { - displayWidth = 'unset'; - displayHeight = 'unset'; + displayWidth = `${maxThumbnailSize}px`; + displayHeight = '54px'; } const numericWidth = parseInt(displayWidth) || undefined; @@ -288,7 +288,7 @@ const CatalogPost = memo( {author?.displayName || capitalize(t('anonymous'))} {isCatalogPostAuthorMod && {` ## Board ${catalogPostAuthorRole}`}} - {(isInAllView || isInSubscriptionsView) && subplebbitAddress && ` to p/${Plebbit.getShortAddress({ address: subplebbitAddress })}`} + {(isInAllView || isInSubscriptionsView) && subplebbitAddress && ` to p/${getShortAddress(subplebbitAddress)}`} {getFormattedTimeAgo(timestamp)} {replyCount > 0 && (
diff --git a/src/components/comment-content/comment-content.tsx b/src/components/comment-content/comment-content.tsx index d0d58f8d..83e2c76d 100644 --- a/src/components/comment-content/comment-content.tsx +++ b/src/components/comment-content/comment-content.tsx @@ -4,7 +4,7 @@ import { Trans, useTranslation } from 'react-i18next'; import { Comment, useComment } from '@plebbit/plebbit-react-hooks'; import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages'; import usePostNumberStore from '../../stores/use-post-number-store'; -import Plebbit from '@plebbit/plebbit-js'; +import getShortAddress from '../../lib/get-short-address'; import { getFormattedDate, getFormattedTimeAgo } from '../../lib/utils/time-utils'; import { isPostPageView } from '../../lib/utils/view-utils'; import useIsMobile from '../../hooks/use-is-mobile'; @@ -253,7 +253,7 @@ const CommentContent = ({ comment: post }: { comment: Comment }) => {
alertThresholdSeconds; - const userID = address && Plebbit.getShortAddress({ address }); // shortened to 8 chars for display; users can verify the full user ID via "Copy user ID" in the post menu to guard against spoofing + const userID = address && getShortAddress(address); // shortened to 8 chars for display; users can verify the full user ID via "Copy user ID" in the post menu to guard against spoofing const userIDBackgroundColor = hashStringToColor(userID); const userIDTextColor = getTextColorForBackground(userIDBackgroundColor); @@ -562,7 +562,7 @@ const PostMedia = ({ ? boardPath : subplebbitAddress.endsWith('.eth') || subplebbitAddress.endsWith('.sol') ? subplebbitAddress - : Plebbit.getShortAddress({ address: subplebbitAddress }); + : getShortAddress(subplebbitAddress); return (
@@ -741,7 +741,7 @@ const PostDesktop = ({ ? boardPath : subplebbitAddress.endsWith('.eth') || subplebbitAddress.endsWith('.sol') ? subplebbitAddress - : Plebbit.getShortAddress({ address: subplebbitAddress }) + : getShortAddress(subplebbitAddress) : undefined; const { hidden, unhide, hide } = useHide({ cid }); diff --git a/src/components/post-form/post-form.tsx b/src/components/post-form/post-form.tsx index a75ae7de..bf68bcf7 100644 --- a/src/components/post-form/post-form.tsx +++ b/src/components/post-form/post-form.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { Comment, setAccount, useAccount, useAccountComment, useAccountSubplebbits, useEditedComment } from '@plebbit/plebbit-react-hooks'; -import Plebbit from '@plebbit/plebbit-js'; +import getShortAddress from '../../lib/get-short-address'; import useSubplebbitsStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits'; import useSubplebbitsPagesStore from '@plebbit/plebbit-react-hooks/dist/stores/subplebbits-pages'; import { getHasThumbnail, getLinkMediaInfo } from '../../lib/utils/media-utils'; @@ -280,7 +280,7 @@ const PostFormFields = ({ {isInModView && accountSubplebbitAddresses.map((address: string) => ( ))} {isInSubscriptionsView && diff --git a/src/components/post-mobile/post-mobile.tsx b/src/components/post-mobile/post-mobile.tsx index 7af42c61..5bc86d8b 100644 --- a/src/components/post-mobile/post-mobile.tsx +++ b/src/components/post-mobile/post-mobile.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'; import { Link, useLocation, useNavigationType, useParams } from 'react-router-dom'; import { Virtuoso, VirtuosoHandle, StateSnapshot } from 'react-virtuoso'; import { Comment, useEditedComment, useReplies, useAccount, usePublishCommentModeration, useAccountComment } from '@plebbit/plebbit-react-hooks'; -import Plebbit from '@plebbit/plebbit-js'; +import getShortAddress from '../../lib/get-short-address'; import styles from '../../views/post/post.module.css'; import { shouldShowSnow } from '../../lib/snow'; import { getHasThumbnail } from '../../lib/utils/media-utils'; @@ -66,7 +66,7 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos ? boardPath : subplebbitAddress.endsWith('.eth') || subplebbitAddress.endsWith('.sol') ? subplebbitAddress - : Plebbit.getShortAddress({ address: subplebbitAddress }) + : getShortAddress(subplebbitAddress) : undefined; const isReply = parentCid; const title = post?.title?.trim(); @@ -203,7 +203,7 @@ const PostInfoAndMedia = ({ post, postReplyCount = 0, roles, threadNumber }: Pos return Math.max(domCount, 1); })(); - const userID = address && Plebbit.getShortAddress({ address }); // shortened to 8 chars for display; users can verify the full user ID via "Copy user ID" in the post menu to guard against spoofing + const userID = address && getShortAddress(address); // shortened to 8 chars for display; users can verify the full user ID via "Copy user ID" in the post menu to guard against spoofing const userIDBackgroundColor = hashStringToColor(userID); const userIDTextColor = getTextColorForBackground(userIDBackgroundColor); diff --git a/src/components/settings-modal/subscriptions-setting/subscriptions-setting.tsx b/src/components/settings-modal/subscriptions-setting/subscriptions-setting.tsx index 869f9922..3334eca8 100644 --- a/src/components/settings-modal/subscriptions-setting/subscriptions-setting.tsx +++ b/src/components/settings-modal/subscriptions-setting/subscriptions-setting.tsx @@ -1,5 +1,5 @@ import { setAccount, useAccount, useSubscribe } from '@plebbit/plebbit-react-hooks'; -import Plebbit from '@plebbit/plebbit-js'; +import getShortAddress from '../../../lib/get-short-address'; import styles from './subscriptions-setting.module.css'; import { useTranslation } from 'react-i18next'; import { useState } from 'react'; @@ -79,7 +79,7 @@ const SubscriptionsSetting = () => {
    {subscriptions?.map((address: string) => (
  • - {address && Plebbit.getShortAddress({ address })} + {address && getShortAddress(address)}
  • ))}
diff --git a/src/hooks/use-fetch-gif-first-frame.ts b/src/hooks/use-fetch-gif-first-frame.ts index 9a8fdd92..ded99cea 100644 --- a/src/hooks/use-fetch-gif-first-frame.ts +++ b/src/hooks/use-fetch-gif-first-frame.ts @@ -2,6 +2,7 @@ import { useEffect, useState } from 'react'; import localForageLru from '@plebbit/plebbit-react-hooks/dist/lib/localforage-lru/index.js'; const gifFrameDb = localForageLru.createInstance({ name: '5chanGifFrames', size: 500 }); +const failedUrls = new Set(); const getCachedGifFrame = async (url: string): Promise => { return await gifFrameDb.getItem(url); @@ -72,6 +73,10 @@ const useFetchGifFirstFrame = (url: string | undefined) => { let isActive = true; const fetchFrame = async () => { + if (failedUrls.has(url)) { + if (isActive) setFrameUrl(null); + return; + } try { const cachedFrame = await getCachedGifFrame(url); if (cachedFrame) { @@ -91,6 +96,7 @@ const useFetchGifFirstFrame = (url: string | undefined) => { await setCachedGifFrame(url, objectUrl); } } catch (error) { + failedUrls.add(url); console.error('Failed to load GIF frame:', error); if (isActive) setFrameUrl(null); } diff --git a/src/hooks/use-hide.ts b/src/hooks/use-hide.ts index d63eca6f..0443592d 100644 --- a/src/hooks/use-hide.ts +++ b/src/hooks/use-hide.ts @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { useCallback, useMemo } from 'react'; import { create } from 'zustand'; import localForageLru from '@plebbit/plebbit-react-hooks/dist/lib/localforage-lru/index.js'; @@ -46,16 +46,14 @@ const initializeHideStore = async () => { initializeHideStore(); const useHide = ({ cid }: { cid: string }) => { - const hiddenCids = useHideStore((state) => state.hiddenCids); + const hidden = useHideStore((state) => !!state.hiddenCids[cid]); const hide = useHideStore((state) => state.hide); const unhide = useHideStore((state) => state.unhide); - const hidden = !!hiddenCids[cid]; - const hideCallback = useCallback(() => hide(cid), [hide, cid]); const unhideCallback = useCallback(() => unhide(cid), [unhide, cid]); - return { hidden, hide: hideCallback, unhide: unhideCallback }; + return useMemo(() => ({ hidden, hide: hideCallback, unhide: unhideCallback }), [hidden, hideCallback, unhideCallback]); }; export default useHide; diff --git a/src/lib/get-short-address.ts b/src/lib/get-short-address.ts new file mode 100644 index 00000000..20cd3bc2 --- /dev/null +++ b/src/lib/get-short-address.ts @@ -0,0 +1,8 @@ +const getShortAddress = (address: string): string => { + if (!address) return ''; + if (address.includes('.')) return address; + if (address.length < 20) return ''; + return address.slice(8, 20); +}; + +export default getShortAddress; diff --git a/src/lib/react-scan.ts b/src/lib/react-scan.ts index c63eca17..7716137f 100644 --- a/src/lib/react-scan.ts +++ b/src/lib/react-scan.ts @@ -1,10 +1,9 @@ if (import.meta.env.DEV) { - const { scan, getReport } = await import('react-scan'); - scan({ - enabled: true, - showToolbar: !(window as any).__PROFILING__, - playSound: !(window as any).__PROFILING__, - report: true, + import('react-scan').then(({ scan, getReport }) => { + scan({ + enabled: true, + showToolbar: !(window as any).__PROFILING__, + }); + (window as any).__getReactScanReport = getReport; }); - (window as any).__getReactScanReport = getReport; } diff --git a/src/views/mod-queue/mod-queue.tsx b/src/views/mod-queue/mod-queue.tsx index a3b3faea..70ba5558 100644 --- a/src/views/mod-queue/mod-queue.tsx +++ b/src/views/mod-queue/mod-queue.tsx @@ -622,12 +622,17 @@ export const ModQueueButton = ({ boardIdentifier, isMobile }: ModQueueButtonProp // Only fetch if we have addresses to check and permissions const shouldFetch = subplebbitAddresses.length > 0 && isModOfBoard; - const { feed } = useFeed({ - subplebbitAddresses: shouldFetch ? subplebbitAddresses : [], - modQueue: ['pendingApproval'], - sortType: 'new', - postsPerPage: 200, // Fetch more items to get accurate pending count for the badge - }); + const feedAddresses = shouldFetch ? subplebbitAddresses : []; + const feedOptions = useMemo( + () => ({ + subplebbitAddresses: feedAddresses, + modQueue: ['pendingApproval'], + sortType: 'new' as const, + postsPerPage: 200, + }), + [feedAddresses], + ); + const { feed } = useFeed(feedOptions); if (!shouldFetch || subplebbitAddresses.length === 0) { return null; @@ -694,11 +699,15 @@ const ModQueueView = ({ boardIdentifier: propBoardIdentifier }: ModQueueViewProp const subplebbit = useSubplebbit({ subplebbitAddress }); const { error: subplebbitError } = subplebbit || {}; - const { feed, hasMore, loadMore, reset } = useFeed({ - subplebbitAddresses, - modQueue: ['pendingApproval'], - postsPerPage: 50, - }); + const feedOptions = useMemo( + () => ({ + subplebbitAddresses, + modQueue: ['pendingApproval'], + postsPerPage: 50, + }), + [subplebbitAddresses], + ); + const { feed, hasMore, loadMore, reset } = useFeed(feedOptions); const setResetFunction = useFeedResetStore((state) => state.setResetFunction); useEffect(() => { diff --git a/vite.config.js b/vite.config.js index 744f0a3b..6ded3fc7 100644 --- a/vite.config.js +++ b/vite.config.js @@ -169,6 +169,15 @@ export default defineConfig({ rollupOptions: { output: { manualChunks(id) { + if (/[\\/]node_modules[\\/](@plebbit[\\/]plebbit-js)[\\/]/.test(id)) { + return 'plebbit-js'; + } + if (/[\\/]node_modules[\\/](@plebbit[\\/]plebbit-react-hooks)[\\/]/.test(id)) { + return 'plebbit-react-hooks'; + } + if (/[\\/]node_modules[\\/](@react-spring|@use-gesture)[\\/]/.test(id)) { + return 'spring-gesture'; + } if (/[\\/]node_modules[\\/](react|react-dom|react-router-dom|react-i18next|i18next|i18next-browser-languagedetector|i18next-http-backend)[\\/]/.test(id)) { return 'vendor'; }