پروژه عملی کاهش حجم تصاویر یک سایت قدیمی

پروژه عملی کاهش حجم تصاویر یک سایت قدیمی با داریوش حقیقی! چند روزه یک پروژه جدید دستم رسیده که کار روی یک سایت قدیمی و پر بازدید هست و بهینه سازیش. کاهش حجم تصاویر یک سایت وردپرس قدیمی با Optimize کردن چند فایل تازه تفاوت زیادی دارد. وقتی با آرشیو واقعی یک مؤسسه آموزشی روبه‌رو هستیم، باید نام فایل‌ها، ساختار wp-content/uploads، فرمت، ابعاد، مسیرهای فارسی و Unicode و امکان بازگشت به نسخه اصلی حفظ شود؛ در عین حال خروجی باید آن‌قدر کوچک‌تر باشد که جایگزینی و آپلود دوباره آن واقعاً ارزش عملی داشته باشد.
موردی که مهمه این فاز دوم بهینه سازی بوده و در فاز اول حدود 50 درصد حجم ومدیا های سایت کم شده مخصوصا اینکه از فرمت های غیر استاندارد و حجیم استفاده شده بود در اکثر موارد و سایت حدود 25 هزار فایل مدیا دارد! با توجه به مفصل بودن بحث در این پروژه صرفا فازدوم پروژه را براتون بررسی میکنم.
در فاز دوم پروژه کار از دانلود 861 فایل شروع شد که با افزونه های رایج optimize شده بودند ولی عملکردش خوب نبود و روال بررسی دستی انجام شد و به ساخت یک Pipeline کامل برای بهینه‌سازی تصاویر رسید. Run نهایی روی 860 تصویر قابل پردازش انجام شد و با ترکیب FFmpeg برای JPG و WebP، OxiPNG برای PNG و ImageMagick به‌عنوان Palette fallback، تعداد 850 تصویر کوچک‌تر شد، 10 فایل هیچ سود حجمی نداشت و هیچ فایل پردازشی Fail نشد. حجم کل Corpus از 130.52 مگابایت به 106.69 مگابایت رسید؛ یعنی 23.82 مگابایت یا 18.25 درصد کاهش.ارزش اصلی این پروژه فقط عدد نهایی نیست. در مسیر کار، FFmpeg برای تقریباً تمام PNGها نتیجه مطلوب نداد، ImageMagick در معماری brute-force بیش از حد کند شد، اجبار PNG32 بعضی Candidateها را بزرگ‌تر کرد و حتی یک Regression در Reporting بعد از پردازش کامل 860 فایل باعث Crash انتهای Run شد. این مقاله مسیر واقعی مسئله، آزمون‌ها، شکست‌ها، اصلاح معماری، Validation، Restore، گزارش‌گیری و تصمیم نهایی برای حذف فایل‌های کم‌ارزش از نظر Re-upload را مستند می‌کند.برای شناخت عمیق‌تر خود فرمت‌ها نیز بررسی فرمت‌های تصویری و بهترین انتخاب در DJH.ir مکمل این Case Study است.
جدول مطالب

فرمت‌های رایج تصویر

قبل از ورود به خود پروژه، باید یک نکته پایه روشن باشد: فرمت تصویر فقط پسوند فایل نیست. JPEG، PNG، WebP و AVIF روش‌های متفاوتی برای ذخیره داده تصویری دارند و به همین دلیل یک تنظیم یا Encoder واحد الزاماً روی همه آن‌ها نتیجه مشابهی نمی‌دهد. همین تفاوت در این پروژه به‌صورت عملی دیده شد؛ مسیری که برای JPG مفید بود، برای بیشتر PNGها تقریباً هیچ سودی ایجاد نکرد.

اگر می‌خواهید قبل از ادامه مقاله ساختار، کاربرد و تفاوت این فرمت‌ها را عمیق‌تر بشناسید، راهنمای بررسی فرمت‌های تصویری و بهترین انتخاب در DJH.ir مرجع کامل‌تری برای خود Formatهاست. در این مقاله فقط به اندازه‌ای وارد جزئیات می‌شویم که منطق تصمیم‌های پروژه روشن شود.

فرمتمزیت اصلیمحدودیت مهم
JPEG / JPGحجم مناسب برای عکس‌های واقعی و امکان فشرده‌سازی Lossy مؤثرشفافیت ندارد و Re-encode مکرر می‌تواند افت کیفیت را بیشتر کند
PNGLossless، مناسب شفافیت، Screenshot، نمودار و گرافیکبرای عکس‌های پیچیده معمولاً بزرگ‌تر است و نیاز به Optimizer مناسب دارد
WebPپشتیبانی از Lossy، Lossless و Transparency با حجم مناسبتبدیل همه فایل‌های قدیمی به WebP می‌تواند Workflow و URLهای موجود را تغییر دهد
AVIFفشرده‌سازی بسیار قوی در بسیاری از تصاویر و پشتیبانی از قابلیت‌های مدرنEncode سنگین‌تر است و مهاجرت یک سایت قدیمی به آن باید جداگانه Validate شود

JPEG برای عکس‌ها

JPEG معمولاً برای عکس‌های دوربین، تصاویر پرجزئیات و محتوایی که میلیون‌ها رنگ دارد مناسب است. این فرمت غالباً با فشرده‌سازی Lossy استفاده می‌شود؛ یعنی برای کاهش حجم بخشی از اطلاعات تصویری حذف می‌شود. نکته مهم این است که «Lossy» لزوماً به معنی تصویر بد نیست؛ اگر Quality درست انتخاب شود، می‌توان حجم را به‌طور محسوسی کم کرد بدون اینکه افت کیفیت در استفاده واقعی آزاردهنده باشد.

