یک وباپلیکیشن میتواند در لپتاپ تیم توسعه سریع باشد و روی موبایل میانرده کاربر ایرانی عملاً از کار بیفتد. دلیل معمول فقط «اینترنت کند» نیست: مرورگر باید باندل جاوااسکریپت را دانلود، از حالت فشرده خارج، Parse، Compile و اجرا کند؛ سپس Hydration، درخواستهای API، اسکریپتهای تحلیل و ویجتهای ثالث هم برای همان Main Thread رقابت میکنند. نتیجه صفحهای است که دیده میشود اما لمس دکمه، تایپ در فرم یا بازشدن منو با مکث پاسخ میگیرد.
بهینهسازی اپلیکیشن جاوااسکریپت یک مسابقه برای کوچکترین فایل نیست. هدف، ساختن یک قرارداد قابلاندازهگیری میان تجربه کاربر، قابلیت محصول، امنیت و هزینه عملیات است. این راهنما برای مدیر فنی، توسعهدهنده فرانتاند، مالک محصول و تیم امنیت نوشته شده تا از داده واقعی به گلوگاه برسند، اصلاح کمریسک را انتخاب کنند و بدون قربانیکردن امنیت یا قابلیت نگهداشت منتشر کنند.
بهینهسازی اپلیکیشن جاوااسکریپت دقیقاً چه مسئلهای را حل میکند؟
«اپلیکیشن سنگین» تعریف ثابتی برحسب مگابایت ندارد. یک داشبورد حسابداری با ۴۰۰ کیلوبایت جاوااسکریپت ممکن است روی دستگاه ضعیف و هنگام Hydration کند باشد؛ اپ دیگری با باندل بزرگتر، بهدلیل Cache مناسب و بارگذاری مرحلهای، پاسخگو بماند. مسئله زمانی جدی است که هزینه شبکه، CPU، حافظه یا کار Main Thread از بودجه تجربه عبور کند.
چهار هزینهای که باید جداگانه ببینید
| هزینه | نشانه کاربر | شاهد مناسب | خطای تشخیص رایج |
|---|---|---|---|
| انتقال | نمایش اولیه دیر است | Transfer size، Waterfall، Cache hit | نگاهکردن به حجم خام بهجای حجم Brotli/Gzip |
| Parse و Compile | صفحه روی موبایل دیر آماده میشود | Performance trace و CPU profile | فرض اینکه دانلود سریع یعنی اجرای سریع |
| اجرای Main Thread | کلیک یا تایپ مکث دارد | INP، Long Task و Event timing | تمرکز صرف بر LCP |
| حافظه | پس از چند دقیقه افت سرعت یا Crash | Heap snapshot و Allocation timeline | تست فقط در بارگذاری اول |
برای روش نظاممند جمعآوری Trace، CPU Profile و Heap Snapshot، راهنمای پروفایلینگ و دیباگ پیشرفته وباپلیکیشن را کنار این مقاله بخوانید.
خروجی بهینهسازی باید Outcome باشد، نه امتیاز ابزار
امتیاز Lighthouse برای تشخیص آزمایشگاهی مفید است، اما موفقیت محصول را ثابت نمیکند. خروجی را با جملهای مانند این تعریف کنید: «در Route جستوجوی محصول، صدک ۷۵ INP کاربران موبایل از ۳۸۰ به کمتر از ۲۰۰ میلیثانیه برسد؛ نرخ خطای افزودن به سبد افزایش نیابد؛ و حجم JavaScript اولیه از بودجه تعیینشده عبور نکند.» این تعریف هم قابل آزمون است و هم جلوی بهینهسازی نمایشی را میگیرد.
خط مبنا: Field، Lab و داده کاربران ایرانی
قبل از تغییر کد، یک Baseline نسخهدار بسازید. داده میدانی میگوید کاربر چه تجربهای داشته و آزمایشگاه کمک میکند علت را بازتولید کنید. یکی جای دیگری را نمیگیرد.
Core Web Vitals فعلی را درست بخوانید
مجموعه فعلی Core Web Vitals شامل LCP برای بارگذاری، INP برای پاسخگویی و CLS برای پایداری بصری است؛ FID دیگر معیار اصلی این مجموعه نیست. طبق راهنمای رسمی Web Vitals، آستانه «خوب» در صدک ۷۵ بازدیدها، برای LCP حداکثر ۲٫۵ ثانیه، INP حداکثر ۲۰۰ میلیثانیه و CLS حداکثر ۰٫۱ است. تفکیک موبایل و دسکتاپ ضروری است.
اگر میخواهید جمعآوری، سگمنتبندی و تفسیر این سه معیار را دقیق طراحی کنید، راهنمای Core Web Vitals و RUM مسیر کاملتری ارائه میکند.
RUM را به سگمنت قابل اقدام تبدیل کنید
میانگین کل سایت معمولاً گلوگاه را پنهان میکند. حداقل این ابعاد را ثبت کنید:
- Route یا Page template و نسخه Release؛
- نوع دستگاه، حافظه تقریبی و کلاس CPU در حدی که مرورگر اجازه میدهد؛
- نوع اتصال گزارششده، کشور/ناحیه در سطح غیرشخصی و اپراتور در صورت دسترسی قانونی؛
- کاربر تازه یا بازگشتی و وضعیت Cache؛
- نام تعامل برای INP، مانند جستوجو، فیلتر، افزودن به سبد یا ارسال فرم؛
- وجود خطای JavaScript، Timeout API یا شکست Chunk.
برای بازار ایران، تست با اینترنت دفتر تهران و پرچمدار تیم کافی نیست. موبایل میانرده، اتصال سلولی با نوسان RTT، Packet loss و قطع موقت را در ماتریس آزمون بگذارید. اگر سرویس برای خارج از ایران هم کاربر دارد، دو مسیر شبکه را جدا بسنجید؛ میانگین آنها برای تصمیم معماری گمراهکننده است.
Lab برای بازتولید، نه جایگزینی واقعیت
در CI از Lighthouse یا ابزار مشابه با تنظیم ثابت CPU و Network استفاده کنید و Performance trace را برای سناریوهای کلیدی نگه دارید. Lighthouse بدون تعامل واقعی نمیتواند INP میدانی را مستقیماً اندازه بگیرد؛ TBT فقط یک Proxy آزمایشگاهی برای کار مسدودکننده است. سناریوی Lab باید با داده RUM انتخاب شود، نه با صفحهای که تیم بهصورت تصادفی دوست دارد تست کند.
بودجه عملکرد: مرز قابلاجرا برای جلوگیری از بازگشت مشکل
عبارت «باندل را کوچک کنیم» بهتنهایی الزام مهندسی نیست. بودجه باید Scope، Metric، روش اندازهگیری و پیامد عبور را مشخص کند.
| بودجه نمونه | Scope | روش کنترل | واکنش به شکست |
|---|---|---|---|
| JavaScript اولیه فشرده | هر Route عمومی | گزارش Build و CI | Block merge یا تأیید استثنا |
| Long Task بالای ۵۰ms | سناریوی افزودن به سبد | Trace خودکار/دستی | مالک و Issue زماندار |
| INP صدک ۷۵ | کاربران موبایل هر Route | RUM هفتگی | توقف Rollout |
| اسکریپت ثالث | کل سایت | Inventory و Waterfall | حذف، تأخیر یا Sandbox |
| Heap پس از چرخه تکراری | SPA پس از ۲۰ Navigation | Heap comparison | رفع Leak پیش از Release |
عدد جهانی برای همه محصولات وجود ندارد. بودجه را از Baseline، هدف تجربه، دستگاه مرجع و محدودیت تجاری خود استخراج کنید. یک نقشه سهبعدی یا ویرایشگر آنلاین ممکن است بودجهای متفاوت از صفحه ورود داشته باشد، اما هر دو باید مرز و دلیل روشن داشته باشند.
اول حذف کنید، بعد فشرده و تقسیم کنید
ارزانترین JavaScript همان کدی است که ارسال نمیشود. قبل از تنظیم پیچیده Bundler، Inventory بسازید: کدام Feature، کتابخانه، Polyfill و اسکریپت ثالث برای Route اولیه واقعاً لازم است؟ مالک هر مورد کیست و حذفش چه اثری دارد؟
Bundle را با Treemap و Import graph بخوانید
- حجم Raw و فشرده هر Chunk را ثبت کنید؛
- نسخههای تکراری یک Dependency را پیدا کنید؛
- Locale، Icon pack و Utilityهای کامل را که فقط بخشی از آنها مصرف میشود جدا کنید؛
- Polyfill را براساس Browser support واقعی هدفگذاری کنید؛
- کد Featureهای خاموش و Flagهای منقضی را حذف کنید؛
- اثر هر تغییر را بر Parse/Compile و نه فقط Transfer بسنجید.
Tree Shaking شرط دارد
نوشتن ES Module بهتنهایی تضمین نمیکند کد مرده حذف شود. Side effect اعلامنشده، الگوی Import نامناسب، بسته CommonJS یا تنظیم اشتباه Build میتواند Tree Shaking را ناکارآمد کند. خروجی نهایی را بررسی و یک Regression test حجمی اضافه کنید. Minification و Brotli مفیدند، اما کد فشرده همچنان باید روی دستگاه کاربر Parse و اجرا شود.
اسکریپت ثالث را محصولی مستقل حساب کنید
چت، Heatmap، A/B testing، تبلیغات و Tag Manager میتوانند بیش از کد خود محصول Main Thread را اشغال کنند. برای هر Vendor این موارد را بنویسید: هدف کسبوکاری، مالک، دامنههایی که فراخوانی میکند، زمان بارگذاری، دسترسی داده، رفتار هنگام Timeout و تاریخ بازبینی. اسکریپت بدون مالک، نامزد حذف است.
Code Splitting و Lazy Loading بدون Waterfall جدید
تقسیم کد باید JavaScript لازم برای شروع Route را کاهش دهد. راهنمای رسمی Code Splitting در web.dev توضیح میدهد که کاهش Payload اولیه میتواند هزینه Parse، Compile و اجرا را کم و INP را بهتر کند. اما Split افراطی تعداد زیادی Chunk ریز، Waterfall و سربار Request میسازد.
مرزهای مناسب Split
- Route-level: کد پنل گزارش نباید همراه صفحه ورود بیاید؛
- Feature-level: ویرایشگر، نمودار پیشرفته یا Export تنها هنگام نیاز؛
- Interaction-level: مودال یا ابزار سنگین پس از Intent معتبر کاربر؛
- Vendor-level: کتابخانههای بزرگ با نرخ تغییر متفاوت، پس از تحلیل Cache.
// نمونه ساده: بارگذاری قابلیت فقط هنگام درخواست کاربر
button.addEventListener('click', async () => {
const { openAdvancedReport } = await import('./advanced-report.js');
openAdvancedReport();
});برای Interaction محتمل میتوان Prefetch را پس از Idle یا نشانه Intent آغاز کرد؛ برای Chunk واقعاً بحرانی Preload را با احتیاط بهکار برد. Preload زیاد اولویت شبکه را خراب میکند. روی اتصال ضعیف و Data Saver رفتار متفاوت تعریف کنید و شکست بارگذاری Chunk پس از انتشار نسخه جدید را با Retry محدود و پیام قابلفهم مدیریت کنید.
Lazy Loading باید وضعیت تجربه داشته باشد
کاربر نباید پس از کلیک با فضای خالی مواجه شود. Loading state، Timeout، Error state و Retry را طراحی کنید. اگر قابلیت برای تکمیل خرید ضروری است، آن را بیشازحد دیر بار نکنید. Lazy Loading تصمیم UX است، نه فقط تابع import().
Main Thread، Long Task و بهبود INP
یک Task طولانیتر از ۵۰ میلیثانیه Long Task محسوب میشود. مرورگر هنگام اجرای آن فرصت پاسخگویی و Paint مناسب ندارد. راهنمای رسمی بهینهسازی Long Task پیشنهاد میکند کار طولانی به بخشهای کوچکتر شکسته شود تا مرورگر بتواند کار با اولویت بالاتر، از جمله تعامل کاربر، را انجام دهد.
INP را تا Interaction مقصر دنبال کنید
INP فقط «سرعت JavaScript» نیست و سه بخش دارد: Input delay، زمان اجرای Callback و Presentation delay. برای هر Interaction کند بپرسید:
- آیا Task دیگری پیش از Event handler صف را مسدود کرده است؟
- Callback محاسبه، Validation یا State update سنگین دارد؟
- DOM بزرگ، Style recalculation یا Layout thrashing زمان Paint را بالا برده است؟
- آیا چند Handler یا کتابخانه ثالث به همان Interaction متصلاند؟
کار را خرد و به مرورگر Yield کنید
کار ضروری برای Feedback فوری را انجام دهید و بخش غیرقابلمشاهده را به Task بعدی ببرید. از API زمانبندی متناسب با Browser support پروژه استفاده کنید؛ Fallback داشته باشید و ترتیب داده را از دست ندهید.
async function saveSettings(input) {
showPendingState(); // بازخورد فوری
const validated = validate(input);
await new Promise(resolve => setTimeout(resolve, 0));
await persist(validated); // کار بعدی، خارج از Task اول
scheduleAnalytics(validated); // غیر بحرانی
}خردکردن کورکورانه هم هزینه Scheduling دارد. Trace قبل و بعد را مقایسه کنید. هدف کاهش Blocking و پاسخ سریعتر است، نه افزایش تعداد Promiseها.
Layout Thrashing و DOM بزرگ را اصلاح کنید
خواندن Layout پس از چند تغییر Style در همان Task میتواند Forced synchronous layout ایجاد کند. خواندنها را گروهبندی، سپس نوشتنها را انجام دهید؛ List بلند را Virtualize کنید؛ State را در کوچکترین Subtree لازم بهروزرسانی کنید و محاسبه مشتقشده را بیدلیل در هر Render تکرار نکنید. برای Animation نیز Transform و Opacity معمولاً کمهزینهترند، اما هر Motion باید با سنجش و پشتیبانی prefers-reduced-motion همراه باشد. راهنمای UI Motion و INP این مرز را عملیتر توضیح میدهد.
Web Worker چه زمانی مفید است؟
Worker یک Thread جدا برای کار CPU-bound فراهم میکند، اما به DOM دسترسی مستقیم ندارد و انتقال پیام نیز هزینه دارد. راهنمای Web Workers در MDN مبانی مدل پیام را توضیح میدهد.
کارهای مناسب برای Worker
- پردازش فایل، CSV یا تصویر در مرورگر؛
- محاسبات آماری، رمزنگاری مجاز و Transformation سنگین؛
- جستوجو یا فیلتر روی Dataset بزرگ؛
- Parser یا الگوریتمی که به DOM وابسته نیست.
برای یک Callback ۱۰ میلیثانیهای Worker نسازید. Serialization، Structured clone، Startup و نگهداشت کد دوطرفه ممکن است سود را از بین ببرد. برای داده حجیم، Transferableها را بررسی کنید؛ Protocol پیام را Version و خطا/لغو/Timeout را تعریف کنید.
// main.js
const worker = new Worker(new URL('./report.worker.js', import.meta.url), {
type: 'module'
});
worker.postMessage({ type: 'BUILD_REPORT', rows });
worker.onmessage = ({ data }) => renderResult(data);
// report.worker.js
self.onmessage = ({ data }) => {
if (data.type === 'BUILD_REPORT') {
self.postMessage(buildReport(data.rows));
}
};اگر مسئله واقعاً محاسباتی و قابلحمل است، WebAssembly هم گزینه بررسی است؛ اما WASM خودکار سریعتر یا امنتر نیست و هزینه Bridge، Memory و Toolchain باید در Pilot سنجیده شود.
استراتژی رندر: CSR، SSR، SSG یا ترکیبی؟
هیچ برنده مطلقی وجود ندارد. «SSR برای سئو بهتر است» یک سادهسازی خطرناک است؛ خروجی HTML معنادار معمولاً کشف و نمایش محتوا را آسانتر میکند، اما SSR ضعیف میتواند TTFB بالا، Hydration سنگین و دو بار هزینه رندر بسازد. تصمیم باید Route-by-route باشد.
| مدل | مناسب برای | ریسک اصلی | کنترل |
|---|---|---|---|
| CSR | ابزار پشت Login با تعامل زیاد | HTML اولیه کممحتوا و Startup سنگین | Shell سبک، Split، Cache و تست Crawl در صفحات عمومی |
| SSR | صفحه عمومی پویا یا شخصیسازیشده | TTFB، هزینه Server و Hydration | Cache، Streaming، کاهش Client JS |
| SSG | محتوای پایدار و قابل Build | Build طولانی و تازگی محتوا | Incremental rebuild و Invalidation روشن |
| Islands/ترکیبی | صفحه محتوایی با چند جزء تعاملی | پیچیدگی مرز State | Contract کامپوننت و Observability |
Hydration را یک هزینه مستقل حساب کنید
HTML سریع بهتنهایی کافی نیست. اگر مرورگر برای فعالکردن صفحه مجبور به اجرای باندل بزرگ و Hydrate کل Tree باشد، کاربر یک «صفحه ظاهراً آماده اما بیپاسخ» میبیند. Partial hydration، Islands یا Server Components بسته به Framework میتوانند Client JavaScript را کاهش دهند، اما Migration باید با Vertical slice و اندازهگیری انجام شود.
قرارداد Crawl و Index برای Route عمومی
برای صفحه عمومی، Source HTML و Rendered DOM، Status code، Canonical، Robots، لینکهای قابلخزش و محتوای اصلی را بررسی کنید. راهنمای سئو PWA و رندر JavaScript چکهای Crawl/Index و Cache را پوشش میدهد. Render شدن توسط Google مجوزی برای مخفیکردن محتوای حیاتی پشت Interaction یا URL ناپایدار نیست.
Network، Cache و بارگذاری Resource
بهینهسازی Runtime بدون Delivery مناسب نیمهکاره است. فایلهای Content-hashed را با Cache بلندمدت منتشر کنید، HTML را با سیاست متفاوت نگه دارید و Rollout را طوری طراحی کنید که HTML جدید به Chunk حذفشده اشاره نکند.
async و defer را با ترتیب اجرا انتخاب کنید
deferبرای اسکریپت کلاسیک وابسته به DOM مناسب است و ترتیب سند را حفظ میکند؛asyncپس از دانلود اجرا میشود و ترتیب نسبت به سایر Asyncها تضمینشده نیست؛- Moduleها بهصورت پیشفرض رفتار deferمانند دارند، اما Dependency graph و Dynamic import باید سنجیده شود؛
- اسکریپت بحرانی را صرفاً برای حذف هشدار ابزار، بیدلیل عقب نیندازید.
Cache Contract و Deploy اتمیک
نام فایل Chunk را Hashدار کنید؛ Artifact نسخه قبل را تا پایان TTL و پنجره بازگشت نگه دارید؛ Service Worker را نسخهگذاری کنید؛ و خطای ChunkLoadError را در Telemetry جدا ثبت کنید. در PWA، Cache-first کورکورانه ممکن است کد قدیمی را با API جدید ترکیب کند. قرارداد سازگاری Backend/Frontend و مسیر Rollback لازم است.
حافظه، Leak و افت عملکرد در نشست طولانی
SPA را فقط با Reload آزمایش نکنید. Listener حذفنشده، Timer، Subscription، Closure، DOM جداشده و Cache بدون سقف باعث رشد Heap میشوند. سناریوی تکراری تعریف کنید: ورود، ۲۰ بار جابهجایی Route، باز و بستهکردن Modal، بارگذاری فهرست و خروج. Heap Snapshot اول و آخر را مقایسه کنید.
نشانههای Leak
- Heap پس از Garbage Collection به خط مبنای نزدیک برنمیگردد؛
- تعداد Detached DOM node یا Listener پیوسته رشد میکند؛
- Navigationهای بعدی کندتر از اول میشوند؛
- تب موبایل پس از مدتی Reload یا Crash میشود.
هر Component باید Lifecycle خروج روشن داشته باشد: Abort کردن Fetch، Unsubscribe، پاککردن Timer/Observer و محدودکردن Cache. اصلاح Leak را با Snapshot و سناریوی تکرارپذیر ثابت کنید، نه با حس بهتر تیم.
امنیت XSS: Encoding، Sanitization و Safe Sink
Framework مدرن خطر XSS را کم میکند، اما از بین نمیبرد. Escape hatchهایی مانند dangerouslySetInnerHTML، HTML خام، URL کنترلنشده، Template injection و Dependency قدیمی مرز اعتماد را میشکنند. راهنمای رسمی پیشگیری از XSS در OWASP تأکید میکند که هیچ دفاع واحدی کافی نیست.
دفاع را با Context تطبیق دهید
- ورودی را براساس قرارداد کسبوکار Validate کنید؛
- خروجی را متناسب با Context یعنی HTML، Attribute، URL، CSS یا JavaScript Encode کنید؛
- اگر واقعاً باید HTML کاربر را نمایش دهید، از Sanitizer نگهداریشده استفاده کنید؛
- بهجای
innerHTMLاز Safe sink مانندtextContentبهره ببرید؛ - URL scheme را Allowlist و از
javascript:جلوگیری کنید؛ - تست امنیتی را برای Source تا Sink بنویسید.
// بد: داده کنترلنشده به یک injection sink میرود
target.innerHTML = profile.bio;
// بهتر برای متن ساده
target.textContent = profile.bio;CSP لایه دوم است، نه جایگزین اصلاح کد
CSP قوی با Nonce یا Hash و strict-dynamic میتواند بهرهبرداری از بخشی از XSS را دشوار کند، اما Allowlist شل یا اتکای تنها به CSP کافی نیست. ابتدا در حالت Report-Only داده بگیرید، Violationها را تحلیل و سپس Enforcement را مرحلهای کنید. برای طراحی Policy، Nonce و Rollout به راهنمای Strict CSP مراجعه کنید.
Trusted Types میتواند عبور String خام به Injection sinkهای مشخص را محدود کند. راهنمای CSP و Trusted Types در MDN نحوه Enforcement را توضیح میدهد. Browser support هدف و مسیر Legacy را قبل از اجبار بررسی کنید.
امنیت زنجیره تأمین JavaScript
اجرای npm audit fix --force سیاست امنیت نیست. ممکن است تغییر Major ناسازگار وارد کند یا ریسک واقعی Runtime را روشن نکند. مستندات npm audit توصیه میکند Advisory، عامل کاهنده و Patch موجود بررسی شود.
حداقل کنترلهای Dependency
- Lockfile را Commit و نصب CI را Reproducible کنید؛
- Registry و Scope بسته را محدود و دسترسی Publish را با MFA کنترل کنید؛
- Dependency بلااستفاده را حذف و Transitive dependencyهای پرریسک را بشناسید؛
- SBOM یا Inventory نسخهدار بسازید؛
- Advisory را با Reachability و Exposure اولویتبندی کنید؛
- بهروزرسانی را در Branch با تست عملکردی، امنیتی و Bundle انجام دهید؛
- Secret را هرگز در باندل Client قرار ندهید؛ متغیر Build عمومی همچنان برای کاربر قابلمشاهده است.
برای وصلکردن SAST، Dependency scan، Secret scan و Policy exception به Pipeline، راهنمای DevSecOps را ببینید.
دارایی Client راز نیست: Token و داده حساس را درست مرزبندی کنید
Minify یا Obfuscate کردن JavaScript راز را پنهان نمیکند. API key دارای اختیار، Credential سرویس، منطق ضدتقلب حساس و داده خصوصی باید در سمت قابلاعتماد باقی بماند. Token مرورگر حداقل Scope و عمر لازم را داشته باشد؛ Cookie حساس با ویژگیهای مناسب و دفاع CSRF طراحی شود؛ و داده ذخیرهشده در Local Storage با فرض دسترسی اسکریپت صفحه ارزیابی شود.
خطاهای Client نباید Stack، Token یا داده شخصی را به Telemetry بفرستند. Redaction، Sampling، Retention و دسترسی به Log را قرارداد کنید. «برای دیباگ لازم است» مجوز جمعآوری نامحدود داده کاربر نیست.
تست: عملکرد و امنیت را در یک ماتریس ببینید
تغییر Performance میتواند کنترل امنیتی را تضعیف کند و برعکس. انتقال منطق به Worker، Dynamic import یا CDN مرز اعتماد و Failure mode جدید میسازد. ماتریس زیر حداقل پوشش را نشان میدهد.
| لایه | تست | شاهد پذیرش |
|---|---|---|
| Build | Bundle budget، duplicate dependency، sourcemap exposure | گزارش CI و Diff نسخه |
| Unit/Component | Sanitization، Cleanup، Error state | تست خودکار |
| Integration | Chunk failure، API timeout، Token expiry | سناریوی قابل تکرار |
| Lab performance | Mobile CPU/Network، Long Task، Heap loop | Trace و Snapshot |
| Security | XSS source-to-sink، CSP Report، Dependency advisory | Finding و Retest |
| Field | INP/LCP/CLS، JS error، Conversion guardrail | داشبورد RUM سگمنتشده |
انتشار امن: Canary، Guardrail و Rollback
بهینهسازی بزرگ را یکباره برای همه منتشر نکنید. Vertical slice انتخاب کنید؛ برای نمونه فقط Route فهرست کالا و Interaction فیلتر. نسخه را ابتدا برای تیم داخلی، سپس درصد کوچکی از کاربران و بعد مرحلهای فعال کنید.
چه چیزهایی باید Rollout را متوقف کنند؟
- بهبود INP همراه با افزایش خطای JavaScript؛
- کاهش حجم باندل همراه با رشد ChunkLoadError؛
- بهبود LCP همراه با افت Conversion یا دسترسپذیری؛
- افزایش CSP violation غیرمنتظره یا شکست Login/پرداخت؛
- رشد TTFB یا هزینه Server پس از SSR؛
- افزایش Memory در نشست طولانی.
Rollback باید پیش از Release تمرین شود: Feature flag، Artifact نسخه قبل، سازگاری API، Cache invalidation و مالک تصمیم. برای اتصال معیار فنی به اثر تجاری، راهنمای سرعت سایت، UX و تبدیل چارچوب مناسبی دارد.
نمونه ایرانی: فروشگاه با فیلتر سنگین و اینترنت ناپایدار
فرض کنید فروشگاهی فارسی در صفحه دستهبندی ۱٫۲ مگابایت JavaScript فشرده میفرستد. فیلتر با هر تغییر همه کارتها را دوباره Render میکند، ابزار چت در Startup اجرا میشود و داده RUM فقط میانگین کل سایت را نشان میدهد. کاربران موبایل میگویند دکمه فیلتر «گاهی کار نمیکند».
تشخیص
- RUM براساس Route و Interaction نشان میدهد صدک ۷۵ INP فیلتر در موبایل ضعیف است؛
- Trace یک Task ۲۸۰ میلیثانیهای برای Normalize و Sort داده و Render کامل List نشان میدهد؛
- Treemap کتابخانه نمودار و پنل ادمین را در Chunk عمومی پیدا میکند؛
- چت ثالث هنگام Startup دو Long Task میسازد؛
- Heap پس از ده بار بازوبستهکردن فیلتر رشد میکند، چون Listener پاک نمیشود.
اصلاح مرحلهای
- کد ادمین از Entry عمومی حذف و نمودار با Dynamic import جدا میشود؛
- چت پس از Consent و Idle یا Intent معتبر بار میشود؛
- فهرست Virtualize و State update محدود میشود؛
- Sort سنگین در Pilot به Worker منتقل و هزینه انتقال داده سنجیده میشود؛
- Cleanup Listener و Abort درخواست قبلی اضافه میشود؛
- Canary با Guardrail نرخ افزودن به سبد، خطای JS و INP منتشر میشود.
نتیجه معتبر نه «امتیاز ۱۰۰»، بلکه رسیدن Interaction هدف به بودجه، ثابتماندن تبدیل و نبود Regression امنیتی در سگمنت واقعی است.
نقشه اجرایی ۳۰ روزه
| بازه | کار | تحویلدادنی |
|---|---|---|
| روز ۱ تا ۵ | Inventory Route/Bundle/Third-party و Baseline RUM/Lab | نقشه هزینه و سه گلوگاه اول |
| روز ۶ تا ۱۰ | تعریف بودجه، SLO و Guardrail محصول | Performance contract در CI و داشبورد |
| روز ۱۱ تا ۱۸ | حذف کد، Split محدود، رفع Long Task/Leak | Vertical slice قابل مقایسه |
| روز ۱۹ تا ۲۳ | XSS review، CSP Report-Only، Dependency triage | Threat/Dependency register و Retest |
| روز ۲۴ تا ۲۷ | آزمون شبکه/دستگاه، Chunk failure و Rollback | Evidence pack و Runbook |
| روز ۲۸ تا ۳۰ | Canary، مشاهده Guardrail و تصمیم ادامه | گزارش Outcome و Backlog بعدی |
چکلیست نهایی بهینهسازی اپلیکیشن JavaScript
- برای Route و Interaction حیاتی، مالک و Outcome تعریف شده است؛
- RUM به تفکیک موبایل، Route، Release و شبکه تحلیل میشود؛
- LCP، INP و CLS فعلی بهجای FID قدیمی سنجیده میشوند؛
- بودجه JavaScript، Long Task، Third-party و Memory در CI یا Review وجود دارد؛
- کد، Polyfill و Dependency بلااستفاده پیش از فشردهسازی حذف شده است؛
- Split براساس Route/Feature است و Waterfall یا Chunk failure کنترل میشود؛
- Interaction کند تا Input delay، Callback و Presentation delay تحلیل شده است؛
- Worker فقط پس از سنجش هزینه پیام برای کار CPU-bound استفاده شده است؛
- استراتژی رندر Route-by-route و هزینه Hydration اندازهگیری شده است؛
- Cache و Deploy از HTML/Chunk ناسازگار جلوگیری میکنند؛
- Leak با سناریوی نشست طولانی و Heap evidence آزموده شده است؛
- XSS با Encoding زمینهای، Sanitization و Safe sink کنترل میشود؛
- CSP لایه دوم و Rollout آن Report-Only تا Enforcement است؛
- Lockfile، Audit، SBOM/Inventory و Dependency triage وجود دارد؛
- Canary، Guardrail و Rollback پیش از انتشار تعریف و تمرین شدهاند.
جمعبندی
بهینهسازی اپلیکیشن جاوااسکریپت از «کمکردن حجم فایل» شروع میشود اما آنجا تمام نمیشود. شما باید هزینه انتقال، Parse/Compile، اجرای Main Thread، Hydration و حافظه را برای دستگاه و شبکه واقعی بسنجید؛ سپس با حذف، Split متعادل، Yield، Worker و انتخاب رندر مناسب گلوگاه را برطرف کنید. همزمان، Encoding، Sanitization، CSP و کنترل زنجیره تأمین اجازه نمیدهند سرعت به قیمت امنیت تمام شود.
بهترین تیمها یکبار سایت را سریع نمیکنند؛ آنها بودجه، Telemetry، CI و Rolloutی میسازند که Regression را زود آشکار کند. معیار نهایی نیز روشن است: تعامل حیاتی برای بیشتر کاربران سریعتر شده، خطا و ریسک بالا نرفته و اثر در محصول قابل مشاهده است.
پرسشهای متداول
آیا حجم کمتر Bundle همیشه اپلیکیشن را سریعتر میکند؟
معمولاً کمک میکند، اما کافی نیست. Parse، Compile، اجرای Main Thread، Hydration، تعداد Chunk، Cache و هزینه اسکریپت ثالث نیز مهماند. حجم را همراه Trace و RUM بسنجید.
برای بهبود Core Web Vitals باید FID را بهینه کنیم یا INP را؟
INP معیار فعلی پاسخگویی در Core Web Vitals است. FID فقط تأخیر اولین ورودی را میسنجید؛ INP تعاملهای طول نشست و تأخیر تا Paint بعدی را بهتر پوشش میدهد.
SSR همیشه برای سئو و سرعت بهتر از CSR است؟
خیر. SSR میتواند HTML معنادار و کشف Resource بهتر بدهد، اما TTFB و Hydration سنگین ممکن است تجربه را بدتر کند. تصمیم را برای هر Route با Crawl test، RUM و هزینه عملیات بگیرید.
چه زمانی از Web Worker استفاده کنیم؟
وقتی کار CPU-bound و مستقل از DOM واقعاً Main Thread را مسدود میکند؛ مانند پردازش فایل یا محاسبه سنگین. ابتدا Trace بگیرید و هزینه Startup و انتقال پیام را در Pilot اندازه بگیرید.
آیا CSP بهتنهایی جلوی XSS را میگیرد؟
خیر. CSP دفاع تکمیلی است. دفاع اصلی شامل Output encoding متناسب با Context، Sanitization برای HTML مجاز، Safe sink، Validation و حذف الگوهای ناامن است؛ CSP باید لایه دوم باشد.






