Improve bundle chunking: lazy-load emoji picker, markdown, and remove runtime tailwind config
- Hardcode md breakpoint in useIsMobile and toaster to eliminate runtime import of tailwind.config (was pulling in tailwindcss, postcss-selector-parser, and plugin code ~100KB) - Lazy-load EmojiPicker in ComposeBox (emoji-mart + data ~500KB deferred) - Dynamic import webxdcMeta.ts (smol-toml + fflate only loaded for .xdc uploads) - Lazy-load ArticleContent, PullRequestCard, CustomNipCard in NoteCard and PostDetailPage (react-markdown + unified pipeline ~147KB deferred) - Consolidate 60+ lucide-react icon micro-chunks into a single chunk via manualChunks, reducing HTTP request overhead - ReplyComposeModal chunk: 808KB -> 296KB (-63%) - JS file count: 226 -> 181 (-45 files)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useState, useRef, useCallback, useMemo, useEffect } from 'react';
|
||||
import { lazy, Suspense, useState, useRef, useCallback, useMemo, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Paperclip, Smile, AlertTriangle, X, Loader2, Mic, Square, Sticker, BarChart3, Plus, ChevronLeft } from 'lucide-react';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
@@ -14,7 +14,6 @@ import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { Dialog, DialogContent } from '@/components/ui/dialog';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||
import { EmojiPicker } from '@/components/EmojiPicker';
|
||||
import { useCustomEmojis } from '@/hooks/useCustomEmojis';
|
||||
import { useFeedSettings } from '@/hooks/useFeedSettings';
|
||||
import { GifPicker } from '@/components/GifPicker';
|
||||
@@ -34,8 +33,10 @@ import { useToast } from '@/hooks/useToast';
|
||||
import { useAppContext } from '@/hooks/useAppContext';
|
||||
import type { EventStats } from '@/hooks/useTrending';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { extractWebxdcMeta } from '@/lib/webxdcMeta';
|
||||
import { extractVideoUrls, extractAudioUrls, IMETA_MEDIA_URL_REGEX, mimeFromExt } from '@/lib/mediaUrls';
|
||||
|
||||
/** Lazy-loaded EmojiPicker — keeps emoji-mart + its data out of the main bundle. */
|
||||
const LazyEmojiPicker = lazy(() => import('@/components/EmojiPicker').then(m => ({ default: m.EmojiPicker })));
|
||||
import { parseImetaMap } from '@/lib/imeta';
|
||||
import { useProfileUrl } from '@/hooks/useProfileUrl';
|
||||
import { useInsertText } from '@/hooks/useInsertText';
|
||||
@@ -528,6 +529,7 @@ export function ComposeBox({
|
||||
|
||||
// Extract name and icon from the .xdc archive
|
||||
try {
|
||||
const { extractWebxdcMeta } = await import('@/lib/webxdcMeta');
|
||||
const meta = await extractWebxdcMeta(file);
|
||||
const metaEntry: { name?: string; iconUrl?: string } = { name: meta.name };
|
||||
|
||||
@@ -1425,17 +1427,19 @@ export function ComposeBox({
|
||||
)}
|
||||
</div>
|
||||
{/* Picker content */}
|
||||
{pickerTab === 'emoji' ? (
|
||||
<EmojiPicker
|
||||
customEmojis={customEmojis}
|
||||
onSelect={(selection) => {
|
||||
if (selection.type === 'native') {
|
||||
insertEmoji(selection.emoji);
|
||||
} else {
|
||||
insertEmoji(`:${selection.shortcode}:`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{pickerTab === 'emoji' ? (
|
||||
<Suspense fallback={<div className="w-[316px] h-[435px] flex items-center justify-center"><Loader2 className="size-6 animate-spin text-muted-foreground" /></div>}>
|
||||
<LazyEmojiPicker
|
||||
customEmojis={customEmojis}
|
||||
onSelect={(selection) => {
|
||||
if (selection.type === 'native') {
|
||||
insertEmoji(selection.emoji);
|
||||
} else {
|
||||
insertEmoji(`:${selection.shortcode}:`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
) : pickerTab === 'stickers' ? (
|
||||
<div className="w-[316px] h-[435px]">
|
||||
{customEmojis.length === 0 ? (
|
||||
|
||||
@@ -19,9 +19,10 @@ import {
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { lazy, memo, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { ArticleContent } from "@/components/ArticleContent";
|
||||
/** Lazy-loaded markdown-heavy components — keeps react-markdown + unified pipeline out of the main feed bundle. */
|
||||
const ArticleContent = lazy(() => import("@/components/ArticleContent").then(m => ({ default: m.ArticleContent })));
|
||||
import {
|
||||
MusicPlaylistContent,
|
||||
MusicTrackContent,
|
||||
@@ -37,7 +38,7 @@ import {
|
||||
import { CommentContext } from "@/components/CommentContext";
|
||||
import { ContentWarningGuard } from "@/components/ContentWarningGuard";
|
||||
import { EmojifiedText, ReactionEmoji } from "@/components/CustomEmoji";
|
||||
import { CustomNipCard } from "@/components/CustomNipCard";
|
||||
const CustomNipCard = lazy(() => import("@/components/CustomNipCard").then(m => ({ default: m.CustomNipCard })));
|
||||
import { EmojiPackContent } from "@/components/EmojiPackContent";
|
||||
import { FileMetadataContent } from "@/components/FileMetadataContent";
|
||||
import { FollowPackContent } from "@/components/FollowPackContent";
|
||||
@@ -59,7 +60,7 @@ import { PatchCard } from "@/components/PatchCard";
|
||||
import { PollContent } from "@/components/PollContent";
|
||||
import { ProfileBadgesContent } from "@/components/ProfileBadgesContent";
|
||||
import { ProfileHoverCard } from "@/components/ProfileHoverCard";
|
||||
import { PullRequestCard } from "@/components/PullRequestCard";
|
||||
const PullRequestCard = lazy(() => import("@/components/PullRequestCard").then(m => ({ default: m.PullRequestCard })));
|
||||
import { ReactionButton } from "@/components/ReactionButton";
|
||||
import { ReplyComposeModal } from "@/components/ReplyComposeModal";
|
||||
import { ReplyContext } from "@/components/ReplyContext";
|
||||
@@ -437,7 +438,9 @@ export const NoteCard = memo(function NoteCard({
|
||||
) : isFollowPack ? (
|
||||
<FollowPackContent event={event} />
|
||||
) : isArticle ? (
|
||||
<ArticleContent event={event} preview className="mt-2" />
|
||||
<Suspense fallback={<Skeleton className="h-24 w-full rounded-lg" />}>
|
||||
<ArticleContent event={event} preview className="mt-2" />
|
||||
</Suspense>
|
||||
) : isMagicDeck ? (
|
||||
<MagicDeckContent event={event} />
|
||||
) : isStream ? (
|
||||
@@ -469,9 +472,13 @@ export const NoteCard = memo(function NoteCard({
|
||||
) : isPatch ? (
|
||||
<PatchCard event={event} />
|
||||
) : isPullRequest ? (
|
||||
<PullRequestCard event={event} />
|
||||
<Suspense fallback={<Skeleton className="h-24 w-full rounded-lg" />}>
|
||||
<PullRequestCard event={event} />
|
||||
</Suspense>
|
||||
) : isCustomNip ? (
|
||||
<CustomNipCard event={event} />
|
||||
<Suspense fallback={<Skeleton className="h-24 w-full rounded-lg" />}>
|
||||
<CustomNipCard event={event} />
|
||||
</Suspense>
|
||||
) : isNsite ? (
|
||||
<NsiteCard event={event} />
|
||||
) : isZapstoreApp ? (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import { useToast } from "@/hooks/useToast"
|
||||
import tailwindConfig from "../../../tailwind.config"
|
||||
import {
|
||||
Toast,
|
||||
ToastClose,
|
||||
@@ -11,7 +10,8 @@ import {
|
||||
ToastViewport,
|
||||
} from "@/components/ui/toast"
|
||||
|
||||
const MD_BREAKPOINT = parseFloat(tailwindConfig.theme.screens.md);
|
||||
/** Matches the `md` breakpoint in tailwind.config.ts (768px). Hardcoded to avoid pulling the entire Tailwind config + plugins into the client bundle. */
|
||||
const MD_BREAKPOINT = 768;
|
||||
|
||||
export function Toaster() {
|
||||
const { toasts } = useToast()
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import tailwindConfig from "../../tailwind.config"
|
||||
|
||||
const MOBILE_BREAKPOINT = parseFloat(tailwindConfig.theme.screens.md);
|
||||
/** Matches the `md` breakpoint in tailwind.config.ts (768px). Hardcoded to avoid pulling the entire Tailwind config + plugins into the client bundle. */
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile(): boolean {
|
||||
const [isMobile, setIsMobile] = useState(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
|
||||
@@ -17,9 +17,10 @@ import {
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { ArticleContent } from "@/components/ArticleContent";
|
||||
/** Lazy-loaded markdown-heavy components — keeps react-markdown + unified pipeline out of the detail page bundle. */
|
||||
const ArticleContent = lazy(() => import("@/components/ArticleContent").then(m => ({ default: m.ArticleContent })));
|
||||
import { AudioVisualizer } from "@/components/AudioVisualizer";
|
||||
import { BadgeDetailContent } from "@/components/BadgeDetailContent";
|
||||
import { CalendarEventDetailPage } from "@/components/CalendarEventDetailPage";
|
||||
@@ -33,7 +34,7 @@ import {
|
||||
ReactionEmoji,
|
||||
RenderResolvedEmoji,
|
||||
} from "@/components/CustomEmoji";
|
||||
import { CustomNipCard } from "@/components/CustomNipCard";
|
||||
const CustomNipCard = lazy(() => import("@/components/CustomNipCard").then(m => ({ default: m.CustomNipCard })));
|
||||
import { FileMetadataContent } from "@/components/FileMetadataContent";
|
||||
import { FollowPackContent } from "@/components/FollowPackContent";
|
||||
import { FollowPackDetailContent } from "@/components/FollowPackDetailContent";
|
||||
@@ -55,7 +56,7 @@ import { NoteMoreMenu } from "@/components/NoteMoreMenu";
|
||||
import { PatchCard } from "@/components/PatchCard";
|
||||
import { PodcastDetailContent } from "@/components/PodcastDetailContent";
|
||||
import { PollContent } from "@/components/PollContent";
|
||||
import { PullRequestCard } from "@/components/PullRequestCard";
|
||||
const PullRequestCard = lazy(() => import("@/components/PullRequestCard").then(m => ({ default: m.PullRequestCard })));
|
||||
import { ReactionButton } from "@/components/ReactionButton";
|
||||
import { ReplyComposeModal } from "@/components/ReplyComposeModal";
|
||||
import { RepostMenu } from "@/components/RepostMenu";
|
||||
@@ -1659,7 +1660,9 @@ function PostDetailContent({ event }: { event: NostrEvent }) {
|
||||
{isVideo ? (
|
||||
<VideoDetailContent event={event} />
|
||||
) : isArticle ? (
|
||||
<ArticleContent event={event} className="mt-3" />
|
||||
<Suspense fallback={<Skeleton className="h-32 w-full rounded-lg" />}>
|
||||
<ArticleContent event={event} className="mt-3" />
|
||||
</Suspense>
|
||||
) : isMagicDeck ? (
|
||||
<MagicDeckContent event={event} />
|
||||
) : isFileMetadata ? (
|
||||
@@ -1679,13 +1682,17 @@ function PostDetailContent({ event }: { event: NostrEvent }) {
|
||||
<PatchCard event={event} preview={false} />
|
||||
</div>
|
||||
) : isPullRequest ? (
|
||||
<div className="mt-3">
|
||||
<PullRequestCard event={event} preview={false} />
|
||||
</div>
|
||||
<Suspense fallback={<Skeleton className="h-32 w-full rounded-lg" />}>
|
||||
<div className="mt-3">
|
||||
<PullRequestCard event={event} preview={false} />
|
||||
</div>
|
||||
</Suspense>
|
||||
) : isCustomNip ? (
|
||||
<div className="mt-3">
|
||||
<CustomNipCard event={event} preview={false} />
|
||||
</div>
|
||||
<Suspense fallback={<Skeleton className="h-32 w-full rounded-lg" />}>
|
||||
<div className="mt-3">
|
||||
<CustomNipCard event={event} preview={false} />
|
||||
</div>
|
||||
</Suspense>
|
||||
) : isNsite ? (
|
||||
<div className="mt-3">
|
||||
<NsiteCard event={event} />
|
||||
|
||||
@@ -155,6 +155,16 @@ export default defineConfig(() => {
|
||||
},
|
||||
build: {
|
||||
target: 'esnext',
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
// Consolidate lucide icons into a single chunk instead of 60+ micro-chunks.
|
||||
if (id.includes('node_modules/lucide-react')) {
|
||||
return 'lucide-icons';
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ['@capacitor/filesystem', '@capacitor/share'],
|
||||
|
||||
Reference in New Issue
Block a user