PNG برای گرافیک و شفافیت

PNG ذاتاً انتخاب متفاوتی است. مسیر معمول آن Lossless است و برای لوگو، متن، Screenshot، نمودار، Illustration و تصاویر دارای Alpha کاربرد زیادی دارد. به همین دلیل استفاده از همان روشی که JPEG را خوب کوچک می‌کند، الزاماً برای PNG نتیجه نمی‌دهد. در پروژه حاضر همین تفاوت باعث شد Engine مخصوص PNG تغییر کند.

WebP و AVIF

WebP و AVIF فرمت‌های مدرن‌تری هستند و در بسیاری از سناریوها می‌توانند فایل کوچک‌تری تولید کنند. با این حال هدف این پروژه «تغییر فرمت» نبود؛ Requirement اصلی حفظ نام، Extension و ساختار فعلی بود. بنابراین WebPهای موجود Optimize شدند، اما JPG و PNG به WebP یا AVIF تبدیل نشدند. تبدیل فرمت باید به‌عنوان یک پروژه Migration جدا، همراه با بررسی Browser، WordPress، Cache، URL و Thumbnailها انجام شود.

مسئله یک سایت قدیمی

در یک سایت تازه، معمولاً می‌توان از همان ابتدا سیاست مشخصی برای ابعاد، فرمت و فشرده‌سازی تصاویر تعریف کرد. اما سایت قدیمی ممکن است سال‌ها فایل با تنظیمات متفاوت جمع کرده باشد. بعضی تصاویر از قبل خوب فشرده شده‌اند، بعضی PNG هستند، بعضی JPG، بخشی WebP شده‌اند و نام فایل‌ها یا مسیرها نیز ممکن است فارسی باشند. اگر قرار باشد این آرشیو دوباره روی همان WordPress قرار گیرد، تغییر بی‌برنامه نام یا ساختار پوشه می‌تواند مشکل جدیدی ایجاد کند.

برای این پروژه چند محدودیت از ابتدا غیرقابل تغییر بود. هدف Resize یا مهاجرت همه تصاویر به WebP و AVIF نبود؛ هدف این بود که همان فایل‌ها تا جای ممکن سبک‌تر شوند و قابلیت Rollback نیز از بین نرود.

محدودیتتصمیم پروژهدلیل
ابعادبدون تغییرجلوگیری از تغییر رفتار صفحات
فرمت و نامحفظ شودسازگاری با ساختار موجود
نسخه اصلی.original.*Rollback و Retest
جایگزینیفقط Candidate معتبر و کوچک‌ترجلوگیری از Regression
مسیرحفظ ساختار uploadsآماده‌سازی برای Re-upload

چرا فقط افزونه کافی نبود؟

افزونه‌های WordPress برای بسیاری از سایت‌ها انتخاب خوبی هستند، مخصوصاً وقتی هدف Optimize کردن Media Library فعال و تصاویر جدید باشد. اما مسئله این پروژه متفاوت بود: یک Corpus موجود باید خارج از WordPress، با کنترل دقیق روی فایل‌سیستم، Backup، Candidate، Validation، گزارش‌گیری و امکان اجرای مجدد پردازش می‌شد. بنابراین ابزار اصلی یک Workflow مستقل PowerShell بود، نه یک Plugin داخل پنل مدیریت.

این به معنی برتری مطلق روش اسکریپتی نیست. اگر سایت فعال است و می‌خواهید تصاویر جدید به‌صورت خودکار Optimize شوند، Plugin یا Image CDN ممکن است عملی‌تر باشد. این پروژه برای یک عملیات Batch روی آرشیو موجود طراحی شد.

معماری امن پروژه

قانون مرکزی Pipeline ساده بود: هیچ Encoder حق نداشت مستقیم فایل اصلی را نابود کند. ابتدا یک Candidate جدا ساخته می‌شد، سپس اعتبار آن بررسی می‌شد و فقط اگر خروجی معتبر و کوچک‌تر از Source بود، نسخه اصلی با نام .original.ext نگه‌داری و Candidate با نام اصلی جایگزین می‌شد.

Source
  ↓
Create Candidate
  ↓
Validate Candidate
  ↓
Compare Size
  ↓
Smaller + Valid? ── No ──> Keep Source
  │
 Yes
  ↓
Source -> .original.ext
Candidate -> Original Filename
  ↓
Log Result

Candidate و Validation

ساخته شدن فایل خروجی به‌تنهایی نشانه موفقیت نیست. Candidate باید واقعاً به‌عنوان تصویر قابل خواندن باشد. در این پروژه FFprobe برای Validation فرمت‌هایی که با FFmpeg پردازش می‌شدند به کار رفت و برای مسیر PNG نیز خروجی ابزار تخصصی قبل از پذیرش نهایی بررسی می‌شد.

Backup و Rollback

Backup فقط برای روز مبادا نبود. Restore بخشی از روش Benchmark شد. وقتی Engine یا تنظیمات تغییر می‌کرد، باید دوباره از Source واقعی شروع می‌کردیم؛ مقایسه یک Encoder جدید با خروجی قبلاً Re-encodeشده نتیجه قابل اتکایی نمی‌دهد.

قانون کوچک‌تر بودن

اگر Candidate حتی یک تصویر کاملاً معتبر بود اما حجم بیشتری داشت، Source حفظ می‌شد. این وضعیت Error محسوب نمی‌شد؛ یک فایل ممکن است از قبل به‌اندازه کافی فشرده باشد و Re-encode آن هیچ مزیتی ایجاد نکند.

