بهینه‌سازی فونت وب در ۲۰۲۶: راهنمای بارگذاری سریع، WOFF2 و حذف CLS

راهنمای عملی ۲۰۲۶ برای بهینه‌سازی فونت وب: WOFF2، preload درست، حذف CLS با size-adjust و subsetting برای LCP سریع‌تر.

بهینه‌سازی فونت وب ۲۰۲۶: راهنمای جامع

به‌روزرسانی: ۱ اوت ۲۰۲۶

بهینه‌سازی فونت وب یعنی تحویل کوچک‌ترین فایل ممکن، در سریع‌ترین زمان، با کمترین جابه‌جایی چیدمان (CLS). در سال ۲۰۲۶ سه اهرم اصلی این کار عبارت‌اند از فرمت WOFF2، preload حساب‌شده تنها برای فونت بحرانی LCP، و تنظیم size-adjust روی یک @font-face جایگزین تا سوییچ فونت هیچ pixel جابه‌جایی ایجاد نکند. راستش را بخواهید، من روی هزاران ترِیس Chrome DevTools کار کرده‌ام و همیشه همین سه اهرم را می‌بینم که تفاوت میان یک صفحه‌ی سبز در Core Web Vitals و یک صفحه‌ی مرده به دلیل فونت را رقم می‌زنند.

  • در ۲۰۲۶ تنها فرمت مورد نیاز WOFF2 است؛ حجم آن حدود ۳۰٪ کمتر از WOFF و پشتیبانی مرورگرها ۹۸٪+ است.
  • فقط فونتی را preload کنید که در بلوک متن LCP رندر می‌شود؛ preload بی‌رویه به rendering بحرانی آسیب می‌زند.
  • ترکیب font-display: swap با یک @font-face جایگزین تنظیم‌شده با size-adjust، ascent-override و descent-override می‌تواند CLS ناشی از فونت را به صفر برساند.
  • Self-host فونت‌ها به جای Google Fonts CDN: از زمان partitioning کش HTTP در Chrome و Safari (2020)، هیچ کش اشتراکی بین‌سایتی وجود ندارد و مزیت CDN از بین رفته است.
  • ابزارهای خودکار مانند next/font و Fontaine محاسبه‌ی descriptorهای متریک را انجام می‌دهند؛ در ۲۰۲۶ استفاده‌ی دستی توجیهی ندارد.
  • Subsetting با unicode-range حجم پیلود را ۵۰ تا ۹۰٪ کاهش می‌دهد؛ برای فارسی به‌طور خاص، subsets عربی و ارقام باید جداگانه صرف شوند.

چرا بهینه‌سازی فونت برای عملکرد وب حیاتی است؟

فونت‌ها روی هر سه معیار Core Web Vitals اثر مستقیم دارند و متأسفانه در بسیاری از سایت‌ها هنوز به عنوان یک «asset معمولی» با آن‌ها برخورد می‌شود. در آخرین ترِیس‌هایی که در Chrome DevTools تحلیل کرده‌ام، فونت وب معمولاً بین ۳۰۰ تا ۹۰۰ میلی‌ثانیه از زمان LCP یک صفحه‌ی متنی را می‌بلعد و اگر سوییچ فونت با یک قلم fallback بی‌تنظیم انجام شود، به‌راحتی ۰.۱۵ تا ۰.۲۵ به CLS اضافه می‌کند.

وقتی متن، بزرگ‌ترین المان قابل نمایش صفحه (LCP element) باشد، بلوک شدن رندر تا زمانی که فونت دانلود شود، مستقیماً LCP را عقب می‌اندازد. برای INP هم اگرچه parse فونت روی main thread سبک است، اما loaderهای font-driven مبتنی بر جاوااسکریپت (مثل قدیمی‌های Typekit یا بعضی راه‌حل‌های آیکون‌فونت) می‌توانند long task ایجاد کنند.

در گزارش Web Almanac 2025، ۵۴٪ سایت‌ها هنوز از Google Fonts استفاده می‌کنند و بخش بزرگی از آن‌ها روی origin سوم‌شخص، بدون preconnect و بدون فونت جایگزین تنظیم‌شده. این یعنی بهینه‌سازی LCP بدون توجه به فونت مثل بستن یک در در حالی که پنجره باز است.

بهترین فرمت فونت در ۲۰۲۶: چرا فقط WOFF2؟

