حجم باندل جاوااسکریپت یکی از بزرگترین دشمنان عملکرد وب است — و صادقانه بگویم، یکی از چیزهایی که خیلی راحت نادیده گرفته میشود تا وقتی که Lighthouse نمره قرمز بدهد. یک باندل سنگین یعنی زمان بیشتر برای دانلود، parse کردن، کامپایل و اجرا — همه اینها مستقیماً روی بهینهسازی LCP (Largest Contentful Paint) و بهینهسازی INP (Interaction to Next Paint) تأثیر میگذارند. در این راهنما، گامبهگام یاد میگیریم چطور با ابزارهای مدرن ۲۰۲۶ حجم باندل را به شکل چشمگیری کاهش دهیم.
چرا حجم باندل جاوااسکریپت مهم است؟
طبق دادههای ۲۰۲۶، متوسط صفحه وب روی دسکتاپ بیش از ۶۲۰ کیلوبایت جاوااسکریپت فشردهنشده دانلود میکند. اما مشکل فراتر از پهنای باند است — بسیاری از توسعهدهندگان این را میدانند ولی هنوز دقیقتر فکر نکردهاند:
- Parse time: مرورگر باید کد را به درخت AST تبدیل کند. برای هر ۱ مگابایت جاوااسکریپت روی گوشی میانه، این فرآیند ۱ تا ۴ ثانیه طول میکشد.
- Compile time: موتور V8 باید کد را به bytecode کامپایل کند (و این روی باتری گوشی هم تأثیر میگذارد!).
- Execution time: کدهای اضافی main thread را بلاک میکنند و INP را مستقیماً آسیب میزنند.
- Cache invalidation: باندل بزرگتر یعنی احتمال بیشتر تغییر و کشباطلشدن مکرر.
Google با معرفی INP به عنوان معیار Core Web Vitals، ارتباط مستقیم بین حجم جاوااسکریپت و رتبهبندی جستجو را آشکارتر کرده است. هر کیلوبایت اضافی میتواند زمان پاسخدهی به تعامل کاربر را افزایش دهد.
گام اول: تحلیل باندل با ابزارهای مناسب
قبل از هر اقدامی، باید بدانید چه کدی در باندل شما وجود دارد. بهترین ابزارها عبارتند از:
برای Vite: rollup-plugin-visualizer
npm install --save-dev rollup-plugin-visualizer
// vite.config.js
import { visualizer } from "rollup-plugin-visualizer";
export default {
plugins: [
visualizer({
filename: "dist/stats.html",
open: true,
gzipSize: true,
brotliSize: true,
}),
],
};
برای Webpack: webpack-bundle-analyzer
npm install --save-dev webpack-bundle-analyzer
// webpack.config.js
const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer");
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: "static",
reportFilename: "bundle-report.html",
}),
],
};
پس از اجرا، یک نقشه درختی تعاملی میبینید که حجم هر ماژول را نشان میدهد. دنبال «غولهای پنهان» بگردید: moment.js با ۳۳۰KB، lodash با ۷۰KB، و کتابخانههایی که فکر نمیکردید اصلاً در پروژه دارید. (اعتراف میکنم که بار اول دیدم moment.js نصف باندلم بود، واقعاً شوکه شدم.)
گام دوم: Tree Shaking — حذف کدهای مرده
Tree Shaking فرآیندی است که bundlerهای مدرن مثل Webpack، Rollup و Vite انجام میدهند و کدهای استفادهنشده را از باندل نهایی حذف میکنند. این تکنیک روی ES Modules متکی است و چند اصل ساده دارد:
۱. استفاده از ES Modules (import/export)
// bad — CommonJS قابل tree-shake نیست
const { debounce } = require("lodash");
// good — ES Module قابل تحلیل ایستا است
import { debounce } from "lodash-es";
۲. علامتگذاری ماژولها به عنوان side-effect-free
{
"name": "my-app",
"sideEffects": false
}
اگر فایلهای CSS یا polyfill دارید که side effect محسوب میشوند، باید آنها را صریحاً مشخص کنید:
{
"sideEffects": ["*.css", "./src/polyfills.js"]
}
۳. دقت در Barrel Files
یکی از رایجترین دامهای tree shaking، barrel fileها هستند — همان فایلهای index.js که همه چیز را re-export میکنند. خیلی خوب به نظر میرسند ولی میتوانند کل ماژول را وارد باندل کنند:
// bad — ممکن است کل barrel وارد شود
import { Button } from "../components";
// good — import مستقیم از سورس
import { Button } from "../components/Button/Button";
۴. تنظیم Webpack برای tree shaking بهینه
// webpack.config.js
module.exports = {
mode: "production", // تمام tree shaking به صورت خودکار فعال میشود
optimization: {
usedExports: true,
providedExports: true,
sideEffects: true,
concatenateModules: true, // Scope hoisting
minimize: true,
},
};
گام سوم: Code Splitting و Dynamic Imports
در حالی که tree shaking کد مرده را حذف میکند، code splitting باندل را به chunkهای کوچکتر تقسیم میکند که فقط در زمان نیاز بارگذاری میشوند. این احتمالاً مهمترین تکنیک برای بهبود Time to Interactive است — پس بیایید کمی بیشتر روی آن وقت بگذاریم.
Dynamic Import در جاوااسکریپت خالص
// bad — همه چیز در باندل اولیه
import { HeavyChart } from "./HeavyChart";
// good — فقط وقتی کاربر نیاز دارد بارگذاری میشود
const loadChart = async () => {
const { HeavyChart } = await import("./HeavyChart");
return HeavyChart;
};
React.lazy و Suspense برای code splitting کامپوننتها
import React, { Suspense, lazy } from "react";
const RichTextEditor = lazy(() => import("./RichTextEditor"));
const HeavyDashboard = lazy(() => import("./HeavyDashboard"));
function App() {
return (
<Suspense fallback={<div>در حال بارگذاری...</div>}>
<RichTextEditor />
</Suspense>
);
}
Route-based Code Splitting در Next.js
// app/dashboard/page.tsx
import dynamic from "next/dynamic";
const HeavyChart = dynamic(() => import("@/components/HeavyChart"), {
loading: () => <ChartSkeleton />,
ssr: false,
});
const MapComponent = dynamic(() => import("@/components/MapComponent"), {
loading: () => <MapSkeleton />,
ssr: false,
});
تنظیم splitChunks در Webpack
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
react: {
test: /[\/]node_modules[\/](react|react-dom)[\/]/,
name: "react-vendor",
priority: 20,
},
vendor: {
test: /[\/]node_modules[\/]/,
name: "vendors",
priority: 10,
reuseExistingChunk: true,
},
},
},
},
};
گام چهارم: جایگزینی کتابخانههای سنگین
بزرگترین اثر فوری را از جایگزینی کتابخانههای سنگین میگیرید. این بخش است که وقتی نتایجش را در DevTools میبینید، واقعاً لذت میبرید:
moment.js (330KB) → date-fns یا Temporal API
// bad — moment.js با تمام localeهایش (330KB)
import moment from "moment";
const formatted = moment(date).format("YYYY-MM-DD");
// good — date-fns با tree shaking (~1KB)
import { format } from "date-fns";
const formatted = format(date, "yyyy-MM-dd");
// better — Temporal API بومی (صفر KB اضافه در ۲۰۲۶)
const formatted = Temporal.PlainDate.from(date).toString();
lodash (70KB) → lodash-es یا native APIs
// bad — کل lodash وارد میشود
import _ from "lodash";
const unique = _.uniqBy(items, "id");
// good — فقط تابع مورد نیاز
import uniqBy from "lodash/uniqBy";
// better — native JavaScript
const unique = [...new Map(items.map(i => [i.id, i])).values()];
axios (45KB) → fetch API بومی
const data = await fetch("/api/users")
.then(res => {
if (!res.ok) throw new Error(res.statusText);
return res.json();
});
خلاصه کتابخانههای رایج و جایگزینهای سبکتر آنها:
- moment.js (330KB) → date-fns یا Temporal API
- lodash (70KB) → توابع native یا lodash-es با tree shaking
- axios (45KB) → fetch API بومی
- jQuery (87KB) → vanilla JS یا توابع native DOM
- animate.css (80KB) → CSS transitions بومی یا Web Animations API
گام پنجم: React Server Components — بزرگترین انقلاب ۲۰۲۶
React Server Components (RSC) تکنولوژیای است که میتواند حجم JavaScript client-side را تا ۷۰٪ کاهش دهد. کامپوننتهای سرور کاملاً روی سرور اجرا میشوند و کدشان اصلاً به مرورگر ارسال نمیشود — این یک تغییر پارادایم واقعی است، نه فقط یک بهینهسازی جزئی.
اصل اساسی: use client را تا عمق ممکن ببرید
// bad — کل صفحه به client bundle اضافه میشود
"use client";
export default function ProductPage({ id }) {
const [liked, setLiked] = useState(false);
return (
<div>
<ProductDetails id={id} />
<Reviews id={id} />
<button onClick={() => setLiked(true)}>Like</button>
</div>
);
}
// good — فقط کامپوننت کوچک interactive در client bundle
// LikeButton.tsx
"use client";
export function LikeButton() {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked(true)}>
{liked ? "Liked!" : "Like"}
</button>
);
}
// ProductPage.tsx — Server Component (بدون "use client")
export default async function ProductPage({ id }) {
const product = await fetchProduct(id);
return (
<div>
<ProductDetails product={product} />
<Reviews id={id} />
<LikeButton />
</div>
);
}
با این الگو، کتابخانههایی که فقط در Server Components استفاده میشوند (مثل ORM، کتابخانههای markdown، ابزارهای data fetching) اصلاً به client bundle اضافه نمیشوند. خیلی ساده و خیلی قدرتمند.
گام ششم: فشردهسازی Brotli
فشردهسازی Brotli نسبت به gzip تا ۲۰٪ نتیجه بهتری میدهد و در تمام مرورگرهای مدرن ۲۰۲۶ پشتیبانی میشود. اگر هنوز فقط از gzip استفاده میکنید، این یک «quick win» واقعی است.
فعالسازی در Vite
npm install --save-dev vite-plugin-compression
// vite.config.js
import compression from "vite-plugin-compression";
export default {
plugins: [
compression({ algorithm: "brotliCompress", ext: ".br" }),
compression({ algorithm: "gzip", ext: ".gz" }),
],
};
تنظیم Nginx برای Brotli
http {
brotli on;
brotli_comp_level 6;
brotli_types
application/javascript
application/json
text/css
text/html;
}
گام هفتم: بودجه عملکردی (Performance Budget)
بدون بودجه عملکردی، باندل در طول زمان به آرامی رشد میکند — آنقدر کند که متوجه نمیشوید. یک CI check ساده این مشکل را حل میکند:
// scripts/check-bundle-size.js
import { statSync } from "fs";
import { glob } from "glob";
const BUDGET_KB = 250;
const jsFiles = glob.sync("dist/assets/*.js");
let failed = false;
for (const file of jsFiles) {
const sizeKB = statSync(file).size / 1024;
if (sizeKB > BUDGET_KB) {
console.error(`FAIL ${file}: ${sizeKB.toFixed(1)}KB exceeds ${BUDGET_KB}KB budget`);
failed = true;
}
}
if (failed) process.exit(1);
console.log("PASS Bundle size budget OK");
{
"scripts": {
"build:check": "vite build && node scripts/check-bundle-size.js"
}
}
نتیجهگیری: ترکیب تکنیکها برای حداکثر اثر
ترکیب این تکنیکها میتواند باندل شما را به طرز چشمگیری کوچک کند. دادههای واقعی از پروژههای ۲۰۲۶ نشان میدهد که نتایج واقعاً قابل توجه هستند:
- React Server Components: تا ۷۰٪ کاهش JavaScript client-side
- Route-based code splitting: ۴۰ تا ۶۰٪ کاهش باندل اولیه
- Tree shaking + جایگزینی کتابخانه: ۲۰ تا ۴۰٪ کاهش اضافی
- Brotli compression: ۷۰ تا ۸۰٪ کاهش حجم انتقالی
با اجرای همه این گامها، تیمهایی که باندلهای ۲ مگابایتی داشتند به زیر ۲۰۰ کیلوبایت رسیدهاند — بهبودی که مستقیماً روی LCP، INP و رتبه جستجو اثر میگذارد. برای اندازهگیری دقیق نتایج، Lighthouse یا Chrome DevTools را قبل و بعد از هر تغییر اجرا کنید.
سوالات متداول
Tree shaking دقیقاً چیست و چطور کار میکند؟
Tree shaking یک فرآیند حذف کد مرده است که bundlerهایی مثل Webpack، Rollup و Vite انجام میدهند. این ابزارها ساختار ایستای ES Modules را تحلیل میکنند و exportهای استفادهنشده را از باندل نهایی حذف میکنند. برای کارکرد صحیح، باید از import/export (نه require) استفاده کنید و sideEffects: false را در package.json تنظیم کنید.
آیا Code Splitting تأثیر منفی روی SEO دارد؟
خیر، code splitting هیچ تأثیر منفی روی SEO ندارد. Google میتواند JavaScript را رندر کند و lazy loading به درستی توسط Googlebot مدیریت میشود. در واقع با بهبود Core Web Vitals (مخصوصاً LCP و INP)، code splitting تأثیر مثبت روی رتبهبندی جستجو دارد.
بهترین ابزار برای آنالیز حجم باندل در Vite چیست؟
بهترین ابزار برای Vite، پلاگین rollup-plugin-visualizer است که یک نقشه درختی تعاملی از تمام ماژولهای باندل شما تولید میکند. برای Webpack، از webpack-bundle-analyzer استفاده کنید. هر دو ابزار حجم هر ماژول را به صورت بصری نمایش میدهند تا نقاط مشکل را راحتتر شناسایی کنید.
آیا React Server Components با همه فریمورکها سازگار است؟
React Server Components در حال حاضر به بهترین شکل در Next.js (نسخه ۱۳ به بالا) پشتیبانی میشود. فریمورکهایی مثل Remix و Gatsby نیز در حال پیادهسازی این قابلیت هستند. اگر از Vite با React خالص استفاده میکنید، میتوانید از Astro یا Qwik به عنوان جایگزینهای server-first که JavaScript صفر را به client ارسال میکنند، بهره ببرید.
چه حجمی از باندل جاوااسکریپت ایدهآل است؟
برای عملکرد بهینه در ۲۰۲۶، هدف شما باید زیر ۱۵۰ کیلوبایت JavaScript فشردهشده (Brotli) برای مسیر اصلی سایت باشد. Google توصیه میکند کل JavaScript اولیه از ۲۰۰ کیلوبایت تجاوز نکند. از Performance Budget در CI/CD استفاده کنید تا مانع رشد تدریجی باندل شوید.