جمع‌آوری تصاویر

مرحله اول پروژه ساخت Downloader بود. 861 URL مستقیماً داخل اسکریپت قرار گرفت تا اجرای نهایی به فایل Excel یا منبع جانبی وابسته نباشد. مسیر هر URL بعد از wp-content/uploads/ استخراج و همان ساختار روی دیسک ساخته شد.

wp-content\uploads\2025\05\3.webp

این تصمیم برای Re-upload مهم بود، چون خروجی صرفاً یک پوشه تخت از تصاویر نبود؛ ساختار واقعی WordPress حفظ می‌شد. Run Downloader با 861 دانلود موفق، صفر Existing و صفر Failure تمام شد.

URLs        : 861
Downloaded  : 861
Existing    : 0
Failed      : 0

اختلاف 861 و 860

پس از انتقال فایل‌ها به Root عملی پروژه، Optimizer تعداد 860 تصویر قابل پردازش پیدا کرد. اطلاعات موجود علت دقیق اختلاف یک فایل را ثبت نکرده است. بنابراین نسبت دادن آن به فایل خراب، فرمت خاص یا خطای دانلود درست نیست؛ تنها چیزی که با Evidence پروژه می‌توان گفت این است که 861 URL دانلود شد و Corpus پردازش‌شده Optimizer شامل 860 تصویر بود.

روش‌های کاهش حجم

کاهش حجم تصویر یک تکنیک واحد نیست. در این پروژه چند روش متفاوت استفاده شد و هرکدام نقش مشخصی داشتند. بعضی روش‌ها کاملاً Lossless بودند، بعضی عمداً از فشرده‌سازی Lossy استفاده می‌کردند و برای PNG نیز یک مسیر Palette با Quality Guard آزمایش شد. انتخاب روش بر اساس Format، نوع تصویر و نتیجه واقعی Candidate انجام می‌شد.

فشرده‌سازی Lossless

در فشرده‌سازی بدون اتلاف یا Lossless، داده تصویری قابل بازسازی باقی می‌ماند و هدف این است که همان تصویر با ساختار فشرده‌تری ذخیره شود. OxiPNG در مسیر اصلی PNG پروژه دقیقاً همین نقش را داشت. مزیت Lossless این است که افت بصری ایجاد نمی‌کند؛ محدودیتش این است که اگر Source از قبل خوب Optimize شده باشد، فضای زیادی برای کاهش حجم باقی نمی‌ماند.

فشرده‌سازی Lossy

JPEG و WebP در این پروژه با تنظیمات Lossy پردازش شدند. در این روش Encoder بخشی از اطلاعات کم‌اهمیت‌تر را حذف می‌کند تا فایل کوچک‌تر شود. هرچه تنظیمات تهاجمی‌تر باشند، معمولاً حجم کمتر و ریسک Artifact بیشتر می‌شود. به همین دلیل مقدار qscale یا Quality یک «عدد جادویی» برای همه سایت‌ها نیست و باید روی Sample واقعی همان Corpus آزمایش شود.

کاهش رنگ با Palette

برای تعدادی از PNGها، Candidate پالت 256 رنگ نیز ساخته شد. این روش می‌تواند برای Screenshot، نمودار، آیکن یا گرافیک‌هایی که تعداد رنگ واقعی محدودی دارند کاهش حجم خوبی ایجاد کند. اما چون تبدیل تصویر Full-color به Palette ممکن است اختلاف بصری ایجاد کند، در این پروژه Candidate پالت فقط در صورت عبور از RMSE Guard اجازه پذیرش داشت.

حذف داده‌های غیرضروری

بخشی از کاهش حجم PNG می‌تواند از Optimization ساختار داخلی، فیلترها و حذف Metadata غیرضروری به دست بیاید. تنظیم safe برای Strip در مسیر OxiPNG انتخاب شد تا پروژه به‌جای حذف تهاجمی اطلاعات، روی داده‌هایی تمرکز کند که برای نمایش تصویر لازم نیستند. این بخش نیز باید متناسب با نیاز سایت تنظیم شود؛ مثلاً اگر Metadata خاصی برای Workflow دیگری لازم است نباید کورکورانه حذف شود.

بدون Resize

یکی از رایج‌ترین روش‌های کاهش حجم، کم کردن Resolution است؛ اما در این پروژه عمداً Resize انجام نشد. دلیل آن Constraint پروژه بود: ابعاد فایل باید دقیقاً حفظ می‌شد. در سایت دیگر، اگر تصاویر بسیار بزرگ‌تر از اندازه واقعی نمایش باشند، Resize صحیح می‌تواند حتی از Re-encoding مؤثرتر باشد.

بدون تغییر فرمت

روش دیگر، تبدیل JPEG یا PNG به WebP یا AVIF است. این کار در پروژه حاضر انجام نشد چون Extension نهایی باید همان Extension اولیه باقی می‌ماند. بنابراین Saving گزارش‌شده نتیجه Optimize کردن فایل‌ها در همان Format است، نه Migration به یک Format جدید. برای شناخت تفاوت این دو رویکرد، مقاله راهنمای فرمت‌های تصویری مکمل مناسبی است.

از نظر فلسفه پردازش Batch، این پروژه شباهت زیادی به پروژه عملی کاهش حجم آرشیو موسیقی با FFmpeg دارد: Source حفظ می‌شود، تنظیمات روی داده واقعی سنجیده می‌شوند و نتیجه بر اساس حجم کل Corpus ارزیابی می‌شود، نه یک نمونه انتخابی.

اولین Run

