
فونتها یکی از فراموششدهترین بخشهای بهینهسازی عملکرد وب هستند. خیلی از تیمها روی تصاویر، جاوااسکریپت و کش کردن API وقت میگذارند، اما فونت وب را با یک <link> ساده به Google Fonts متصل میکنند و از کنارش رد میشوند. نتیجه معمولاً همان چیزی است که همهمان تجربهاش کردهایم: صفحه لود میشود، متن با فونت پیشفرض سیستم نمایش داده میشود، و یک لحظه بعد، وقتی فونت واقعی از سرور گوگل میرسد، کل چیدمان صفحه میپرد. این پدیده Cumulative Layout Shift یا CLS نام دارد و یکی از معیارهای اصلی Core Web Vitals گوگل است.
Next.js برای حل دقیق همین مشکل، ماژول next/font را ارائه کرده است. این ماژول فونتها را در زمان build دانلود و بهصورت خودکار روی همان سرور شما میزبانی (self-host) میکند؛ یعنی مرورگر کاربر هیچ درخواستی به سرورهای گوگل ارسال نمیکند. در این مقاله قرار است این ابزار را از صفر تا استفاده حرفهای در پروژههای واقعی بررسی کنیم.
قبل از اینکه سراغ کد برویم، بهتر است بدانیم این ابزار دقیقاً چه مشکلاتی را حل میکند:
از نسخهی 13.2 به بعد Next.js، دیگر نیازی به نصب پکیج جداگانه نیست. قبلاً این قابلیت با نام @next/font یک پکیج مستقل بود، اما الان مستقیماً داخل خود Next.js قرار دارد و فقط کافی است از next/font/google یا next/font/local ایمپورت کنید.
سادهترین حالت استفاده، ایمپورت مستقیم یک فونت از گوگل و اعمال آن روی layout اصلی پروژه است:
import { Inter } from 'next/font/google'
// اگر فونت متغیر (variable) است، نیازی به تعیین weight نیست
const inter = Inter({
subsets: ['latin'],
display: 'swap',
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
)
}نکتهی مهم: نامهای فونتی که چند کلمه دارند (مثل Roboto Mono) باید با آندرلاین (_) ایمپورت شوند، یعنی بهصورت Roboto_Mono.
اگر فونت شما variable نباشد، حتماً باید مقدار weight را مشخص کنید:
import { Roboto } from 'next/font/google'
const roboto = Roboto({
weight: '400',
subsets: ['latin'],
display: 'swap',
})و برای چند وزن یا استایل، از آرایه استفاده میکنید:
const roboto = Roboto({
weight: ['400', '700'],
style: ['normal', 'italic'],
subsets: ['latin'],
display: 'swap',
})فونتهای گوگل بهصورت خودکار subset (زیرمجموعهی حروف مربوط به یک زبان) میشوند تا حجم فایل کاهش پیدا کند. اگر preload فعال باشد (که بهطور پیشفرض هست) و شما subset را مشخص نکنید، Next.js یک هشدار در کنسول نمایش میدهد. برای پروژههای انگلیسیزبان معمولاً کافی است:
const inter = Inter({ subsets: ['latin'] })هر دو تابع next/font/google و next/font/local مجموعهای از آپشنها را میپذیرند که دانستن دقیق آنها برای هر پروژهای ضروری است:
src: مسیر فایل فونت. فقط در next/font/local استفاده میشود و اجباری است. میتواند یک رشته یا آرایهای از آبجکتها با path، weight و style باشد.weight: وزن فونت. اگر فونت variable نباشد، این مقدار اجباری است. برای فونتهای variable میتوانید یک رنج مثل '100 900' بدهید.style: مقدار normal یا italic (برای فونتهای گوگل) یا هر استایل استاندارد دیگر برای فونتهای local.subsets: فقط برای next/font/google. آرایهای از زیرمجموعههای زبانی که باید preload شوند.axes: محورهای اضافهی یک فونت variable (مثل slnt برای Inter)، فقط برای فونتهای گوگل.display: مقدار font-display در CSS، با مقادیر auto, block, swap, fallback یا optional. مقدار پیشفرض swap است.preload: بولین که مشخص میکند فونت preload شود یا نه. پیشفرض true است.fallback: آرایهای از فونتهای جایگزین در صورت شکست لود شدن فونت اصلی.adjustFontFallback: برای فونتهای گوگل یک بولین است که CLS را کاهش میدهد (پیشفرض true)؛ برای فونتهای local میتواند 'Arial', 'Times New Roman' یا false باشد.variable: نام یک متغیر CSS برای استفاده در روش CSS Variables.declarations: فقط برای next/font/local؛ آرایهای از توصیفگرهای @font-face مثل ascent-override.اگر فونت شما از گوگل نیست (مثلاً یک فونت اختصاصی برند)، از next/font/local استفاده میکنید:
import localFont from 'next/font/local'
// فایل فونت میتواند داخل خود پوشه app قرار بگیرد
const myFont = localFont({
src: './my-font.woff2',
display: 'swap',
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={myFont.className}>
<body>{children}</body>
</html>
)
}اگر یک فونت چند فایل مختلف برای وزنها و استایلهای متفاوت دارد، src را بهصورت آرایه میدهید:
const roboto = localFont({
src: [
{ path: './Roboto-Regular.woff2', weight: '400', style: 'normal' },
{ path: './Roboto-Italic.woff2', weight: '400', style: 'italic' },
{ path: './Roboto-Bold.woff2', weight: '700', style: 'normal' },
{ path: './Roboto-BoldItalic.woff2', weight: '700', style: 'italic' },
],
})در پروژههای واقعی معمولاً بیش از یک فونت داریم؛ مثلاً یک فونت برای متن عادی و یک فونت مونواسپیس برای کدها. دو روش استاندارد برای مدیریت این حالت وجود دارد.
روش اول: فایل utility مجزا. یک فایل جدا برای تعریف فونتها میسازید تا هر فونت فقط وقتی واقعاً رندر میشود preload شود:
import { Inter, Roboto_Mono } from 'next/font/google'
export const inter = Inter({
subsets: ['latin'],
display: 'swap',
})
export const roboto_mono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
})سپس هرجا لازم بود، همان کلاس را ایمپورت و استفاده میکنید:
import { roboto_mono } from './fonts'
export default function Page() {
return <h1 className={roboto_mono.className}>My page</h1>
}روش دوم: متغیرهای CSS. بهجای className، از variable استفاده میکنید تا فونت را در قالب یک متغیر CSS تعریف کنید:
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
})
const roboto_mono = Roboto_Mono({
subsets: ['latin'],
variable: '--font-roboto-mono',
display: 'swap',
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html className={`${inter.variable} ${roboto_mono.variable}`}>
<body>{children}</body>
</html>
)
}و در فایل CSS خود:
html {
font-family: var(--font-inter);
}
h1 {
font-family: var(--font-roboto-mono);
}نکته: بیش از حد از فونتهای مختلف استفاده نکنید؛ هر فونت جدید یعنی یک فایل دیگر که کاربر باید دانلود کند.
اگر از Tailwind استفاده میکنید (که در اکثر پروژههای امروزی همینطور است)، ترکیب next/font با متغیرهای CSS بهترین روش است:
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
variable: '--font-inter',
})
const roboto_mono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono',
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html
className={`${inter.variable} ${roboto_mono.variable} antialiased`}
>
<body>{children}</body>
</html>
)
}سپس در فایل CSS اصلی خود (برای Tailwind v4):
@import 'tailwindcss';
@theme inline {
--font-sans: var(--font-inter);
--font-mono: var(--font-roboto-mono);
}اگر هنوز از Tailwind نسخه 3 استفاده میکنید، این تنظیمات در tailwind.config.js انجام میشود:
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
'./app/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {
fontFamily: {
sans: ['var(--font-inter)'],
mono: ['var(--font-roboto-mono)'],
},
},
},
}بعد از این تنظیم، بهراحتی از کلاسهای font-sans و font-mono استفاده میکنید.
next/font سه روش برای اعمال فونت روی المانها ارائه میدهد:
className: سادهترین روش. کلاس تولیدشده را مستقیم روی المان میگذارید: <p className={inter.className}>style: یک آبجکت CSS برمیگرداند که شامل fontFamily و فونتهای fallback است: <p style={inter.style}>اگر پروژهی شما چند فونت مختلف دارد که در بخشهای مختلف استفاده میشوند، بهتر است یک فایل مرکزی برای تعریف تمام فونتها بسازید تا هر فونت فقط یک بار در کل اپلیکیشن instantiate شود:
import { Inter, Lora, Source_Sans_3 } from 'next/font/google'
import localFont from 'next/font/local'
const inter = Inter()
const lora = Lora()
const sourceCodePro400 = Source_Sans_3({ weight: '400' })
const sourceCodePro700 = Source_Sans_3({ weight: '700' })
const greatVibes = localFont({ src: './GreatVibes-Regular.ttf' })
export { inter, lora, sourceCodePro400, sourceCodePro700, greatVibes }سپس در هر جای پروژه، فقط فونت مورد نیاز را ایمپورت میکنید:
import { inter, lora, sourceCodePro700, greatVibes } from '../styles/fonts'
export default function Page() {
return (
<div>
<p className={inter.className}>Hello world using Inter font</p>
<p style={lora.style}>Hello world using Lora font</p>
<p className={sourceCodePro700.className}>Hello with weight 700</p>
<p className={greatVibes.className}>My title in Great Vibes font</p>
</div>
)
}برای دسترسی راحتتر، میتوانید در tsconfig.json یک path alias هم تعریف کنید:
{
"compilerOptions": {
"paths": {
"@/fonts": ["./styles/fonts"]
}
}
}نکتهای که خیلی از توسعهدهندهها اشتباه متوجه میشوند این است: صدا زدن یک تابع فونت بهمعنی preload شدن آن در کل سایت نیست. رفتار دقیق به محل استفاده بستگی دارد:
subsets هنگام preload فعال، که باعث هشدار در کنسول میشود.adjustFontFallback، درحالیکه این آپشن دقیقاً همان چیزی است که Layout Shift را کاهش میدهد.next/font یکی از آن ابزارهایی است که وقتی درست استفاده شود، کاملاً نامرئی است: کاربر هیچوقت متوجه نمیشود که فونت شما بهینه شده، فقط حس میکند سایت شما سریعتر و باثباتتر است. برای اکثر پروژهها، شروع با یک فونت variable از Google Fonts همراه با subsets و display: 'swap' کافی است؛ و برای پروژههای بزرگتر، ترکیب متغیرهای CSS با Tailwind و یک فایل تعریف فونت مرکزی، بهترین معماری ممکن است.