در ۲۰۲۶ فقط یک فرمت لازم دارید: WOFF2. این فرمت از فشرده‌سازی Brotli استفاده می‌کند، حدود ۳۰٪ کوچک‌تر از WOFF است و پشتیبانی مرورگرها بر اساس Can I Use بالای ۹۸٪ است (حتی iOS Safari از نسخه ۱۲ به بعد). TTF، OTF، EOT و WOFF قدیمی را از تولید حذف کنید؛ هر byte اضافه‌ی fallback در HTTP response شما به نفع هیچ‌کس نیست.

یک بلوک @font-face بهینه در ۲۰۲۶ این شکلی است:

/* فقط WOFF2، بدون فرمت‌های legacy */
@font-face {
  font-family: 'Vazirmatn';
  src: url('/fonts/vazirmatn-latin.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0000-00FF, U+0131, U+0152-0153;
}

@font-face {
  font-family: 'Vazirmatn';
  src: url('/fonts/vazirmatn-arabic.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0600-06FF, U+FB50-FDFF, U+FE70-FEFF;
}

سایز نمونه‌ی مقایسه‌ای که در پروژه‌های واقعی می‌بینم: TTF کامل با تمام glyphها ۲۰۰ تا ۵۰۰KB، WOFF2 غیرsubset شده حدود ۶۰ تا ۱۲۰KB، و WOFF2 با subset فارسی+لاتین ۱۵ تا ۳۰KB. تفاوت میان آخری و اولی روی 4G یعنی یک ثانیه‌ی تمام از LCP.

چگونه فونت وب را به‌درستی preload کنیم؟

Preload کردن فونت روش نگفتنِ «مرورگر، این فایل را زودتر شروع کن؛ بعد از parse شدن CSS برایش نمان» است. اما preload مثل نمک است: اگر همه چیز را نمکین کنید، هیچ چیز مزه‌دار نخواهد بود. قاعده‌ی من: فقط یک یا حداکثر دو فایل فونت (آنهایی که در بلوک متن LCP رندر می‌شوند) با preload بارگذاری شوند.

<!-- در <head>، پیش از preload کردن CSS اصلی -->
<link
  rel="preload"
  as="font"
  type="font/woff2"
  href="/fonts/vazirmatn-arabic.woff2"
  crossorigin>

سه نکته‌ی حیاتی که در code reviewها دائم گیر می‌دهم:

  1. ویژگی crossorigin اجباری است حتی برای فونت same-origin. بدون آن، مرورگر دو بار فونت را fetch می‌کند: یک بار برای preload بدون CORS، یک بار برای @font-face با CORS. برای فونت‌ها همیشه crossorigin="anonymous".
  2. ویژگی type را ذکر کنید تا مرورگرهایی که WOFF2 را پشتیبانی نمی‌کنند (تقریباً هیچ‌کدام در ۲۰۲۶، ولی محکم‌کاری) از دانلود صرف‌نظر کنند.
  3. روی سرورهای کند، preload را به HTTP 103 Early Hints ببرید تا حتی پیش از رسیدن HTML اصلی، مرورگر شروع به دانلود کند.

هشدار عملی: preload بیش از دو فونت با LCP رقابت می‌کند و تصویر hero یا CSS اصلی را به تعویق می‌اندازد. اگر باید سه weight دارید، به variable font فکر کنید.

font-display چیست و چه زمانی از swap استفاده کنیم؟

خاصیت CSS font-display به مرورگر می‌گوید تا زمان دانلود فونت وب چه کار کند. پنج مقدار دارد: auto، block، swap، fallback، و optional. تفاوت اصلی در طول دو دوره است: block period (زمانی که متن نامرئی است) و swap period (زمانی که با فونت جایگزین رندر می‌شود و بعد سوییچ می‌کند).

مقدارBlock periodSwap periodپدیده‌ی UXمورد استفاده
block~۳ ثانیهبی‌نهایتFOIT طولانیلوگو یا آیکون‌فونت
swap۰ (فوری fallback)بی‌نهایتFOUTمتن بدنه، اکثر موارد
fallback~۱۰۰ms~۳ ثانیهFOIT کوتاه + سوییچ محدودمتن غیربحرانی
optional~۱۰۰ms۰یا در اولین بار می‌آید یا نهوقتی خواندنی بودن > برند
autoمرورگر تصمیم می‌گیردنامعلوممعمولاً مشابه blockهرگز؛ صریح انتخاب کنید

پیشنهاد من برای ۲۰۲۶: swap با فونت جایگزین تنظیم‌شده. FOIT (متن نامرئی) تجربه‌ی کاربر و SEO را می‌کشد چون تا زمانی که فونت نیامده حتی screen reader هم متن ندارد. FOUT (فلش فونت جایگزین) وقتی fallback به‌درستی تنظیم شده باشد تقریباً نامرئی است. برای سایت‌های خیلی حساس به تجربه‌ی برند، optional در بازدید اول قربانی می‌کند اما در بازدیدهای بعدی تجربه‌ی کامل می‌دهد.

چگونه CLS ناشی از فونت را با size-adjust به صفر برسانیم؟

وقتی مرورگر از فونت جایگزین (Arial) به فونت اصلی (Vazirmatn) سوییچ می‌کند، اگر ابعاد جعبه‌ی خط (line-box) این دو فونت متفاوت باشد، تمام متن پایین‌تر تکان می‌خورد. همان دلیل رایج ۰.۱ تا ۰.۲۵ CLS در سایت‌های محتوایی. راه‌حل ۲۰۲۶: یک @font-face جداگانه برای فونت جایگزین تعریف کنید و با چهار descriptor آن را با فونت اصلی هم‌متریک کنید.

/* جایگزین تنظیم‌شده برای Vazirmatn روی Arial */
@font-face {
  font-family: 'Vazirmatn Fallback';
  src: local('Arial');
  ascent-override: 92.5%;
  descent-override: 25.3%;
  line-gap-override: 0%;
  size-adjust: 108.2%;
}

body {
  font-family: 'Vazirmatn', 'Vazirmatn Fallback', system-ui, sans-serif;
}

سه descriptor کلیدی:

  • size-adjust: گلیف‌های فونت جایگزین را افقی و عمودی مقیاس می‌دهد. اگر Arial باریک‌تر از Vazirmatn است، مثلاً ۱۰۸.۲٪ آن را «چاق» می‌کند تا عرض متن یکی شود و هیچ reflow افقی نداشته باشیم.
  • ascent-override و descent-override: بالا و پایین جعبه‌ی خط را تنظیم می‌کنند تا ارتفاع خط با فونت اصلی برابر شود.
  • line-gap-override: فاصله‌ی بین خطوط را کنترل می‌کند؛ برای اکثر فونت‌ها ۰٪ کار می‌کند.

محاسبه‌ی این اعداد را دستی نکنید. ابزار Automatic Font Adjusting از Malte Ubl را استفاده کنید یا اگر روی Next.js هستید، next/font این کار را خودکار انجام می‌دهد. من در پروژه‌های production همیشه این توصیه را می‌کنم چون یک درصد اشتباه در size-adjust می‌تواند خودش تولید CLS کند. برای درک عمیق‌تر مکانیک CLS، مقاله‌ی بهینه‌سازی CLS ما را ببینید.

آیا باید Google Fonts را self-host کنیم در ۲۰۲۶؟

بله. در سال ۲۰۲۶، self-host کردن فونت‌های Google تقریباً همیشه سریع‌تر، امن‌تر و از نظر GDPR ایمن‌تر است. استدلال قدیمی «کش اشتراکی بین‌سایتی» با partitioning کش HTTP در Chrome و Safari از سال ۲۰۲۰ به بعد از بین رفت. هر origin کش جداگانه‌ی خودش را دارد، پس اگر کاربر Vazirmatn را از سایت A دانلود کند، در سایت B دوباره دانلود می‌شود.

مزایای عملی self-hosting:

  • حذف یک DNS lookup، TCP handshake و TLS negotiation به origin سوم‌شخص (معمولاً ۱۰۰–۳۰۰ms صرفه‌جویی روی 4G).
  • امکان استفاده از Cache-Control: immutable با max-age یک‌ساله برای فایل‌های hashed.
  • سازگاری با CSP سختگیرانه بدون نیاز به whitelist کردن fonts.gstatic.com.
  • مطابقت با رأی دادگاه ایالتی مونیخ (ژانویه ۲۰۲۲) که Google Fonts CDN را نقض GDPR اعلام کرد.

روش عملی: از Google Webfonts Helper فایل‌های WOFF2 را دانلود کنید، در /public/fonts بگذارید و @font-face رول‌ها را داخل CSS اصلی بنویسید. یادتان نرود در response header فونت Cache-Control: public, max-age=31536000, immutable و Access-Control-Allow-Origin: * بگذارید.

Subsetting و unicode-range: کاهش حجم فونت تا ۹۰٪

یک فایل Vazirmatn کامل حاوی هزاران گلیف عربی، لاتین، ارقام فارسی، اموجی و علائم است، اما یک صفحه‌ی وب معمولی شاید فقط ۲۰۰ گلیف را استفاده کند. Subsetting یعنی برش دادن فونت به فقط آن گلیف‌هایی که واقعاً استفاده می‌شوند و ارائه‌ی آن‌ها به مرورگر با اعلام unicode-range.

/* Subset فارسی */
@font-face {
  font-family: 'Vazirmatn';
  src: url('/fonts/vazirmatn-arabic.woff2') format('woff2');
  unicode-range: U+0600-06FF, U+FB50-FDFF, U+FE70-FEFF;
  font-display: swap;
}
/* Subset لاتین برای برندها، URL، اعداد انگلیسی */
@font-face {
  font-family: 'Vazirmatn';
  src: url('/fonts/vazirmatn-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153;
  font-display: swap;
}
/* Subset ارقام فارسی/عربی-هندی */
@font-face {
  font-family: 'Vazirmatn';
  src: url('/fonts/vazirmatn-digits.woff2') format('woff2');
  unicode-range: U+06F0-06F9, U+0660-0669;
  font-display: swap;
}

مرورگر تنها فایل‌هایی را fetch می‌کند که حداقل یک کاراکتر روی صفحه در range آن‌ها بیفتد. برای یک صفحه‌ی محصول انگلیسی که تصادفاً یک نام فارسی دارد، مرورگر تنها Latin subset را دانلود می‌کند (~۱۵KB) نه کل فونت را (~۱۲۰KB). برای ساخت subset از fonttools/pyftsubset یا glyphhanger استفاده کنید.

Variable Fonts: کی به عملکرد کمک می‌کنند و کی نه؟

Variable fonts یک فایل واحدند که چندین axis (وزن، پهنا، slant، optical size) را در خود دارند. سؤال کلیدی: کاهش تعداد HTTP request به قیمت افزایش حجم یک فایل، برای شما به‌صرفه است یا نه؟

قاعده‌ی سرانگشتی من:

  • اگر ۳ weight یا بیشتر استفاده می‌کنید (مثلاً ۴۰۰، ۵۰۰، ۷۰۰): variable font معمولاً برنده است. یک فایل VF ۸۰KB جای سه فایل ۲۵KB.
  • اگر فقط ۱ یا ۲ weight نیاز دارید: static WOFF2 subset‌شده کوچک‌تر و سریع‌تر است.
  • اگر animation روی axisها می‌خواهید (مثلاً وزن هنگام hover): VF بدون رقیب است.

نکته‌ی مهم برای فارسی و عربی: Vazirmatn و بسیاری از فونت‌های مدرن نویسه‌های عربی به‌صورت variable با axis wght عرضه می‌شوند. اگر از weight animation استفاده نمی‌کنید، یک static subset برای weight ۴۰۰ و یک برای ۷۰۰ کافی است و کوچک‌تر از VF درمی‌آید.

ابزارهای خودکار: next/font، Fontaine و Early Hints

در ۲۰۲۶ اکثر کار متمایل خودکار شده است. اگر روی Next.js 14+ هستید، next/font کل زنجیره را انجام می‌دهد:

// app/layout.tsx
import { Vazirmatn } from 'next/font/google';

const vazir = Vazirmatn({
  subsets: ['arabic', 'latin'],
  display: 'swap',
  weight: ['400', '700'],
  variable: '--font-vazir',
  adjustFontFallback: 'Arial', // متریک‌ها را خودکار محاسبه می‌کند
});

export default function Layout({ children }) {
  return (
    <html lang="fa" dir="rtl" className={vazir.variable}>
      <body>{children}</body>
    </html>
  );
}

این کد: (۱) فونت را در build time دانلود و در بیلد شما embed می‌کند (self-hosting خودکار)، (۲) subset‌های درخواستی را جدا می‌کند، (۳) ‌هم‌متریک fallback را با adjustFontFallback خودکار محاسبه می‌کند، (۴) فایل‌ها را با هش نام‌گذاری و با Cache-Control: immutable serve می‌کند. اگر روی Nuxt یا Vite هستید، Fontaine / @nuxtjs/fontaine همین کار را انجام می‌دهد.

برای سرورهای کند، HTTP 103 Early Hints را فعال کنید. Cloudflare، Fastly و Vercel از ۲۰۲۴ به بعد پشتیبانی می‌کنند. سرور می‌تواند پیش از تولید HTML اصلی، هدر Link: </fonts/vazirmatn.woff2>; rel=preload; as=font; crossorigin را با status 103 بفرستد و مرورگر همان لحظه شروع به دانلود می‌کند. در پروژه‌های واقعی این تکنیک ۱۰۰ تا ۴۰۰ms از LCP کم می‌کند، به‌خصوص وقتی TTFB بالاست.

اشتباهات رایج در بهینه‌سازی فونت

در auditهایی که انجام می‌دهم، همین چند خطای تکراری بیش از ۸۰٪ مشکلات فونت را می‌سازند:

  1. استفاده از @import در CSS: parse CSS را بلوک می‌کند و به discovery فونت تأخیر می‌اندازد. همیشه <link rel="stylesheet"> در HTML.
  2. preload کردن هر فونت: اگر ۵ فونت preload کنید، مرورگر با prioritization بحرانی رقابت می‌کند و LCP بدتر می‌شود.
  3. font-display: block روی متن بدنه: کاربر ۳ ثانیه متن نامرئی می‌بیند. برای INP و SEO فاجعه است.
  4. فراموش کردن unicode-range: بدون آن، هر فونت هر سه subset (فارسی، لاتین، ارقام) را همیشه دانلود می‌کند حتی اگر صفحه فقط لاتین باشد.
  5. fallback بدون size-adjust: منبع اصلی CLS. اگر ابزار خودکار ندارید، حداقل با font-size-adjust: 0.5 مدرن (پشتیبانی همه‌ی مرورگرهای اصلی از 2024) شروع کنید.
  6. لود کردن Google Fonts از CDN با یک تگ <link> بدون preconnect: حتی اگر self-host نمی‌کنید، حداقل <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> را اضافه کنید.
  7. ذخیره‌سازی فونت با Cache-Control کوتاه: فایل‌های فونت hashed را با ۱ سال max-age و immutable serve کنید.

پرسش‌های متداول

چگونه یک فونت وب را به‌درستی preload کنم؟

فقط فونت بحرانی LCP را با <link rel="preload" as="font" type="font/woff2" href="..." crossorigin> در <head> preload کنید. ویژگی crossorigin اجباری است حتی برای same-origin؛ بدون آن مرورگر دو بار fetch می‌کند و preload بی‌فایده می‌شود.

FOUT بهتر است یا FOIT؟

در تقریباً همه‌ی موارد FOUT (با font-display: swap) بهتر است. FOIT متن را نامرئی نگه می‌دارد، تجربه کاربر و SEO را می‌کشد. اگر fallback را با size-adjust هم‌متریک کنید، فلش FOUT تقریباً نامرئی می‌شود.

آیا variable fonts به عملکرد آسیب می‌زنند؟

نه اگر ۳ وزن یا بیشتر استفاده کنید. یک فایل VF کوچک‌تر از چند static font است. اگر فقط ۱ یا ۲ وزن نیاز دارید، static WOFF2 subset‌شده کوچک‌تر است. تصمیم را با اندازه‌گیری واقعی حجم بگیرید نه با فرض.

چقدر می‌توانم با subsetting حجم فونت را کم کنم؟

در پروژه‌های واقعی بین ۵۰ تا ۹۰٪. یک فونت فارسی-عربی کامل حدود ۱۲۰KB است؛ همان فونت با subset فارسی+لاتین شاید ۲۰KB شود. تفاوت اصلی حذف گلیف‌های زبان‌های دیگر و اموجی است.

آیا در ۲۰۲۶ باید Google Fonts را self-host کنم؟

بله. کش HTTP از ۲۰۲۰ به بعد partition شده و مزیت CDN اشتراکی از بین رفته. Self-hosting یک DNS lookup و TLS handshake اضافی را حذف می‌کند، امکان immutable caching یک‌ساله می‌دهد، و GDPR-friendly است.

Alex Petrov
درباره نویسنده Alex Petrov

Web performance engineer who treats every millisecond as a personal challenge. Has profiled more sites than he can count.