نسخه اولیه Optimizer تقریباً همه فرمت‌ها را از مسیر FFmpeg عبور می‌داد. معماری Candidate و Backup از همان ابتدا ایمن بود، اما نتیجه حجمی نشان داد انتخاب یک Engine واحد برای همه فرمت‌ها تصمیم مناسبی نیست.

Images found       : 860
Optimized          : 465
No size benefit    : 395
Already processed  : 0
Failed             : 0
Before optimized   : 48.67 MB
After optimized    : 43.33 MB
Total saved        : 5.33 MB (11.0%)

در ظاهر 11 درصد Saving بد نبود، اما این Summary یک محدودیت مهم داشت: Before optimized فقط حجم فایل‌هایی را جمع می‌کرد که واقعاً Replace شده بودند، نه کل 860 تصویر. بنابراین برای تصمیم‌گیری درباره کل Corpus کافی نبود. در نسخه‌های بعدی دو شاخص مستقل برای «فایل‌های Replaceشده» و «کل Corpus» اضافه شد.

مشکل PNG

برای پیدا کردن منشأ 395 مورد No size benefit، گزارش به تفکیک فرمت توسعه پیدا کرد. همین کار مسیر پروژه را عوض کرد. JPG و WebP تا حد قابل قبولی با FFmpeg کوچک می‌شدند، اما PNG تقریباً هیچ سودی نداشت.

فرمتRun اولیهبرداشت
JPG442 از 591FFmpeg مؤثر بود
PNG4 از 246Engine نامناسب برای این Corpus
WebP19 از 23نتیجه قابل قبول

تحلیل بر اساس فرمت

از 246 PNG فقط چهار فایل Candidate کوچک‌تر داشتند و 242 مورد هیچ Benefit حجمی ایجاد نکردند. این داده مهم‌تر از درصد Saving همان چهار فایل موفق بود. اگر فقط میانگین Saving فایل‌های Replaceشده دیده می‌شد، ممکن بود نتیجه اشتباه گرفته شود.

چرا درصد PNG گمراه‌کننده بود؟

Summary اولیه برای چهار PNG موفق Saving بالایی نشان می‌داد، اما سؤال درست این نبود که «فایل‌های موفق چند درصد کوچک شدند؟». سؤال مهم این بود که «چه سهمی از Corpus با این روش واقعاً قابل بهینه‌سازی است؟». وقتی 242 فایل از 246 مورد هیچ Candidate بهتری ندارند، باید خود معماری بررسی شود.

Restore قبل از Retest

بعد از هر تغییر جدی در Engine، آزمایش باید روی Originalها تکرار می‌شد. برای همین اسکریپت Restore تمام فایل‌های *.original.* را پیدا می‌کرد، نسخه Optimizeشده فعلی را حذف می‌کرد و Source اصلی را به نام اولیه برمی‌گرداند.

Backups found : 465
Restored      : 465
Failed        : 0

این مرحله از نظر فنی ساده به نظر می‌رسد، اما برای Benchmark اهمیت زیادی دارد. Re-encode کردن خروجی قبلی می‌تواند هم کیفیت را تغییر دهد و هم مقایسه حجم را منحرف کند. اگر قرار است دو روش Compression مقایسه شوند، هر دو باید تا حد امکان از یک Source یکسان شروع کنند.

آزمون ImageMagick

بعد از مشخص شدن ضعف مسیر FFmpeg برای PNGهای این Corpus، ImageMagick به‌عنوان گزینه بعدی آزمایش شد. ایده اولیه این بود که چند Candidate Lossless با Strategyهای مختلف ساخته شود و در کنار آن یک Candidate پالت 256 رنگ نیز تولید شود. Candidate پالت فقط در صورتی می‌توانست برنده شود که اختلاف تصویری آن از Quality Guard مبتنی بر RMSE عبور کند.

مشکل PNG32

در یکی از نسخه‌ها خروجی Lossless با PNG32: نوشته می‌شد. این اجبار می‌توانست PNGهایی را که ذاتاً Palette یا Grayscale بودند به RGBA 32-bit تبدیل کند و به‌جای کاهش حجم، فایل بزرگ‌تری بسازد. بعد از مشاهده این رفتار، اجبار PNG32 حذف شد و Color Type اصلی تا حد امکان حفظ شد.

مشکل سرعت

اصلاح PNG32 مشکل اصلی Performance را حل نکرد. معماری brute-force برای هر PNG چند Process مستقل ImageMagick اجرا می‌کرد: Candidateهای مختلف، Compare و RMSE. روی Batch شامل 246 PNG، هزینه Process launch و تکرار Encode به گلوگاه تبدیل شد.

[22:37:28] [FILE ] 125/860 | ...\10-5.png
[22:37:32] [PNG  ] Winner | mode=LOSSLESS_F0_S0 | candidate=109.7 KB | RMSE=0
[22:37:32] [KEEP ] No benefit | source=108.4 KB candidate=109.7 KB

[22:37:39] [FILE ] 128/860 | ...\12-3.png
[22:37:48] [PNG  ] Winner | mode=LOSSLESS_F0_S0 | candidate=115.6 KB | RMSE=0

چرا کنار گذاشته شد؟

ImageMagick از پروژه حذف نشد؛ نقش آن تغییر کرد. مسئله این نبود که ImageMagick نمی‌تواند PNG را پردازش کند، بلکه استفاده از آن به‌عنوان موتور brute-force اصلی برای این Batch هزینه زمانی نامناسبی داشت. راه‌حل بهتر این بود که مسیر معمول PNG به یک Optimizer تخصصی واگذار شود و ImageMagick فقط در سناریویی که واقعاً مزیت اضافه دارد اجرا شود.

