Next.js: تحسين الصور والخطوط
آخر تحديث: 2026-08-26
الصور والخطوط تشكل أكثر من 70% من حجم الصفحة — تحسينها يمكن أن يقلل LCP من 4 ثوان إلى ثانية واحدة.
1. ما ستتعلمه
- السمات الأساسية للمكون (
next/image/<Image>/remotePatterns/sizes/priority/placeholder) - مبادئ تحسين الصور: إعادة ترميز Sharp من جانب الخادم، الترميز التكيفي WebP / AVIF
- تحسين الخطوط باستخدام
next/font(Google Web Fonts،display:swap،size-adjust، التحميل المسبق) - استراتيجيات تحسين الأداء لـ Core Web Vitals (LCP / CLS / INP)
- أمان الصور البعيدة وإهمال
images.domainsوالترحيل عنه
2. قصة حقيقية لمهندس أداء واجهات أمامية
(1) نقطة الألم: صورة واحدة يمكن أن تدمر أداء الصفحة بأكملها
Diana هي مهندسة DevOps في فريق TaskFlow. اكتشفت أن LCP (Largest Contentful Paint) لصفحة Dashboard يصل إلى 4.8 ثانية:
"البانر في أعلى الصفحة هو صورة PNG بحجم 5MB (الحجم الأصلي 4000×3000px)، يتم تحميلها مباشرة
<img src="/banner.png" />. Chrome DevTools يظهر: فك الترميز استغرق 800 مللي ثانية، Cumulative Layout Shift (CLS) كان 0.45، وتحميل الخط حجب العرض لمدة 600 مللي ثانية."
| المشكلة | التأثير | مقياس CWV |
|---|---|---|
| الصورة الأصلية 5MB غير مضغوطة | تحميل 3.2 ثانية | ❌ LCP 4.8 ثانية |
| لا توجد خصائص أبعاد | تخطيط الصفحة يستمر في التحرك | ❌ CLS 0.45 |
| حجب عرض الخط | شاشة بيضاء ممتدة | ❌ FCP 2.1 ثانية |
| صور غير متجاوبة | صور 4K تُحمل على الأجهزة المحمولة أيضاً | ❌ إهدار البيانات |
(2) حل next/image + next/font
استخدم مكون
<Image>لتحسين الصور تلقائياً، وnext/fontلإزالة حجب الخط.
import Image from 'next/image'
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'], display: 'swap' })
export default function Hero() {
return (
<div className={inter.className}>
<Image
src="/banner.webp"
alt="TaskFlow Banner"
width={1200}
height={400}
priority
placeholder="blur"
blurDataURL="data:image/webp;base64,..."
/>
</div>
)
}
(3) العائد
| المقياس | قبل التحسين | بعد التحسين | التحسن |
|---|---|---|---|
| LCP | 4.8 ثانية | 1.2 ثانية | 75% ↓ |
| CLS | 0.45 | 0.02 | 96% ↓ |
| حجم الصورة | 5 MB | 120 KB | 97% ↓ |
| FCP | 2.1 ثانية | 0.8 ثانية | 62% ↓ |
3. السمات الأساسية لمكون next/image
(1) جدول مرجعي للسمات
| السمة | النوع | مطلوبة | الوصف |
|---|---|---|---|
src |
string / StaticImport | ✅ | مسار الصورة أو استيراد ثابت |
width |
number | ✅ (ثابت) | عرض الصورة (px) |
height |
number | ✅ (ثابت) | ارتفاع الصورة (px) |
alt |
string | ✅ | نص بديل (إمكانية الوصول) |
priority |
boolean | ❌ | تحميل مسبق لصورة LCP |
placeholder |
'blur' / 'empty' |
❌ | عنصر نائب للتحميل |
blurDataURL |
string | يتطلب blur |
عنصر نائب ضبابي base64 |
sizes |
string | ❌ | نقاط التوقف المتجاوبة |
fill |
boolean | ❌ | ملء الحاوية الأصل |
quality |
number | ❌ | جودة الضغط (1–100) |
loading |
'lazy' / 'eager' |
❌ | استراتيجية التحميل الكسول |
graph TB
A[<Image src="/photo.jpg"/>] --> B{Next.js Build}
B --> C[إعادة ترميز Sharp من جانب الخادم]
C --> D[نسخة WebP<br/>1200w / 800w / 400w]
C --> E[نسخة AVIF<br/>1200w / 800w / 400w]
D --> F[اختيار المتصفح<br/><picture> تعديل تلقائي]
E --> F
style B fill:#cce5ff
style C fill:#d4edda
style F fill:#fff3cd
(2) مثال على التكوين المتجاوب
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'images.unsplash.com' },
{ protocol: 'https', hostname: 'cdn.taskflow.io', port: '', pathname: '/assets/**' }
],
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 1080, 1200, 1920],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384]
}
}
export default nextConfig
images.domains تم إهماله في Next.js 15 وما بعده؛ يرجى استخدام remotePatterns (الذي يدعم مطابقة مسار بنمط wildcard).
▶ مثال: صورة بعيدة + متجاوبة + عنصر نائب ضبابي
المخرجات:
تم تنفيذ وحدة TypeScript بنجاح.
// components/TeamPhoto.tsx
import Image from 'next/image'
export default function TeamPhoto() {
return (
<Image
src="https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=1200"
alt="صورة فريق TaskFlow"
width={1200}
height={600}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 1200px"
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
priority
className="rounded-lg"
/>
)
}
المخرجات:
يُعرض واجهة مكون TeamPhoto.
sizes يخبر المتصفح بعرض عرض الصورة تحت أحجام منفذ عرض مختلفة، مما يساعد المتصفح على اختيار حجم الصورة الأنسب. إذا لم يتم تعيين هذا، قد ينتهي الأمر بالأجهزة المحمولة بتحميل صورة 1920px أيضاً.
4. placeholder="blur" و blurDataURL
(1) مخططان للعنصر النائب
| الحل | طريقة التوليد | الحجم | السيناريوهات المناسبة |
|---|---|---|---|
| استيراد ثابت لـ "blur" | يُولد تلقائياً بواسطة Next.js | ~2KB | الصور المحلية (import img from './photo.jpg') |
| blurDataURL يدوي | base64 مُولد بأداة | ~200B | الصور البعيدة، الصور المتحركة |
| مكتبة العنصر النائب | يُولد في وقت التشغيل | ~500B | يتطلب URL بعيد ديناميكي |
(2) الاستيراد الثابت ينتج ضبابية تلقائية
// ✅ استيراد ثابت — Next.js يُولد blurDataURL تلقائياً
import teamPhoto from '@/public/team.jpg'
export default function AboutPage() {
return (
<Image
src={teamPhoto}
alt="صورة الفريق"
placeholder="blur" // استخدام الضبابية المُولدة تلقائياً
className="rounded-xl"
/>
)
}
(3) أداة لتوليد معاينات ضبابية للصور البعيدة
# الاستخدام: مكتبة plaiceholder
npm install plaiceholder
// lib/getBlurData.ts — توليد blurURL للصور البعيدة
import { getPlaiceholder } from 'plaiceholder'
export async function getBlurDataURL(src: string) {
try {
const response = await fetch(src)
const buffer = Buffer.from(await response.arrayBuffer())
const { base64 } = await getPlaiceholder(buffer)
return base64
} catch {
return undefined
}
}
▶ مثال: صورة بعيدة ديناميكية + عنصر نائب ضبابي
المخرجات:
تم تنفيذ كود TypeScript بنجاح.
// app/team/page.tsx — قائمة صور الفريق الرمزية + عنصر نائب blur
import Image from 'next/image'
import { getBlurDataURL } from '@/lib/getBlurData'
const members = [
{ name: 'Alice', avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=200' },
{ name: 'Bob', avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=200' },
]
export default async function TeamPage() {
const membersWithBlur = await Promise.all(
members.map(async (m) => ({ ...m, blur: await getBlurDataURL(m.avatar) }))
)
return (
<div style={{ display: 'flex', gap: '1rem' }}>
{membersWithBlur.map((m) => (
<Image
key={m.name}
src={m.avatar}
alt={m.name}
width={100}
height={100}
placeholder="blur"
blurDataURL={m.blur}
className="rounded-full"
/>
))}
</div>
)
}
المخرجات:
يُعرض قائمة بتمرير membersWithBlur، مع عرض كل m.
5. تحسين الخطوط باستخدام next/font
(1) تأثير تحميل الخط على الأداء
graph TB
A[@font-face التقليدي] --> B[حجب العرض<br/>FOIT]
A --> C[إزاحة التخطيط<br/>FOUT]
B --> D[تأخير FCP 300-600 مللي ثانية]
C --> E[CLS 0.1-0.3]
F[next/font] --> G[display:swap<br/>استخدام الخط الاحتياطي فوراً]
F --> H[size-adjust<br/>إزالة إزاحات التخطيط]
F --> I[تحميل مسبق preload<br/>المسار الحرج دون حجب]
F --> J[CSS size-adjust<br/>تداخل مقاييس الخط]
style A fill:#f8d7da
style F fill:#d4edda
| المشكلة | تحميل الخط التقليدي | حل next/font |
|---|---|---|
| FOIT (الخط غير مرئي) | لا يُعرض النص حتى يتم تحميل الخط | display:swap عرض الخط الاحتياطي فوراً |
| CLS (إزاحة التخطيط التراكمية) | تغير الحجم قبل وبعد تحميل الخط | size-adjust يتجاوز مقاييس الخط |
| طلبات إضافية | تحميل تسلسلي لملفات خطوط متعددة | CSS مضمن + تحميل مسبق |
| تأخير Google Fonts | طلبات CDN بطيئة عبر الحدود | يُحمل أثناء البناء، صفر طلبات وقت التشغيل |
(2) خطوط Google المتغيرة
// app/layout.tsx — خطوط Google المتغيرة
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
preload: true,
variable: '--font-inter', // أنماط متغيرات CSS
weight: 'variable' // نطاق الخط المتغير
})
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono'
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ar" className={`${inter.variable} ${robotoMono.variable}`}>
<body style={{ fontFamily: 'var(--font-inter)' }}>
<code style={{ fontFamily: 'var(--font-roboto-mono)' }}>{children}</code>
</body>
</html>
)
}
(3) تعديل حجم الخط (size-adjust)
// خطوط محلية مخصصة + size-adjust لمنع CLS
import localFont from 'next/font/local'
const myFont = localFont({
src: './fonts/CustomFont.woff2',
display: 'swap',
adjustment: {
ascent: 90,
descent: 20,
lineGap: 10,
sizeAdjust: '105%'
}
})
▶ مثال: دمج خطوط متعددة + Tailwind CSS
المخرجات:
يُعرض واجهة مكون Component.
// app/layout.tsx — خطوط الأعمال + خط الكود + Tailwind
import { Inter, JetBrains_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-sans',
display: 'swap'
})
const jetbrainsMono = JetBrains_Mono({
subsets: ['latin'],
variable: '--font-mono',
display: 'swap'
})
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ar" className={`${inter.variable} ${jetbrainsMono.variable}`}>
<body className="font-sans">{children}</body>
</html>
)
}
المخرجات:
يُعرض واجهة مكون RootLayout.
/* tailwind.config.ts */
import type { Config } from 'tailwindcss'
export default {
theme: {
extend: {
fontFamily: {
sans: ['var(--font-sans)'],
mono: ['var(--font-mono)']
}
}
}
} satisfies Config
6. مبادئ تحسين الصور: Sharp + WebP/AVIF
(1) خط أنابيب تحسين Sharp
graph LR
A[الصورة المصدر<br/>PNG/JPEG 5MB] --> B[معالجة Sharp من جانب الخادم]
B --> C[تغيير الحجم<br/>1200w / 800w / 400w]
B --> D[ضغط الجودة<br/>جودة 75%]
B --> E[تحويل التنسيق]
E --> F[WebP<br/>~80% تقليل الحجم↓]
E --> G[AVIF<br/>~90% تقليل الحجم↓]
F --> H[<picture> تفاوض تلقائي<br/>المتصفح: اختيار أفضل تنسيق]
style B fill:#d4edda
style H fill:#cce5ff
| التنسيق | نسبة الضغط (مقابل JPEG) | دعم المتصفح | سرعة فك الترميز |
|---|---|---|---|
| JPEG | المعيار | 100% | الأسرع |
| WebP | ~30% حجم ملف أقل | 96% | سريع إلى حد ما |
| AVIF | ~50% تقليل حجم الملف | 93% | أبطأ |
| HEIC | ~50% حجم ملف أقل | Safari فقط | أسرع |
(2) تكوين جودة الصورة الشاملة
// next.config.ts — إعدادات تحسين الصورة الشاملة
const nextConfig = {
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 1080, 1200, 1920],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
minimumCacheTTL: 60 * 60 * 24 * 30, // 30 يوماً في الذاكرة المؤقتة
dangerouslyAllowSVG: false,
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;"
}
}
▶ مثال: اختبار جودة الصورة وحجمها
المخرجات:
تم تنفيذ وحدة TypeScript بنجاح.
// components/ImageTest.tsx — مقارنة مستويات الجودة المختلفة لنفس الصورة
import Image from 'next/image'
export default function ImageTest() {
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '1rem' }}>
<div>
<h3>الجودة=50</h3>
<Image src="/test.jpg" alt="q50" width={400} height={300} quality={50} />
</div>
<div>
<h3>الجودة=75 (الافتراضي)</h3>
<Image src="/test.jpg" alt="q75" width={400} height={300} quality={75} />
</div>
<div>
<h3>الجودة=100</h3>
<Image src="/test.jpg" alt="q100" width={400} height={300} quality={100} />
</div>
</div>
)
}
المخرجات:
يُعرض: الجودة=50
نص مرئي: الجودة=50 | الجودة=75 (الافتراضي) | الجودة=100
7. التحسين العملي لـ Core Web Vitals
(1) المقاييس الرئيسية الثلاثة لـ CWV
| المقياس | الحد الأقصى للدرجة | الفجوة | إجراءات التحسين |
|---|---|---|---|
| LCP (Largest Contentful Paint) | ≤2.5 ثانية | >4.0 ثانية | تحميل مسبق لصور LCP + ضغط + CDN |
| CLS (Cumulative Layout Shift) | ≤0.1 | >0.25 | أبعاد صورة ثابتة + تعديل حجم الخط |
| INP (Interaction to Next Paint) | ≤200 مللي ثانية | >500 مللي ثانية | تقليل تنفيذ JS + تقسيم الكود |
(2) قائمة تحقق تحسين صورة LCP
// ✅ إعدادات صورة LCP الصحيحة
export default function HeroBanner() {
return (
<Image
src="/hero-banner.webp"
alt="TaskFlow Hero"
width={1440}
height={600}
priority // إخبار Next.js بتحميل هذه الصورة مسبقاً
quality={85}
placeholder="blur"
sizes="100vw"
style={{ width: '100%', height: 'auto' }} // متجاوب
/>
)
}
(3) إزالة CLS الناتج عن الخط
// layout.tsx — حل كامل لإزالة CLS الناتج عن الخط
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
adjustFontFallback: true, // Next.js يحسب size-adjust تلقائياً
fallback: ['system-ui', 'sans-serif']
})
▶ مثال: مكون مراقبة CWV
المخرجات:
يُعرض واجهة مكون Component.
// components/WebVitals.tsx — إرسال CWV إلى منصة التحليلات
'use client'
import { useReportWebVitals } from 'next/web-vitals'
export default function WebVitalsReporter() {
useReportWebVitals((metric) => {
console.log(metric) // عرض أثناء التطوير
// الإبلاغ من بيئة الإنتاج إلى Analytics API
if (process.env.NODE_ENV === 'production') {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
id: metric.id
})
navigator.sendBeacon('/api/analytics', body)
}
})
return null
}
المخرجات:
يُعرض واجهة مكون WebVitalsReporter.
// app/layout.tsx — تركيب المراقبة
import WebVitalsReporter from '@/components/WebVitals'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<body>
{children}
<WebVitalsReporter />
</body>
</html>
)
}
8. مثال كامل: تحسين شامل لصور الصفحة الرئيسية والخطوط و CWV
// app/layout.tsx — التخطيط الجذري، الخطوط الشاملة + الصور + مراقبة Vitals
import { Inter, Noto_Sans_SC } from 'next/font/google'
import WebVitalsReporter from '@/components/WebVitals'
import type { Metadata } from 'next'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
display: 'swap',
adjustFontFallback: true
})
const notoSansSC = Noto_Sans_SC({
subsets: ['latin'],
weight: 'variable',
variable: '--font-noto',
display: 'swap',
adjustFontFallback: true
})
export const metadata: Metadata = {
title: 'TaskFlow - منصة تعاون الفريق',
description: 'TaskFlow تساعد أكثر من 10,000 فريق عالي الفعالية على التعاون'
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ar" className={`${inter.variable} ${notoSansSC.variable}`}>
<body style={{ fontFamily: 'var(--font-inter), var(--font-noto), sans-serif' }}>
{children}
<WebVitalsReporter />
</body>
</html>
)
}
// app/page.tsx — الصفحة الرئيسية Hero + تحسين LCP
import Image from 'next/image'
import Link from 'next/link'
export default function HomePage() {
return (
<div>
{/* منطقة Hero — صورة LCP */}
<div style={{ position: 'relative', width: '100%', height: 500 }}>
<Image
src="https://images.unsplash.com/photo-1522071820081-009f0129c71c?w=1920&q=85"
alt="تعاون فريق TaskFlow"
fill
priority
sizes="100vw"
style={{ objectFit: 'cover' }}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
/>
<div style={{ position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ textAlign: 'center', color: '#fff' }}>
<h1 style={{ fontSize: 'clamp(2rem, 5vw, 4rem)' }}>TaskFlow</h1>
<p style={{ fontSize: '1.25rem' }}>أكثر من 10,000 فريق يستخدمون منصة التعاون</p>
<Link href="/signup" style={{ display: 'inline-block', padding: '0.75rem 2rem', background: '#4f46e5', color: '#fff', borderRadius: 8 }}>
ابدأ مجاناً
</Link>
</div>
</div>
</div>
{/* منطقة الميزات — تحميل كسول للصور */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '2rem', padding: '4rem 2rem' }}>
{features.map((feature) => (
<div key={feature.title}>
<Image
src={feature.icon}
alt={feature.title}
width={48}
height={48}
loading="lazy"
/>
<h3>{feature.title}</h3>
<p>{feature.description}</p>
</div>
))}
</div>
</div>
)
}
const features = [
{ title: 'إدارة المشاريع', description: 'لوحة إعلانات + مخطط جانت', icon: '/icons/project.svg' },
{ title: 'تعاون فوري', description: 'تحرير متزامن من قبل عدة مستخدمين', icon: '/icons/team.svg' },
{ title: 'تحليل ذكي', description: 'مدعوم برؤى AI', icon: '/icons/analytics.svg' },
]
❓ أسئلة شائعة
next/image و fill في width/height؟fill يجعل الصورة تملأ حاويتها الأصل (التي يجب أن يكون لديها position: relative) دون تحديد حجم ثابت؛ يُستخدم بالتزامن مع objectFit. العرض والارتفاع الثابتان يتطلبان تحديد أبعاد الصورة صراحة لحساب المساحة التي تشغلها ومنع CLS.placeholder="blur"؟blurDataURL يدوياً (صورة مصغرة ضبابية مشفرة بـ base64)، والتي يمكن توليدها باستخدام مكتبة plaiceholder أو أداة عبر الإنترنت (مثل https://png-pixel.com لتوليد صورة ضبابية 4×4).display=swap و next/font في Google Fonts هما نفس display:swap؟&display=swap إلى وسم <link> واستبداله فقط بعد انتهاء تحميل الخط. next/font يُحمل ملف الخط أثناء وقت البناء ويضمّنه في الصفحة عبر CSS، مما ينتج عنه صفر طلبات وقت تشغيل، مع توليد size-adjust تلقائياً لإزالة CLS.priority مُعينة على صورة LCP. Next.js يضيف <link rel="preload"> فقط للصور التي لديها priority. بالإضافة إلى ذلك، تكوين remotePatterns غير الصحيح يمكن أن يتسبب أيضاً في فشل التحسين.<picture>. AVIF يوفر ضغطاً أعلى لكنه أبطأ في فك الترميز، مما يجعله مناسباً للصور غير LCP؛ WebP لديه توافق أفضل (96%) ومناسب لصور LCP. Next.js يُولد كلا التنسيقين افتراضياً.📖 ملخص
next/imageيوفر تحسيناً تلقائياً: إعادة ترميز Sharp، ترميز تكيفي WebP/AVIF، صور متجاوبة متعددة الأحجام، وتحميل كسولremotePatternsيستبدلimages.domainsالمُهمل ويدعم المطابقة الآمنة لأنماط المسارplaceholder="blur"معblurDataURLلإزالة الوميض الفارغ عند تحميل الصورnext/fontيُحمل خطوط Google أثناء البناء لإزالة FOIT و FOUT، ويضبط مقاييس الخط تلقائياً لمنع CLS- صور LCP يجب أن تُعين بـ
priorityللتحميل المسبق؛ الصور غير الظاهرة في الشاشة الأولى يجب أن تستخدمloading="lazy"للتحميل الكسول - تحسين Core Web Vitals: ضغط الصور (LCP)، الأبعاد الثابتة (CLS)، وتقسيم كود JavaScript (INP)
📝 تمارين
-
تمرين أساسي (⭐): قم بتكوين
next/imageفي المشروع، استخدمremotePatternsلتمكين مصدر صور Unsplash، ونفذ مكون صورة متجاوب (يشملsizesوplaceholder="blur"). -
تمرين متقدم (⭐⭐): قارن تأثير جودة الصورة المختلفة (50/75/100) والتنسيقات المختلفة (JPEG/WebP/AVIF) على LCP للصفحة، وسجل حجم الملف ووقت التحميل لكل تركيبة، وحدد التكوين الأمثل.
-
تحد (⭐⭐⭐): قم ببناء حل تحسين CWV كامل:
next/fontبتكوين خطين متغيرين (خط النص الأساسي + خط الكود)، تكوين صور LCP بـpriority+ عناصر نائبةblur،useReportWebVitalsللإبلاغ عن المقاييس إلى API مخصص، وتحقيق درجة Lighthouse Performance ≥ 90 في النهاية.