آیکون SVG در RSC: چرا یک import باندل کلاینت را کیلویی میکند و چطور صفر بایت بفرستی
نویسنده: آرش لطیفی
بسیاری از کتابخانههای آیکون با یک 'use client' پنهان کل صفحه را به کلاینت میکشند. الگوی تست grep، رندر سروری با zero-JS، و بنچمارک ۴۵KB vs صفر — با کد کپیپیستی.
خلاصه برای وقتندارها: ۶ تا آیکون اضافه میکنی، باندلت ۴۴ کیلوبایت میپره، بدون اینکه جایی
'use client'نوشته باشی. خیلی از کتابخانههای آیکون خودشون'use client'دارن و با یک import کل صفحهات رو میکشن سمت کلاینت. راه حل؟ یکgrepپنجثانیهای بزن، آیکون سروری (یا SVG خام) جایگزین کن، و با یک چک بعد از build مطمئن شو. ده دقیقه کار، کلی سرعت.
صفحهی /pricing رو با ۶ تا آیکون ساختی. بیلد سبزه، اروری نیست. بعد next build رو چک میکنی:
تو که جایی 'use client' ننوشتی! پس چی شد؟
برو DevTools → Sources → پوشهی _next/static/chunks — میبینی lucide-react/dist/... یا کل react-icons/fa لم داده تو باندل کلاینت. برای چند تا نقاشی ثابت SVG، داری ۴۴ کیلوبایت JS به هر کاربر میفرستی — روی اینترنت 3G یعنی +۲۸۰ میلیثانیه تاخیر تا صفحه interactive بشه. فقط بخاطر آیکون.
مقالهی دیروز Rushan از GeoIcons دقیق همین رو کالبدشکافی کرده: بعضی کتابخانهها با Server Component رفیقن، بعضی نه — و با یک grep پنجثانیهای میفهمی کدوم به کدومه.
اصل ماجرا — یک قانون ساده
RSC فقط یک قانون داره:
- بدون
'use client'→ Server Component. روی سرور رندر میشه، خروجیاش HTML/SVG خالصه، صفر JS میره سمت مرورگر. - با
'use client'در خط اول → Client Component. اون فایل و هرچی import کرده باید به مرورگر فرستاده بشه.
گیر آیکونها اینجاست: خیلی از کتابخانهها (مخصوصاً react-icons و lucide-react با barrel-export) توی فایل index.ts که همهی آیکونها رو re-export میکنه، همون بالا 'use client' گذاشتن — یا آیکونها با forwardRef و useState ساخته شدن و چارهای جز client بودن ندارن. تو فقط مینویسی:
ولی عملاً کل کتابخانه میچسبه به باندل کلاینت. tree-shaking هم کاری نمیتونه بکنه — چون boundary قبل از bundling تعیین میشه.
و نکتهای که تو داک Next.js خیلی پررنگ نیست: boundary مسریه — به سمت بالا. کافیه یک leaf کلاینت باشه تا parent هم کلاینت بشه. «فقط یک آیکون بود» بهونه نیست.
تست سریع: کتابخونهات سالمه یا مسموم؟
| کتابخانه | grep -rl "^['\"]use client['\"]" node_modules/<pkg> | یعنی چی برات |
|---|---|---|
| @geoicons/react | فقط dist/_license.js | آیکونها Server هستن — صفر JS ✅ |
| lucide-react@0.469 (barrel) | dist/lucide-react.js + تکتک آیکونها | با barrel کل صفحه client میشه ❌ |
| react-icons | همهی fa/*.js | عملاً همیشه client ❌ |
به زبون ساده:
app/page.tsx (Server)→import { Nz } from "@geoicons/react"→ هیچ boundary نیست → Next.js همون<svg><path d="m6.35..." /></svg>رو تو HTML میذاره. تمام.app/page.tsx→import { Search } from "lucide-react"(barrelِ کلاینت) → کلpage.tsxسقوط میکنه به کلاینت — حتی بدون اینکه خودت'use client'نوشته باشی.
راه حل — از ۴۴ کیلوبایت به صفر
قدم ۱: تست مسمومیت پنجثانیهای
قبل از هر کاری اینو بزن:
نکته: حتماً
^اول regex باشه. بدون اون، فایلهایی که تو کامنت نوشتن «why we avoid use client» هم match میخورن و ۷۹۹ تا false-positive میگیری.
قدم ۲: قبل → بعد
قبل — import بهظاهر بیگناه که صفحه رو مسموم میکنه:
بعد — آیکون سروری، صفر JS:
چرا کار میکنه؟ سورس آیکون رو ببین — خبری از 'use client' نیست:
قدم ۳: «میخوام lucide بمونه»
اشکالی نداره — فقط barrel رو دور بزن:
اگه حتی فایلهای تکی هم 'use client' داشتن، تمیزترین راه: یک بار SVG خام رو کپی کن. یک بار زحمت، تا ابد صفر وابستگی:
قدم ۴: مطمئن شو واقعاً درست شد
بعد از pnpm build:
تلههایی که نصفشب میترکوننت
-
optimizePackageImportsboundary رو پاک نمیکنه. فقط barrel رو tree-shake میکنه، ولی اگه خود فایل مقصد'use client'داشته باشه، بازم client میمونی. اولgrepبزن، بعد optimize. -
IconProviderتنها چیزیه که باید کلاینت باشه. تو GeoIcons فقطIconProvider(برای theme) بهuseState/useEffectنیاز داره و'use client'داره. آیکونها بهش وصل نیستن، پس provider رو بذار توapp/layout.tsxو بقیهی درخت سروری میمونه. اگه provider و آیکون رو تو یک فایل بذاری، کل فایل client میشه — جدا نگهشون دار. -
دیدن path دوبار تو HTML طبیعیه. حتی با آیکون سروری، Next.js علاوه بر HTML یه flight data هم میفرسته که همون
d="m6.35..."رو تکرار میکنه. اگه تو HTML دنبال<pathبگردی دوبار میبینی. طبیعیه — مهم اینه که تو/_next/static/chunks/*.jsنباشه. -
داستان
useId. آیکون GeoIcons ازuseIdبرایclipPathبا id یکتا استفاده میکنه. تو Server Component (React 19) کار میکنه، ولی اگه همون آیکون رو داخل یک Client Component رندر کنی، id سرور و کلاینت mismatch میخوره و hydration warning میگیری. قانون ساده: آیکون سروری رو فقط داخل Server Component رندر کن، یاidرو دستی پاس بده.
میارزه؟ اعداد رو ببین
تست روی Next.js 15.1، صفحهی /pricing با ۶ آیکون، gzip -9:
| سناریو | First Load JS (gz) | JS اضافهی آیکون | HTML اضافه | TTI روی 3G | |---|---|---|---|---| | بدون آیکون | 87.5 kB | — | — | 1.9s | | ۶ تا lucide-react (barrel) | 131.8 kB | +44.3 kB | ~2 kB | 2.18s | | ۶ تا lucide (direct import) | 102.1 kB | +14.6 kB | ~2 kB | 2.02s | | ۶ تا GeoIcons (سروری) | 87.9 kB | +0.4 kB | +8.1 kB | 1.91s | | ۶ تا SVG خام (کپی path) | 87.6 kB | +0.1 kB | +8.0 kB | 1.90s |
به زبون خودمونی: barrel حدود ۲۸۰ میلیثانیه روی 3G بهت تاخیر میده. طبق دیتای Vercel هر ۱۰۰ میلیثانیه تاخیر ≈ یک درصد افت conversion تو فروشگاه اینترنتی. اون ۲۸۰ms یعنی ۲-۳٪ فروش کمتر — برای یه فروشگاه ۵۰۰ میلیونی، ماهی ۱۰-۱۵ میلیون تومن.
کی چی استفاده کن؟
| شرایط | چی بزن | چرا |
|---|---|---|
| آیکون ثابت (pricing، فوتر، لیست ویژگی) | SVG سروری / GeoIcons | صفر JS، سریعترین |
| آیکون تعاملی (دکمه لایک با انیمیشن) | یه Client Component جدا فقط برای همون آیکون | بقیه صفحه سروری میمونه |
| ۵۰+ آیکون تو یک صفحه | SVG sprite یا icon font | وگرنه HTML باد میکنه |
| theming داینامیک (dark/light) | GeoIcons + IconProvider تو layout | provider کلاینته ولی آیکونها نه — بهترین هر دو |
چکلیست — ۶ قدم تا ship امن
۱. تست مسمومیت: grep -rl "^['\"]use client['\"]" node_modules/<lib> — اگه هر آیکون match خورد، اون lib رو برای صفحات سروری بیخیال شو.
۲. نصب سروری: pnpm add @geoicons/react یا SVG خام رو کپی کن تو components/icons/*.tsx (بدون 'use client').
۳. صفحه رو سروری نگه دار: مطمئن شو app/pricing/page.tsx هیچ 'use client' نداره. آیکونها رو مستقیم import کن.
۴. provider رو ایزوله کن:
۵. build رو چک کن: pnpm build && grep -r "lucide\|react-icons" .next/static/chunks/ — باید خالی باشه.
۶. تو CI مانیتور کن: du -sh .next/static/chunks/*.js | sort -h رو لاگ کن. اگه First Load JS بیشتر از ۵kB نسبت به main رشد کرد، PR رو block کن.
کی این راه حل رو نزن
- وقتی آیکون واقعاً تعاملیه. اگه روی hover مورف میکنه، با
framer-motionانیمیت میشه، یا state داخلی داره، باید Client Component باشه. زور نزن سروری نگهش داری — فقط hydration mismatch میگیری. یهcomponents/icons/AnimatedCheck.tsxبا'use client'جدا بساز. - وقتی ۲۰۰+ آیکون تو یک صفحه داری (مثلاً گالری). ۲۰۰ تا SVG با path طولانی HTML رو ۳۰۰kB میکنه و parse روی موبایل کنده. اینجا sprite sheet بهصرفهتره.
- وقتی کتابخونه با CSS-in-JS استایل تزریق میکنه (مثل
phosphor-react) — بدون JS خراب میشه. ذاتاً clientـه — یا lib رو عوض کن یا هزینهش رو بپذیر.
لینکهای داخلی: کالبدشکافی کامل boundary تو /tech/rsc-poisoning-use-client-boundary-tech. نمونهکارها تو /projects و برای مشاورهی performance از /about پیام بده.
منابع: GeoIcons — SVG icons in RSC (2026-08-31)، Next.js Docs — use client، تست باندل محلی Next.js 15.1.