برای آشنایی عمیق‌تر با خود ابزار، مقاله آموزش ImageMagick صفر تا صد جزئیات بیشتری درباره پردازش تصویر و فرمان‌های آن دارد.

مهاجرت به OxiPNG

موتور اصلی PNG در معماری نهایی به OxiPNG 10.1.1 تغییر کرد. OxiPNG در مسیر معمول یک Candidate Lossless می‌ساخت و ImageMagick فقط برای Palette fallback وارد می‌شد. به این ترتیب هزینه چندین Encode عمومی برای هر فایل حذف شد، ولی امکان پیدا کردن Candidate پالت کم‌حجم‌تر همچنان باقی ماند.

مسیر Lossless

OxiPNG با Optimization Level مشخص روی نسخه موقت کار می‌کرد. اگر Candidate معتبر و کوچک‌تر از Source بود، می‌توانست به‌عنوان Winner پذیرفته شود. برای این مسیر، تغییر بصری وجود نداشت و RMSE معادل صفر بود.

Palette fallback

در کنار Candidate Lossless، امکان تبدیل کنترل‌شده به Palette 256 رنگ نیز وجود داشت. این مسیر Lossy محسوب می‌شود و برای همین شرط RMSE داشت. فقط Candidateای پذیرفته می‌شد که مقدار Normalized RMSE از Guard پروژه یعنی 0.012 بیشتر نباشد.

Preflight ابزار

OxiPNG در زمان اجرای Script نصب نمی‌شد. Dependency از قبل در یک مسیر ثابت قرار داشت و Optimizer فقط وجود و نسخه مورد انتظار را Preflight می‌کرد. این تصمیم باعث می‌شد Run پردازشی از Download یا Installer جدا بماند.

& "D:\AI\#tools\oxipng\oxipng.exe" --version

نسخه ثبت‌شده در پروژه:

oxipng 10.1.1

تنظیمات نهایی

برای همین Corpus یک پروفایل تهاجمی انتخاب شد. این اعداد حاصل تصمیم همین پروژه هستند و نباید بدون Benchmark روی سایت دیگری کپی شوند. عکس پرتره، Screenshot، نمودار و Illustration ممکن است به تنظیمات متفاوتی نیاز داشته باشند.

# JPEG
$JpegQScale = 8

# WebP
$WebpQuality = 65

# AVIF
$AvifCrf = 40

# OxiPNG
$OxiPngOptimizationLevel = '3'
$OxiPngStripMode = 'safe'
$OxiPngAlphaOptimization = $true
$OxiPngInterlace = 'off'
$OxiPngThreads = 4
$OxiPngTimeoutSec = 5

# PNG Palette
$PngEnablePaletteFallback = $true
$PngPaletteColors = 256
$PngPaletteDither = 'FloydSteinberg'
$PngPaletteMaxNormalizedRmse = 0.012
$PngPaletteCompressionLevel = 9

JPEG و WebP

در JPEG مقدار qscale بالاتر به معنی فشرده‌سازی تهاجمی‌تر است. در WebP برعکس، Quality پایین‌تر معمولاً فایل کوچک‌تری تولید می‌کند. FFmpeg برای این دو فرمت در Corpus پروژه نتیجه قابل استفاده‌ای داشت. جزئیات کامل‌تر Encoderها و Syntax را می‌توان در آموزش FFmpeg صفر تا صد دنبال کرد.

PNG

PNG مسیر جداگانه‌ای داشت: OxiPNG به‌عنوان موتور Lossless و ImageMagick فقط برای Palette fallback. این معماری به‌جای تلاش برای یکسان‌سازی همه Formatها، رفتار هر Format را جدا در نظر گرفت.

چرا این اعداد عمومی نیستند؟

کیفیت مطلوب فقط یک عدد نیست. نوع تصویر، حساسیت بصری، اندازه اولیه، هدف نمایش و هزینه Re-upload روی تصمیم اثر دارند. برای سایت دیگری بهتر است ابتدا یک Sample کوچک و نماینده از Corpus انتخاب شود و چند پروفایل روی همان Sample مقایسه شوند.

Validation و Reporting

یکی از نقاطی که این پروژه را از یک Batch Converter ساده جدا کرد، گزارش‌گیری ساختاریافته بود. Console برای مشاهده زنده مناسب است، اما برای تحلیل یک Run چندصدفایلی کافی نیست. هر Session باید Evidence قابل آرشیو تولید کند.

_DJH-Image-Optimizer-Logs\
├── optimizer-YYYYMMDD-HHMMSS.log
├── optimizer-files-YYYYMMDD-HHMMSS.csv
├── optimizer-formats-YYYYMMDD-HHMMSS.csv
├── optimizer-no-convert-YYYYMMDD-HHMMSS.csv
└── optimizer-session-YYYYMMDD-HHMMSS.tar.xz

Validation فایل

هر Candidate قبل از Replace شدن باید از Validation عبور می‌کرد. این کار جلوی حالتی را می‌گرفت که صرفاً به خاطر ایجاد شدن یک فایل کوچک‌تر، Source سالم با خروجی ناقص یا نامعتبر جایگزین شود.

CSVهای هر Run

گزارش فایل‌به‌فایل شامل Format، Status، Mode، SourceBytes، CandidateBytes، SavedBytes، SavedPercent، RMSE، Backup و Error بود. گزارش مجزا به تفکیک Format نیز کمک کرد مشکل PNG به‌جای گم شدن در میانگین کل Corpus سریع دیده شود.

