دليل ضغط الشيفرة: شرح CSS وJS وHTML
ضغط الشيفرة المصدرية (minification) يحذف من مصدر CSS وJavaScript وHTML المحارفَ التي لا تحتاجها الآلة، أي المسافات البيضاء والتعليقات وفواصل الأسطر، ويعيد كتابة الأنماط المطوّلة في صيغ مكافئة أقصر. السلوك يبقى كما هو؛ غاية ما يحدث أن الملف يصغر ويُحمَّل أسرع.
ينبغي حسم نقطة منذ البداية: ضغط الشيفرة المصدرية ليس ضغط النقل. فعملية minify تجري على شيفرتك المصدرية وتزيل التكرار النحوي منها، بينما gzip وbrotli يعملان على البايتات أثناء النقل ويرمّزان الأنماط المتكررة. يعمل كلٌّ منهما في مرحلة مختلفة، ويهاجم نوعًا مختلفًا من التكرار، ويتراكمان فوق بعضهما. لهذا ينبغي أن تستمر في عملية minify حتى حين يخدم خادمك المحتوى عبر brotli أصلًا، وسيتّضح السبب فيما يلي.
تريد أن تضغط شيئًا الآن؟ انتقل مباشرةً إلى مُصغِّر CSS، أو مُصغِّر JavaScript، أو مُصغِّر HTML، وكلٌّ منها يعمل بالكامل داخل متصفحك. لكن فهم الآلية هو ما يتيح لك أن تقرّر أين تضغط وهل تحتاج أصلًا إلى فعلها يدويًا. وسنمرّ على ما يفعله الضغط فعلًا، وكيف يُضغَط كلٌّ من CSS وJS وHTML، وكيف يتراكم minify مع gzip وbrotli، ومتى تتولّى أداة البناء لديك الأمر أصلًا، وكيف تُبقي خرائط المصدر (source maps) الشيفرةَ المضغوطة قابلة للتنقيح.
ما هو الضغط (وما ليس هو)
يفعل الضغط أمرين. يحذف المحارف التي لا تحمل معنى للمحلِّل النحوي، ويعيد كتابة مصدرك في صيغة أقصر تعني الشيء نفسه تمامًا. الناتج مكافئ تمامًا بالنسبة للآلة وشبه غير مقروء بالنسبة للإنسان. لا شيء يتغير في كيفية عمل الشيفرة، يتغير سطحها فقط.
تلك النقطة الأخيرة هي الثابت الذي ينبغي التمسك به في بقية هذا الدليل: minify يحرّر سطح مصدرك فقط (المسافات البيضاء، التعليقات، أسماء المعرّفات، الصيغ الزائدة)، ولا يمسّ السلوك أو الناتج أبدًا. إنها الصورة المعكوسة للتنسيق. التنسيق يضيف مسافات بيضاء ليجعل الشيفرة مقروءة؛ والضغط يجرّدها ليجعل الشيفرة صغيرة. كلاهما على المحور نفسه «المكافئ دلاليًا»، لكنهما يشيران إلى اتجاهين متعاكسين.
يخلط الناس باستمرار بين ثلاث عمليات تتشابه أسماؤها. هذا الجدول يفرز بينها:
| البُعد | التنسيق (beautify) | الضغط (minify) | الضغط للنقل (gzip/brotli) |
|---|---|---|---|
| ما الذي يغيّره | يضيف مسافات بيضاء وفواصل أسطر وإزاحة | يحذف المسافات البيضاء والتعليقات، ويقصّر الصيغة | ترميز على مستوى البايت للأنماط المتكررة |
| أيّ طبقة | الشيفرة المصدرية | الشيفرة المصدرية | النقل / التخزين |
| أما زال شيفرة مصدرية؟ | نعم (مقروءة) | نعم (قابلة للتشغيل، صعبة القراءة) | لا (ثنائي، يجب فكّ ترميزه) |
| من يقوم بها | المطوّر / المحرّر | أداة البناء / المُصغِّر | الخادم + المتصفح |
| قابلة للعكس؟ | دلاليًا | دلاليًا (السلوك لا يتغير) | كليًا (فكّ الضغط يستعيد البايتات) |
التنسيق وminify يعيشان على محور واحد هو محور التكافؤ الدلالي. أما ضغط النقل فيعيش على محور مختلف تمامًا. الملف المنسَّق والملف المضغوط كلاهما شيفرة مصدرية صالحة؛ أما الملف المضغوط للنقل فكتلة ثنائية يجب فكّ ترميزها قبل أن يعمل أي شيء.
وهنا يشيع خطأ مفاهيمي: «خادمي يجري gzip أصلًا، فلا فائدة من minify». هذا غير صحيح، والأرقام لاحقًا في هذا الدليل تبيّن السبب. minify وضغط النقل يزيلان تكرارًا مختلفًا، فإجراء أحدهما لا يجعل الآخر زائدًا. أبقِ ذلك في ذهنك ونحن نمرّ على كل لغة.
يساعد على الفهم أن نفكّر لماذا تُوجد أصلًا البايتات التي يحذفها المُصغِّر. أنت تكتب المسافات البيضاء والتعليقات والأسماء الوصفية من أجلك ومن أجل زملائك، فهي تجعل الشيفرة قابلة للمراجعة والصيانة. أما الآلة التي تحلّل CSS لديك، أو تشغّل JavaScript الخاص بك، أو تبني DOM الخاص بصفحتك، فتتجاهلها جميعًا. الضغط هو الخطوة التي تطرح ما يخصّ البشر وحدهم بعد أن ينتهي البشر من المصدر. ولهذا أيضًا يُعدّ الضغط شأنًا إنتاجيًا لا تطويريًا: تُبقي النسخة المقروءة في مستودعك وتشحن النسخة المجرَّدة إلى المتصفحات. النسخة المقروءة هي مصدر الحقيقة؛ والنسخة المضغوطة قطعة بناء يمكنك توليدها من جديد في أي وقت.
كيف يعمل ضغط CSS
CSS هي ألطف اللغات الثلاث ضغطًا لأن قواعدها النحوية لا تترك مجالًا كبيرًا للالتباس. يحذف المُصغِّر التعليقات، ويطوي تتابعات المسافات البيضاء إلى لا شيء، ويسقط الفاصلة المنقوطة الأخيرة في كل كتلة، ويزيل المسافات حول { و} و: و;. هذا وحده يزيح معظم البايتات.
تتيح CSS أيضًا مجموعةً من عمليات إعادة الكتابة المكافئة لا تشاركها فيها أي لغة أخرى. والمُصغِّر الجيّد يطبّقها بأمان:
- تقصير الألوان.
#ffffffتصير#fff، و#ff0000تنهار إلىred(أو العكس، أيهما أقصر كتابةً). - إسقاط الوحدات على الصفر.
0pxتصير0، وmargin: 0 0 0 0تصيرmargin: 0. - حذف الأصفار البادئة.
0.5emتصير.5em. - دمج الاختصارات. أربعة تصريحات منفصلة
margin-topوmargin-rightوmargin-bottomوmargin-leftتنطوي فيmarginواحد. - دمج القواعد. القواعد المتجاورة ذات المحدِّدات أو التصريحات المتطابقة يمكن دمجها، والتصريحات المكرّرة إسقاطها.
كل واحدة من هذه يُبقي النتيجة المعروضة متطابقة، وهذا هو الحدّ الذي لا يتجاوزه أبدًا مُصغِّر مُلتزم. لكن CSS حساسة للترتيب: القاعدة اللاحقة تتجاوز السابقة عبر التتالي (cascade). لذا فإن المُصغِّر الآمن لن يعيد ترتيب القواعد على نحوٍ أعمى بما قد يغيّر أيّ تصريح يفوز. تقليص البايتات مسموح؛ تغيير التتالي ممنوع.
ذلك القيد أدقّ مما يبدو. تصريحان يبدوان قابلين للدمج قد لا يكونان كذلك، لأن شيئًا بينهما يشير إلى الخاصية نفسها بالخصوصية (specificity) نفسها. تأمّل:
.btn { color: #ff0000; }
.alert .btn { color: blue; }
.btn { color: #f00; }
القاعدتان الأولى والثالثة تتشاركان محدِّدًا ويمكن دمجهما، لكن فقط إذا لم يُحرِّك ذلك التصريحَ متجاوزًا القاعدة الوسطى بطريقة تغيّر أيّهما يفوز لعنصر يطابق كليهما. الدمج الساذج الذي يعيد ترتيب هذه قد يكسر التتالي. هذا نوع الحالة الحدّية التي بُنِيَ محرّك بمستوى إنتاجي مثل CSSO ليفكّر فيها، ولهذا ينبغي ألا تصنع مُصغِّر «احذف المسافات البيضاء» بيدك عبر تعبير نمطي (regex). تبدو التحويلات آليةً، لكن تحليل السلامة الكامن وراءها ليس كذلك.
مُصغِّر CSS لدينا يستخدم محرّك CSSO لهذا النوع تحديدًا من الضغط بلا فقدان، ويعمل بالكامل داخل متصفحك مع قراءة لتوفير البايتات حتى ترى أثر كل تمريرة على الحمولة. وتلك الأداة نفسها تنسّق في الاتجاه المعاكس أيضًا، فتأخذ ورقة أنماط مضغوطة نسختها من موقع حيّ وتعيد توسيعها إلى قواعد مقروءة ومُزاحة. الجأ إليها حين تنسخ مقطعًا من CSS وتريد التحقق من حجمه المضغوط، أو حين تشحن صفحة ثابتة بلا خطوة بناء تتولّى الأمر بدلًا عنك.
كيف يعمل ضغط JavaScript
ضغط JavaScript يذهب أبعد بكثير من CSS، وهناك تقع الوفورات والمصائد معًا. ولترى السبب، انظر إلى دالة صغيرة قبل Terser وبعدها:
// قبل
function calculateTotal(items, taxRate) {
let runningTotal = 0;
for (const item of items) {
runningTotal += item.price * item.quantity;
}
return runningTotal * (1 + taxRate);
}
// بعد
function calculateTotal(t,a){let n=0;for(const o of t)n+=o.price*o.quantity;return n*(1+a)}
اسم الدالة calculateTotal يبقى لأنه مُصدَّر (أو قد يُستدعى من مكان آخر)؛ أما المعاملات ومتغيّرات الحلقة فتنهار إلى أحرف مفردة. هذا جوهر الأمر، لكن مُصغِّر JS يفعل عدة أشياء متمايزة:
- تشويه المعرّفات (mangling). المتغيّرات المحلية والمعاملات تُعاد تسميتها إلى أحرف مفردة:
getUserPreferencesتصيرa. تُشوَّه المحلية فقط، أما المتغيّرات العامة والأسماء المُصدَّرة فتبقى سليمة افتراضيًا، لأن إعادة تسميتها تكسر شيفرة تشير إليها من الخارج. - إزالة الشيفرة الميتة (dead-code elimination). الفروع غير القابلة للوصول والمتغيّرات غير المستخدَمة تُحذف، بالتعاون الوثيق مع التقليم الشجري (tree-shaking) على مستوى المُجمِّع.
- طيّ الثوابت وضغط الصياغة. التعابير تُقصَّر:
trueتصير!0، وfalseتصير!1، وreturn undefined;تصيرreturn;.
أنفع ما ينبغي معرفته عن ضغط JS هو مصيدة الإدراج التلقائي للفاصلة المنقوطة (ASI). تتيح لك JavaScript أن تحذف الفواصل المنقوطة، ويُدرجها المحلِّل نيابةً عنك وفق قواعد محدّدة. وحين يحذف المُصغِّر فواصل الأسطر التي تعتمد عليها تلك القواعد، قد يتغيّر معنى الشيفرة. الإخفاق الكلاسيكي هو جملة تبدأ بـ ( أو [ تُلصَق صمتًا بالسطر السابق:
const x = getValue()
[1, 2, 3].forEach(handle)
من دون فواصل منقوطة، يُحلَّل هذا على أنه getValue()[1, 2, 3]، أي تعبير فهرسة لا جملتين. وما إن يُضغَط في سطر واحد حتى يُثبَّت الخطأ. ويظهر الخطر نفسه مع سطر يبدأ بـ (، حيث يُستدعى التعبير السابق كأنه دالة. تتعامل Terser الحديثة مع معظم الحالات الواقعية بسلاسة لأنها تحلّل الشيفرة إلى شجرة صياغة مجرّدة (AST) أولًا، ثم تعيد بثّ الفواصل المنقوطة حيث تلزم، فهي لا تجري حذفًا نصّيًا أعمى. لكن المصدر السيّئ مقرونًا بضغط عدواني مصدرٌ حقيقي لأخطاء الإنتاج، وهذه الإخفاقات يصعب تتبّعها تحديدًا لأنها لا تظهر إلا في بناء الإنتاج المضغوط، لا في التطوير. الحلّ في جهتك أنت: اكتب شيفرة بفواصل منقوطة صريحة وصياغة لا لبس فيها، فيبقى المُصغِّر آمنًا. قاعدة في أداة الفحص (linter) أو منسّق تلقائي يُدرج الفواصل المنقوطة على مستوى المصدر يزيل الخطر تمامًا.
المُصغِّر المُلتزم يحافظ على السلوك، لكن فقط إذا كان الدخل JavaScript صالحًا وقياسيًا. Terser تحلّل ECMAScript؛ وهي لا تفهم TypeScript ولا JSX. هذان يجب ترجمتهما (transpile) إلى JS عادي أولًا، وإلا فشل الضغط عند خطوة التحليل. إن لصقت ملف .ts في مُصغِّر JS وحصلت على خطأ، فهذا هو السبب.
يتكرر سؤال تسمية: minify مقابل uglify. هما يعنيان الشيء نفسه عمليًا. كلمة «uglify» جاءت من UglifyJS، مُصغِّر JS الشائع المبكّر؛ وTerser فرعه الحديث الذي يدعم ES2015 وما بعده. واليوم صار «minify» المصطلح العام عبر اللغات الثلاث، وبقي «uglify» اسمًا أقدم خاصًا بـ JS للعملية نفسها.
مُصغِّر JavaScript لدينا يشغّل Terser في المتصفح، فيعيد تسمية المحلية، ويسقط الشيفرة الميتة، ويجرّد التعليقات، ويُبلِغك كم بايتًا وفّر في كل تمريرة.
كيف يعمل ضغط HTML
يبدأ ضغط HTML بالأساسيات: حذف التعليقات (مع الإبقاء على تصريح <!DOCTYPE> وأي تعليقات شرطية ما زلت تعتمد عليها)، وطيّ المسافات البيضاء بين الوسوم، وتشذيب المسافات الزائدة داخل قوائم السمات. يُظهر مقطع صغير شكلَ الأمر:
<!-- التنقّل -->
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
تصير:
<ul><li><a href=/>Home</a><li><a href=/about>About</a></ul>
اختفى التعليق، وانطوت الإزاحة بين الوسوم، وأُسقِطت وسوم الإغلاق الاختيارية </li>، وفقدت قيمُ السمات غير المُقتبَسة علاماتِ الاقتباس. ومن هنا يستطيع المُصغِّر تطبيق بضع حِيَل أخرى خاصة بـ HTML:
- إزالة وسوم الإغلاق الاختيارية. تتيح مواصفة HTML حذف
</li>و</p>و</td>وعدة وسوم أخرى، فيمكن للمُصغِّر إسقاطها. - إزالة علامات اقتباس السمات. حين لا تحوي القيمة مسافات أو محارف خاصة، تصير
class="x"هيclass=x. - طيّ السمات المنطقية.
disabled="disabled"تصيرdisabledفحسب، وchecked="checked"تصيرchecked. - ضغط CSS وJS المضمَّنين. محتويات كتل
<style>و<script>تُضغَط أيضًا، فتقلّص تمريرة واحدة المستندَ كله.
وهنا الحدّ الأهم: في HTML، المسافة البيضاء ذات معنى أحيانًا. داخل <pre> و<textarea>، تُعرَض كل مسافة وكل سطر جديد حرفيًا. والعناصر ذات white-space: pre تتصرف بالطريقة نفسها. والمسافة البيضاء بين العناصر السطرية تؤثّر في التخطيط، فمسافة بين وسمَي <a> تظهر فجوةً على الصفحة. الضغط العدواني الذي يُسطِّح هذه المسافة قد يغيّر مظهر الصفحة. القاعدة العملية: بعد الضغط، تحقَّق من العرض حول pre وtextarea وحدود العناصر السطرية قبل أن تشحن.
مُصغِّر HTML لدينا ينسّق بـ js-beautify ويضغط بـ CSSO وTerser للأنماط والنصوص البرمجية المضمَّنة، كل ذلك داخل المتصفح. وهو مفيد بصفة خاصة لـ HTML البريد الإلكتروني والترميز المُصدَّر من أنظمة إدارة المحتوى (CMS)، حيث نادرًا ما توجد خطوة بناء تتولّى الضغط بدلًا عنك.
minify مقابل gzip مقابل brotli: كيف يتراكمون
يبقى السؤال: إذا كان خادمك يخدم gzip أو brotli أصلًا، فهل ما زلت تحتاج إلى minify؟ نعم، والسبب أن التقنيتين تزيلان تكرارًا مختلفًا.
minify يزيل التكرار النحوي على مستوى المصدر: المسافات البيضاء والتعليقات والأسماء الطويلة والبُنى المطوَّلة الموجودة من أجل القراءة البشرية. أما gzip وbrotli فيزيلان التكرار الإحصائي على مستوى البايت: السلاسل والأنماط التي تتكرر عبر الملف يُستبدَل بها رموز أقصر. أحدهما يفهم صياغة شيفرتك؛ والآخر يرى مجرد دفق من البايتات. ولأنهما يستهدفان أشياء مختلفة، فإن تراكمهما ينجح وينجح جيدًا.
طريقة ملموسة لتصوّر الأمر: gzip بارع في ملاحظة أن السلسلة function تظهر مئتي مرة في حزمة واستبدال كل ظهور بإحالة خلفية قصيرة. لكنه لا يدرك أن getUserPreferences وgetUserSettings اسما متغيّرين كان بإمكانه تقصيرهما، ولا أن كتلة if (false) { ... } بأكملها لن تُنفَّذ أبدًا. minify يتولّى ذلك بالضبط، أي المكاسب البنيوية والدلالية التي يعمى عنها ضاغط على مستوى البايت. شغّلهما معًا فينظّف كلٌّ منهما ما لا يراه الآخر.
وإليك الحساب، بالترتيب الذي يحدث به فعلًا:
- minify وحده يقلّص CSS وJS وHTML عادةً بنسبة 20–30%، بحذف المسافات البيضاء والتعليقات وتقصير الصياغة.
- gzip على الناتج المضغوط يزيل 60–80% أخرى، بترميز الأنماط المتكررة الباقية في النص.
- brotli بدل gzip يُنتج ناتجًا أصغر بـ 15–25% مرة أخرى، بفضل قاموس مدمج أكبر وخوارزمية أفضل.
الخلاصة: اضغط بـ minify أولًا، ثم اضغط للنقل، فالنتيجة المجتمعة كثيرًا ما تكون أصغر من المصدر الأصلي بنسبة 80–90%. الأمران لا يستبعد أحدهما الآخر، وتخطّي أيٍّ منهما يترك بايتات دون استثمار.
ولماذا ما زال minify يستحق عناءه فوق brotli؟ لثلاثة أسباب:
- الدخل الأصغر يُضغَط أصغر. الملف المضغوط بـ minify يمنح الضاغط مادةً متكررة أقل ليمضغها، والدخل الأصغر والأنظف ينتج عنه عمومًا ناتج أصغر.
- minify يفعل ما لا يقدر عليه ضغط النقل. إزالة الشيفرة الميتة وأسماء المتغيّرات القصيرة إزالاتٌ دلالية. gzip لا يفهم شيفرتك، إذ يرى البايتات فقط، فلا يمكنه أبدًا حذف دالة غير مستخدَمة أو إعادة تسمية متغيّر.
- المتصفح يحلّل بايتات أقل. بعد فكّ الضغط، يتلقى المتصفح الشيفرة المضغوطة بـ minify. شيفرة أقل تعني تحليلًا وتنفيذًا أسرع، لا مجرد تنزيل أصغر.
الترتيب ليس اختيارًا، بل نتيجة طبيعية لموضع كل خطوة. minify ينتمي إلى وقت البناء (تفعله أنت أو أداة البناء مرة واحدة). وضغط النقل ينتمي إلى وقت النقل (يفعله الخادم لكل طلب، ويفكّه المتصفح عند الوصول). فالمسار طبيعيًا هو minify ← نشر ← يضغط الخادم. ولا يمكنك تشغيله بالعكس: لا سبيل إلى «اضغط للنقل ثم minify»، لأن الناتج المضغوط للنقل لم يعد شيفرة مصدرية.
وثمة تحفّظ صغير لكنه مهم على قاعدة «اضغط بـ minify ثم اضغط للنقل»: ما إن يصبح المحتوى مضغوطًا للنقل أصلًا، يصير ضغطه ثانيةً عديم الجدوى أو معكوس الأثر. الأصول الثنائية أصلًا وعالية الإنتروبيا، مثل JPEG وPNG وWebP والخطوط بصيغة WOFF2، لا تكسب شيئًا من gzip أو brotli ولا ينبغي أن تكون في قواعد ضغط النصوص لديك أصلًا. minify تحويل نصّي فقط، فلا يمسّ تلك الملفات أبدًا؛ أما ضغط النقل فهو الموضع الذي عليك أن تكون انتقائيًا فيه. اضبط خادمك ليضغط أنواع MIME النصية (HTML وCSS وJS وJSON وSVG) ودَع الثنائيات المضغوطة أصلًا وشأنها.
ضبط طبقة النقل، أي تفعيل brotli وتعيين Content-Encoding، شأن تشغيلي يتولّاه خادمك أو شبكة توصيل المحتوى (CDN). يبقى هذا الدليل عند طبقة المصدر حيث يجري minify. وإن كنت تحسّن الحمولة على نطاق أوسع، فالتفكير نفسه «وفّر البايتات في طبقة الترميز» ينطبق على الصور أيضًا؛ يغطّي دليل صيغ الصور لدينا جانب WebP/AVIF/JPEG من تلك الحكاية.
متى لا تحتاج إلى الضغط يدويًا
وثمة حقيقة يتخطّاها كثير من تسويق المُصغِّرات: إن كانت لديك خطوة بناء، فإن ناتج إنتاجك مضغوط أصلًا. خطوط أنابيب البناء الحديثة تفعلها تلقائيًا.
Vite وesbuild يضغطان JavaScript وCSS جاهزَين من دون إعداد. Rollup وwebpack يفعلانها عبر TerserPlugin وCssMinimizerPlugin. وLightning CSS يعالج CSS بسرعة أصيلة. وNext.js وAstro والأُطر المشابهة تضغط وتقلّم شجريًا وتقسّم الحزم في بنائها الإنتاجي من دون أن ترفع إصبعًا. والأمر عادةً ليس أكثر من vite build أو npm run build، فالضغط جزء مما يعنيه «البناء للإنتاج»، لا خطوة منفصلة تُلصِقها فوقه. إن كان هذا يصف مشروعك، فإن تمرير ملف عبر مُصغِّر منفصل بعد ذلك زائدٌ في أحسن الأحوال وضارّ في أسوئها، فالتشويه المزدوج لشيفرة مضغوطة أصلًا قد يُنتج ناتجًا محيّرًا ولن يوفّر بايتات إضافية ذات شأن.
أدوات البناء تفعل أيضًا شيئًا لا يقدر عليه مُصغِّر مستقلّ: إنها تضغط في سياق رسم تبعيّاتك بأكمله. والتقليم الشجري (tree-shaking) بالذات لا يعمل إلا حين يستطيع المُجمِّع رؤية كل استيراد وتصدير وإثبات أن دالة بعينها لا تُستخدَم أبدًا. أما مُصغِّر الملف الواحد فلا رسم لديه ليفكّر فيه؛ يستطيع إسقاط الشيفرة الميتة داخل الملف الذي أعطيته إياه، لكنه لا يستطيع أن يميّز أن وحدةً مستورَدة بأكملها غير قابلة للوصول. وهذا سبب آخر يجعل خط أنابيب البناء الموطن الصحيح لضغط الإنتاج.
فمتى يكون المُصغِّر المستقلّ الأداة الصحيحة؟ في المجموعة الصادقة من الحالات التي لا توجد فيها خطوة بناء تفعلها بدلًا عنك:
- المواقع الثابتة والصفحات المكتوبة يدويًا في ملف واحد بلا مُجمِّع في الحلقة.
- قوالب HTML للبريد الإلكتروني، حيث تحاسب أنظمة كثيرة بالبايت ولا توجد خطوة بناء أصلًا.
- مقاطع الطرف الثالث وشيفرة الودجات التي تضمّنها في صفحة شخص آخر.
- فحوص الحجم السريعة: الصق كتلة، وانظر كم تصير بعد الضغط وكم وفّرت. هذا غرض قراءة توفير البايتات.
- قراءة شيفرة مضغوطة لشخص آخر، حيث تشغّل المنسّق بالعكس لتجعلها مقروءة من جديد.
القرار بسيط. لديك بناء؟ دع البناء يضغط. لا بناء، أو حالة عابرة، أو مجرد فحص حجم؟ فالأداة عبر الإنترنت أسرع مسار، ولأن هذه الأدوات تعمل بالكامل داخل متصفحك، فإن شيفرتك لا تغادر جهازك أبدًا. وهذا يهمّ للشيفرة المملوكة أو غير المنشورة، التي لا ينبغي أبدًا أن تلصقها في منسّق يعمل من جهة الخادم ويتلقى نسخةً من كل شيء. إنها حجة الخصوصية نفسها التي تسري في دليل تنسيق SQL لدينا، الغوص العميق الآخر في التنسيق ضمن هذه المجموعة.
خرائط المصدر: تنقيح الشيفرة المضغوطة
الشيفرة المضغوطة صعبة التنقيح بذاتها. ما إن تعيد Terser تسمية كل متغيّر محلي إلى a وb وc، حتى يصير تتبّع مكدّس الإنتاج الذي يشير إلى bundle.min.js:1:48211 لا يخبرك بشيء يُذكَر عمّا تعطّل فعلًا.
خرائط المصدر تحلّ هذا. خريطة المصدر ملف .map يسجّل التطابق بين كل موضع في الناتج المضغوط والموضع المقابل في مصدرك الأصلي. وحين تحمّلها أدوات مطوّري المتصفح (DevTools)، تترجم أخطاء الشيفرة المضغوطة إلى أسماء ملفات وأرقام أسطر وأسماء متغيّرات حقيقية. تنقّح في مقابل الشيفرة التي كتبتها، مع أن المتصفح يشغّل الشيفرة التي أنتجها بناؤك.
عمليًا، تولّد أداة البناء لديك خريطة المصدر إلى جانب الحزمة المضغوطة، ويشير تعليق //# sourceMappingURL=bundle.min.js.map (أو ترويسة HTTP) المتصفحَ إلى الملف .map. افتح DevTools، واصطدم بخطأ، فيُظهِر تتبّع المكدّس أسماء ملفاتك وأرقام أسطرك الحقيقية بدل حساء الشيفرة المضغوطة. تُحمَّل الخريطة بكسل، فقط حين تكون DevTools مفتوحة، فلا تكلّف زوّارك شيئًا.
وثمة زاوية خصوصية تستحق المعرفة. خريطة مصدر عامة تشحن عمليًا شيفرتك المصدرية الأصلية إلى كل من يفتح DevTools. هذا مقبول للشيفرة المفتوحة؛ وغير مقبول للشيفرة المملوكة. ولهذا وُجِدت خرائط المصدر المخفية (hidden): لا تحمل الحزمة تعليق sourceMappingURL، فلا يرى الجمهور الخريطة أبدًا، لكنك ما زلت ترفعها إلى خدمة رصد أخطاء مثل Sentry. تفكّ الخدمة ضغطَ تتبّعات مكدّس الإنتاج من جهتها، فتمنحك أخطاء مقروءة من دون كشف مصدرك للعالم.
لاحظ كيف يعزّز هذا النقطة السابقة: خرائط المصدر قدرة على مستوى أداة البناء. والمُصغِّر العادي عبر الإنترنت لا يُنتج خريطة عادةً، لأن الضغط العابر لا يحتاجها. وهذا سبب آخر لتدع بناءك يتولّى ضغط الإنتاج، فهو يمنحك الخريطة مجانًا. وتذكّر أن خريطة المصدر لا تغيّر الحزمة المضغوطة نفسها أبدًا؛ إنها معينة تنقيح صرفة تجلس بجوارها. لا تخلط بين الملف .map وتبعية إنتاجية.
الأسئلة الشائعة
هل الضغط (minify) هو نفسه ضغط النقل؟
لا. minify يعيد كتابة شيفرتك المصدرية، إذ يجرّد المسافات البيضاء والتعليقات ويقصّر الأسماء، فتبقى شيفرة صالحة لكن أصغر. أما ضغط النقل (gzip وbrotli) فيرمّز البايتات الناتجة من أجل النقل، ويفكّها المتصفح. يهاجمان تكرارًا مختلفًا، ويعملان في مرحلتين مختلفتين، ويتراكمان: اضغط بـ minify أولًا، ثم اضغط للنقل.
هل أحتاج إلى minify إن كنت أستخدم gzip أو brotli؟
نعم. minify ما زال يهمّ مع gzip وbrotli. الشيفرة المضغوطة بـ minify تمنح الضاغط دخلًا أقل تكرارًا فيضغط أصغر، وminify يجري إزالات دلالية، مثل الشيفرة الميتة وأسماء المتغيّرات القصيرة، لا يقدر عليها ضغط مستوى البايت. والمتصفح أيضًا يحلّل بايتات أقل. استخدم كليهما، بهذا الترتيب.
هل يكسر الضغط شيفرتي؟
المُصغِّر المُلتزم يحافظ على السلوك، إذ تُعرَض CSS بشكل متطابق، وتُبقي Terser JavaScript مكافئة. الناتج يعمل كما يعمل المصدر. تحذيران: JavaScript التي تعتمد على الإدراج التلقائي للفاصلة المنقوطة تحتاج صياغة صالحة، وHTML الحسّاسة للمسافة البيضاء مثل <pre> أو <textarea> ينبغي التحقق منها بعد الضغط.
ما الفرق بين minify وuglify؟
هما يعنيان الشيء نفسه عمليًا في JavaScript. كلمة «uglify» جاءت من UglifyJS، مُصغِّر JS الشائع المبكّر؛ وTerser فرعه الحديث الذي يدعم الصياغة الراهنة. واليوم يقول الناس «minify» على نحو عام عبر CSS وJS وHTML، بينما «uglify» اسم أقدم خاص بـ JS للعملية نفسها.
هل ينبغي أن أضغط في التطوير؟
لا. اضغط في بناءات الإنتاج، لا في التطوير. الشيفرة المضغوطة غير مقروءة وصعبة التنقيح، فأنت تريد مصدرًا كاملًا ومنسّقًا أثناء التطوير. أداة البناء لديك، مثل Vite وesbuild وwebpack، تضغط تلقائيًا حين تبني للإنتاج، غالبًا مع خرائط مصدر حتى تظلّ قادرًا على تنقيح الحزمة المنشورة.
كم يقلّص الضغط حجم الملف؟
minify وحده يقلّص CSS وJS وHTML عادةً بنحو 20–30%، أساسًا بحذف المسافات البيضاء والتعليقات وتقصير الأسماء. وحين يُطبَّق gzip أو brotli فوقه، تكون النتيجة المجتمعة كثيرًا أصغر من المصدر الأصلي بنسبة 80–90%. والرقم الدقيق يعتمد على كم مسافة بيضاء وتكرار كان في الملف.