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:
Alex Gleason
2026-03-27 23:44:20 -05:00
parent 34c40980e3
commit 12d578ff57
6 changed files with 64 additions and 37 deletions
+18 -14
View File
@@ -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 ? (
+14 -7
View File
@@ -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 ? (
+2 -2
View File
@@ -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()
+2 -3
View File
@@ -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);
+18 -11
View File
@@ -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} />
+10
View File
@@ -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'],