آمار کل Corpus

یکی از اصلاحات مهم Reporting اضافه کردن TOTAL SOURCE SIZE و TOTAL FINAL SIZE بود. این دو شاخص پاسخ می‌دهند کل مجموعه واقعاً چقدر کوچک شده است؛ در حالی که آمار فایل‌های Replaceشده فقط عملکرد Encoder روی زیرمجموعه موفق را نشان می‌دهد.

باگ v1.7.1

یکی از مهم‌ترین خطاهای پروژه اصلاً در Encode رخ نداد. نسخه v1.7.1 پردازش تمام 860 فایل را کامل کرد، اما هنگام ساخت گزارش نهایی متوقف شد؛ چون متغیر $noConvertRows قبل از مقداردهی Reference شده بود.

Write-Utf8BomCsv -Rows $noConvertRows -Path $NoConvertCsv

The variable '$noConvertRows' cannot be retrieved because it has not been set.

Crash بعد از 860 فایل

ظاهر ماجرا می‌توانست این تصور را ایجاد کند که کل Run شکست خورده است، اما Stage پردازش تصاویر تمام شده بود. Failure در Reporting رخ داده بود. این تفکیک برای تصمیم بعدی حیاتی بود.

چرا Reprocess نکردیم؟

Restore کردن 860 فایل و اجرای مجدد Encoder فقط برای جبران یک باگ گزارش‌گیری کار منطقی نبود. Evidence فایل‌به‌فایل موجود بود و تصاویر قبلاً پردازش شده بودند. بنابراین باید Reporting Recover می‌شد، نه خود تصاویر.

Fix و Recovery

در v1.7.2 ترتیب ساخت متغیر اصلاح شد و یک Recovery Script نیز طراحی شد تا بتوان Report همان Session را از CSVهای موجود بازسازی کرد. این اتفاق نشان داد Reliability یک Pipeline فقط به Encoder محدود نیست؛ Log، Report و Recovery نیز بخشی از تعریف موفقیت هستند.

نتیجه Run نهایی

Session نهایی معتبر با نسخه v1.7.2 و شناسه 20260823-225751 اجرا شد. این Run همان Evidence اصلی است که نتیجه پروژه بر مبنای آن گزارش می‌شود.

فرمتنتیجهکاهش حجم
JPG581 از 591 Optimize27.85%
PNG246 از 246 Optimize9.63%
WebP23 از 23 Optimize14.90%
Images found       : 860
Optimized          : 850
No size benefit    : 10
Already processed  : 0
Failed             : 0

TOTAL SOURCE SIZE   : 130.52 MB
TOTAL FINAL SIZE    : 106.69 MB
TOTAL CORPUS SAVED  : 23.82 MB (18.25%)

نتیجه هر فرمت

JPG بیشترین Saving درصدی را داشت و 581 فایل از 591 مورد با Candidate کوچک‌تر جایگزین شدند. هر 23 WebP نیز Benefit داشتند. ده فایل بدون Benefit همگی JPG بودند و چون Candidate کوچک‌تری نداشتند، Source آن‌ها دست‌نخورده باقی ماند.

PNG بعد از تغییر موتور

مهم‌ترین تغییر مربوط به PNG بود. در مسیر اولیه فقط 4 مورد از 246 فایل بهبود پیدا کرده بودند؛ در Run نهایی تمام 246 PNG Candidate کوچک‌تر معتبر داشتند. تحلیل فایل‌به‌فایل نشان داد 241 مورد با OXIPNG_LOSSLESS و پنج مورد با PALETTE_256 برنده شدند. برای Candidateهای Lossless مقدار RMSE صفر بود و Candidateهای Palette نیز زیر Guard تعیین‌شده پروژه باقی ماندند.

این نتیجه دلیل خوبی برای یک اصل عمومی است: اگر یک Format در Corpus شما رفتار نامناسبی دارد، قبل از افزایش بی‌نهایت پارامترهای همان Encoder بررسی کنید آیا Engine مناسب آن Format را انتخاب کرده‌اید یا نه.

آیا هر Saving ارزش دارد؟

موفق بودن Optimization لزوماً به معنی ارزشمند بودن Re-upload نیست. اگر یک فایل فقط چند درصد کوچک شده باشد، زمان انتقال، کنترل، جایگزینی و Validation آن ممکن است بیشتر از منفعت واقعی باشد. برای این Batch تصمیم پروژه این بود که Saving کمتر از 9 درصد ارزش Re-upload ندارد.

آستانه 9 درصد

عدد 9 درصد یک Best Practice عمومی نیست. این Threshold برای هزینه و شرایط همین پروژه انتخاب شد. در پروژه‌ای دیگر ممکن است 3 درصد برای فایل چندمگابایتی مهم باشد یا حتی 15 درصد برای یک فایل بسیار کوچک ارزش عملیات نداشته باشد.

نتیجه Prune

با فیلتر SavedPercent < 9.00 تعداد 205 فایل در گروه کم‌ارزش قرار گرفتند.

فرمتتعدادوضعیت
JPG38زیر آستانه
PNG164زیر آستانه
WebP3زیر آستانه

اسکریپت Prune تعداد 205 فایل فعلی و 195 Backup متناظر را حذف کرد و هیچ Failure نداشت. برای 10 فایل، Original متناظر وجود نداشت؛ این همان 10 مورد NO_BENEFIT بود که از ابتدا Replace نشده بودند و طبیعتاً Backup نیز برایشان ساخته نشده بود.

Targets             : 205
Deleted current     : 205
Deleted originals   : 195
Missing current     : 0
Missing originals   : 10
Failed              : 0

مسیر عملی برای سایت دیگر

اگر بخواهیم نتیجه این پروژه را به یک Workflow قابل استفاده برای سایت دیگری تبدیل کنیم، نباید مستقیماً qscale، Quality یا Threshold این پروژه را کپی کنیم. بخش قابل تعمیم، روش تصمیم‌گیری است.

پیشنهاد عملی این است که قبل از پردازش کل آرشیو، تصاویر را بر اساس Format و نوع محتوای بصری گروه‌بندی کنید. برای JPEG یک پروفایل Lossy آزمایشی، برای PNG یک مسیر Lossless و در صورت نیاز Palette، و برای WebP/AVIF تنظیمات جدا در نظر بگیرید. نتیجه هر گروه را مستقل اندازه بگیرید و فقط بعد از آن سراغ Full Run بروید.

قبل از اجرا

  1. از کل wp-content/uploads یا مجموعه هدف Inventory بگیرید و تعداد فایل‌ها، فرمت‌ها و حجم کل را ثبت کنید.
  2. Backup جدا و قابل بازیابی داشته باشید؛ Backup داخل همان Pipeline جای Backup مستقل سایت را نمی‌گیرد.
  3. یک Sample کوچک ولی نماینده از JPG، PNG، WebP و سایر فرمت‌های موجود انتخاب کنید.
  4. ابعاد، Extension، Naming و Metadataهایی را که باید حفظ شوند قبل از Run مشخص کنید.
  5. ابزارها و نسخه‌های مورد استفاده را Preflight کنید.

هنگام پردازش

  1. برای هر Format یک Strategy مناسب انتخاب کنید.
  2. Candidate را جدا از Source بسازید.
  3. Candidate را Validate کنید.
  4. حجم و در صورت Lossy بودن، کیفیت را مقایسه کنید.
  5. فقط خروجی بهتر را جایگزین کنید.
  6. نتیجه هر فایل و Summary هر Format را ذخیره کنید.
  7. در Retestها ابتدا Originalها را Restore کنید.

قبل از Upload

  1. به حجم کل Corpus قبل و بعد نگاه کنید، نه فقط Saving فایل‌های موفق.
  2. Threshold اقتصادی یا عملی Re-upload را برای پروژه خودتان تعیین کنید.
  3. ساختار مسیرها و نام فایل‌ها را دوباره Scan کنید.
  4. چند تصویر Sample را بصری بررسی کنید.
  5. بعد از جایگزینی روی سایت، صفحات مهم، Responsive images و Thumbnailها را Validation کنید.
  6. Backupهای موقت را فقط زمانی حذف کنید که دیگر Rollback لازم نباشد.

اگر پروژه شما علاوه بر تصاویر، آرشیوهای رسانه‌ای بزرگ دیگری دارد، مقاله پروژه عملی کاهش حجم آرشیو موسیقی با FFmpeg نمونه دیگری از همین فلسفه اندازه‌گیری، Batch Processing و تصمیم بر اساس نتیجه واقعی است.

خطاها و وضعیت نهایی

جدول زیر مهم‌ترین مشکلاتی را که در طول پروژه ثبت شدند کنار آخرین وضعیت آن‌ها قرار می‌دهد. این بخش عمداً Failureها را حذف نمی‌کند، چون بخش مهمی از ارزش یک پروژه عملی دانستن مسیرهایی است که نتیجه مطلوب نداده‌اند.

مشکلراه‌حلوضعیت
PNG با FFmpeg سود نداردOxiPNG primaryValidated
ImageMagick brute-force کند استPalette fallback محدودValidated
PNG32 فایل را بزرگ می‌کندحذف forcingResolved
Reporting v1.7.1 Crashv1.7.2 + RecoveryValidated
فایل‌های زیر 9%Prune جداگانهValidated
Cleanup عمومی Originalابزار با تأیید YESNot verified

Candidate بزرگ‌تر است

این Error نیست. Source از قبل ممکن است بهتر Compress شده باشد. Candidate بزرگ‌تر باید حذف شود و Source بدون تغییر بماند.

فایل original وجود ندارد

اگر فایل هرگز Replace نشده باشد، Backup با نام .original.* نیز ساخته نمی‌شود. در Run Prune، ده Original مفقود دقیقاً با ده فایل No-benefit تطابق داشتند.

چه چیزهایی هنوز تأیید نشده؟

آخرین Evidence پروژه اجرای موفق Optimizer v1.7.2 و حذف موفق 205 فایل زیر آستانه 9 درصد را تأیید می‌کند. ابزار Generic برای حذف باقی‌مانده فایل‌های *.original.* ساخته شده، اما Log اجرای آن در مستندات موجود نیست. به همین دلیل نمی‌توان ادعا کرد Cleanup نهایی انجام شده است.

همچنین تعداد 655 فایل فعلی از تفریق 205 فایل حذف‌شده از 860 فایل محاسبه می‌شود، اما Scan مستقل بعد از Prune در Evidence موجود ثبت نشده است. بنابراین 655 «وضعیت مورد انتظار» است، نه Count مستقیم مشاهده‌شده بعد از آخرین عملیات.

سوالات متداول

پرسش‌های زیر روی مهم‌ترین تصمیم‌ها و ابهام‌هایی تمرکز دارند که در پروژه‌های واقعی بهینه‌سازی تصاویر WordPress پیش می‌آیند.

آیا برای کاهش حجم تصاویر وردپرس حتماً باید از افزونه استفاده کنیم؟

خیر. افزونه برای بسیاری از سایت‌ها مناسب است، اما در پروژه‌های Migration یا Batch می‌توان تصاویر را خارج از WordPress نیز Optimize کرد؛ به شرط داشتن Backup، Validation و مسیر بازگشت امن.

آیا این پروژه ابعاد تصاویر را تغییر داد؟

خیر. Resize در Scope این پروژه نبود و ابعاد، نام، مسیر و Extension نهایی فایل‌ها حفظ شدند.

چرا FFmpeg برای PNGهای این پروژه مناسب نبود؟

در Run اولیه فقط 4 مورد از 246 PNG Candidate کوچک‌تر تولید کردند. همین Evidence باعث شد مسیر PNG از FFmpeg جدا و OxiPNG به موتور اصلی تبدیل شود.

OxiPNG کیفیت PNG را کاهش می‌دهد؟

مسیر اصلی OxiPNG در این پروژه Lossless بود. کاهش احتمالی کیفیت فقط در Palette fallback مطرح می‌شد و آن مسیر با RMSE Guard کنترل شد.

چرا ImageMagick به‌تنهایی استفاده نشد؟

ImageMagick قابلیت لازم را داشت، اما معماری brute-force چندمرحله‌ای برای این Batch بیش از حد کند بود. در نسخه نهایی فقط برای Palette fallback و بررسی اختلاف تصویری استفاده شد.

چرا فایل Original نگه داشته شد؟

برای Rollback، Retest و مقایسه معتبر. بدون Original، اجرای Engine جدید ممکن بود روی فایل قبلاً Re-encodeشده انجام شود و Benchmark را منحرف کند.

اگر Candidate از فایل اصلی بزرگ‌تر شود چه اتفاقی می‌افتد؟

Source حفظ می‌شود و Candidate پذیرفته نمی‌شود. بزرگ‌تر شدن خروجی لزوماً Failure نیست و ممکن است نشان دهد فایل اصلی از قبل خوب فشرده شده است.

آیا کاهش 18.25 درصد برای هر سایت وردپرسی قابل انتظار است؟

خیر. این عدد مربوط به همین Corpus، همین تنظیمات و همین ترکیب فرمت‌هاست. سایت دیگر ممکن است Saving بسیار کمتر یا بیشتری داشته باشد.

چرا فایل‌های زیر 9 درصد صرفه‌جویی حذف شدند؟

این یک تصمیم عملی برای همین پروژه بود، چون Saving کمتر از 9 درصد در برابر هزینه Re-upload ارزش کافی نداشت. این Threshold یک استاندارد عمومی نیست.

WebP یا AVIF بهتر از حفظ JPG و PNG نبود؟

ممکن بود در پروژه‌ای دیگر انتخاب مناسبی باشد، اما Requirement این پروژه حفظ Extension اصلی بود. Format conversion یک معماری متفاوت است و باید جداگانه از نظر سازگاری و Workflow سایت بررسی شود.

آیا Cleanup نهایی فایل‌های .original.* انجام شد؟

ابزار Cleanup ساخته شد، اما در Evidence موجود Log اجرای نهایی آن ثبت نشده است. بنابراین وضعیت آن باید Not Verified در نظر گرفته شود.

نتیجه‌گیری

این پروژه با یک فرض ساده شروع شد: تصاویر یک سایت وردپرس قدیمی را با یک Pipeline عمومی فشرده کنیم. داده واقعی خیلی زود نشان داد این فرض کافی نیست. FFmpeg برای JPG و WebP خوب عمل کرد، اما برای 242 مورد از 246 PNG هیچ Benefit نداشت. ImageMagick انعطاف بیشتری ایجاد کرد، ولی معماری brute-force هزینه پردازشی نامناسبی داشت. در نهایت PNG به OxiPNG سپرده شد و ImageMagick فقط در جایی باقی ماند که ارزش اضافه ایجاد می‌کرد.

پروژه عملی کاهش حجم تصاویر یک سایت قدیمی و پر بازدید وردپرس | داریوش حقیقی
پروژه عملی کاهش حجم تصاویر یک سایت قدیمی و پر بازدید وردپرس | داریوش حقیقی

نتیجه Run نهایی روی 860 تصویر، 850 Optimize، 10 No-benefit و صفر Failure بود و حجم کل Corpus 23.82 مگابایت یا 18.25 درصد کاهش پیدا کرد. اما شاید مهم‌ترین نتیجه این باشد که «Encode موفق» آخر Workflow نیست. Candidate باید Validate شود، Original باید قابل Restore باشد، گزارش باید قابل اعتماد باشد و حتی بعد از Compression باید تصمیم گرفت کدام Saving واقعاً ارزش Re-upload دارد.

اگر بخواهم یک قانون از این پروژه باقی بگذارم، این است: **ابزار را بر اساس شهرت یا عادت انتخاب نکنید؛ Corpus را اندازه بگیرید و اجازه دهید Evidence معماری را تعیین کند.**

داریوش حقیقی
نویسنده و توسعه‌دهنده

داریوش حقیقی

بیش از 20 سال تجربه در حوزه فناوری اطلاعات، طراحی سایت، سئو تکنیکال، مدیریت سرورهای لینوکس و ویندوز، توسعه وردپرس، برنامه‌نویسی، اتوماسیون و هوش مصنوعی.در djh.ir تلاش می‌کنم تجربیات واقعی پروژه‌های اجرایی، آموزش‌های کاربردی و راهکارهای عملی را با زبانی ساده و قابل استفاده منتشر کنم.

20+ سال تجربه
100+ پروژه اجرایی
1000+ ساعت آموزش

نظر و سوالتون رو اینجا بنویسید...

تماس در تلگرام