دفتر · ملاحظة 001 · أداء الويب

أين كانت تختبئ آخر 18 نقطة في موقع ثابت

ضغطنا الصور، وقسمنا JavaScript إلى ثلاثة ملفات، ولم نستخدم إطار عمل. مع ذلك ظل تقييم Lighthouse عند 82. توزعت النقاط الباقية على ثلاثة مواضع سمّتها الأداة ولم تشرحها.

Serhat Belen

مؤسس BLN Global · · قراءة 7 دقائق

الجواب المختصر

كانت الصور وJavaScript بحالة جيدة، وبقيت النقاط في ثلاثة مواضع. أولًا: كان فحص تجاوز العمل غير الضروري يقرأ التخطيط ويجبر المتصفح على الحساب. ثانيًا: أخّر ملف الأنماط والخطوط الخارجية أول رسم بمقدار 600 ms. أدرجنا CSS داخل الصفحة ونقلنا الخطوط إلى خادمنا. ثالثًا: كانت 6 من أصل 24 ملاحظة تباين خاطئة. ثم كشف الفحص الكامل 13 صفحة لم تظهر في العينة.

لا يستخدم هذا الموقع إطار عمل. تنشئ نصوص Python صفحاته بصيغة HTML عادية. التنسيق في ملف CSS واحد، وJavaScript في ثلاثة ملفات صغيرة. وننتج صور WebP بثلاثة أحجام. أي أن خطوات الأداء الواضحة كانت مطبقة بالفعل.

توقفت نتيجة الجوال عند 82.

بعد إصلاح المشكلات الواضحة، لم تختبئ النقاط الباقية في خطأ كبير واحد. توزعت على ثلاثة مواضع. وكان العثور عليها يتطلب فهم تحذير الأداة، لا مجرد قراءته. ما الذي يقصده تطلّب فهمه.

المرتبة الأولى: فحص كتبناه كي نتجاوز العمل نفسه

يعني تحذير «إعادة التدفق القسرية» (forced reflow) في Lighthouse أن JavaScript يجبر المتصفح على حساب تخطيط الصفحة قبل أوانه. يجمع المتصفح تغييرات التنسيق ثم يطبقها دفعة واحدة. لكن قراءة أحد القياسات في المنتصف تجبره على تطبيق ما جمعه فورًا.

أظهر التنبيه سطر المصدر، لكن السطر لم يحمل قياسًا مرئيًا. وجدناه في ثلاث جولات.

  • في الجولة الأولى getComputedStyle حذفنا الاستدعاءات. لم تتغير النتيجة.
  • في الجولة الثانية ظهر السبب الحقيقي: كود كتبناه لتجاوز العمل غير الضروري if (window.scrollY > 0) الفحص. الفحص نفسه يقرأ التخطيط. السطر الذي كتبناه لتجاوز العمل كان ينفذ العمل المطلوب تجاوزه.
  • في الجولة الثالثة حذفنا الدالة بالكامل. كان CSS يحدد حالة الفتح أصلًا. لم يحتج JavaScript إلى إنشائها من جديد.

وهناك نمط متبقٍ void element.offsetWidth كان حيلة شائعة ومقبولة تفرض حساب التخطيط عمدًا لإعادة تشغيل الانتقال. استبدلناه باثنين متتاليين من requestAnimationFrame أضفناه. يؤدي الغرض نفسه ولا يضغط التخطيط.

function yenidenBaslat(fn) { /* إذا كان التبويب في الخلفية فلن يعمل rAF وسيبقى العرض فارغًا. */ if (document.hidden) { fn(); return; } requestAnimationFrame(function () { requestAnimationFrame(fn); }); }
أعد تشغيل الانتقال دون فرض حساب التخطيط. أضف مسارًا بديلًا لتبويب الخلفية، لأن rAF لا يعمل فيه.

الدرس هنا: غالبًا لا يخبرك تنبيه الأداء بأنك تنفذ عملًا زائدًا. يخبرك بأنك تنفذه في توقيت خاطئ.

المرتبة الثانية: أول 600 ms قبل الرسم الأول

كان التحذير الثاني عن الموارد التي تؤخر أول رسم. عند فتح الصفحة، كان المتصفح ينزّل ملف الأنماط ثم الخطوط ويعالجهما قبل أن يرسم أي شيء. وسجّلنا تأخيرًا قدره 600 مللي ثانية.

أدخلنا ملف التنسيق داخل الصفحة

في الزيارة الثانية يأتي ملف CSS المنفصل من الذاكرة المخبأة، وهذه نقطة لصالحه. لكن في الزيارة الأولى يضيف طلبًا وردًا. وهذه هي زيارة القادم من نتيجة البحث. عند النشر نصغّر CSS ونضعه داخل الصفحة. قسنا ذلك: بعد ضغط Brotli كان الحجم المضمّن أصغر من مجموع الملف المنفصل والطلب الثاني.

نقلنا الخطوط إلى خادمنا

كان إعداد Google Fonts يتصل بمصدرين مختلفين: واحد للنمط fonts.googleapis.com، للملفات fonts.gstatic.com. يحتاج كل مصدر جديد إلى حل DNS ومصافحة TLS واتصال جديد. وهذه نتيجة إزالته بالأرقام:

250 KB165 KB

ملفات الخط التي تحمّلها الصفحة التركية. بدلًا من 6 ملفات و2 مصدرين خارجيين، تحمّل 3 ملفات من مصدر واحد.

كيف قسناه جلبنا CSS الخاصة بـ Google Fonts بهوية Chrome حقيقية، unicode-range قرأنا حقول unicode-range لعزل المجموعات الفرعية التي ستنزّلها الصفحة التركية فعلًا، latin + latin-ext، ثم قسنا أحجام الملفات واحدًا واحدًا. الطرف المقابل في هذا المستودع assets/fonts الحجم الفعلي لملفاته.

كيف صغّرنا الخطوط، وأين دفعنا الثمن عند فتحها لعشر لغات، في ملاحظة أخرى: عشر لغات، ثلاثة ملفات خطوط.

الموضع الثالث: حيث أخطأت الأداة

رصد فحص إمكانية الوصول 24 تحذيرًا بشأن تباين الألوان. لو أصلحناها كلها على أنها صحيحة، لتضرر نصف التصميم. كان 6 منها غير صحيح.

السبب content-visibility:auto كان كذلك. تؤجل هذه الخاصية معالجة الأجزاء خارج الشاشة، فتسرّع الرسم الأول. وهذا ما أردناه. لكن أداة التدقيق لم تتلقَّ اللون المحسوب للجزء المؤجل، فأبلغت عن «أبيض فوق أبيض». لم يظهر ذلك على الشاشة.

قسنا نسب التباين الفعلية في المتصفح، واحدة تلو الأخرى. كان 18 تحذيرًا صحيحًا فأصلحناها. أما 6 فكانت من نقاط ضعف الأداة. content-visibility حذفناها من الجزأين اللذين فيهما مشكلة فقط، وأبقيناها في البقية.

تحذير أداة الفحص لا يثبت وجود خطأ يجب إصلاحه. إنه ادعاء. تحقّق منه أولًا، ثم أصلحه. تكرر هذا الدرس مرتين أخريين في هذا الموقع. إحداهما في ملاحظة تدقيق اللغة يشرح ذلك.

وهناك جرح صنعناه بأنفسنا

لكل الصور لتفادي انزياح التخطيط width و height أضفنا السمة. خطوة صحيحة. يستطيع المتصفح حجز مكان الصورة قبل تنزيلها. لكن صناديق العرض التجريبي انكسرت: ظهرت بعض الصور بعرض 58 بكسل وارتفاع 1500 بكسل.

السبب هو: الموجود في HTML width و height السمات تعطي تلميح عرض، وفي CSS height يملؤون قيمته. وفي التصميم، لا يوجد لتلك الخانات سوى aspect-ratio كان محددًا، height كان فارغًا. استقرّت السمة هناك وكسرت النسبة. إصلاح بسطر واحد:

img{max-width:100%;height:auto;display:block}
تبقى السمات في مكانها وتواصل منع انزياح التخطيط. يستعيد CSS الارتفاع.

وهنا يفيد الفحص فعلًا

حين وصلت الصفحة الرئيسية إلى 100 بدا أن العمل انتهى. لم ينته. ما قسناه كان عينة. الموقع جميع صفحاته الـ150 فحصنا الصفحات واحدة واحدة في Lighthouse. النتيجة: 137 صفحة سجّلت 100 في العناوين الأربعة، ليست 13 صفحة. سجّلت كل صفحة 100 في إتاحة الوصول وأفضل الممارسات وSEO. الانحراف ظهر في الأداء فقط.

كانت إحدى عشرة صفحة من ثلاث عشرة باللغة نفسها: العربية. أدناها 83.

/ar/safecoast/ 83 /ar/laya/ 84 /ar/localmind/ 84 /ar/adforge/ 85 /ar/garanti/ 85 /ar/gtin/ 86 /ar/visionguard/ 86 /ar/ 89 /ar/growthos/ 97 /ar/dream/ 99 /ar/twin/ 99
هذه الصفحات العربية التي ظهرت في الفحص الكامل. بقي انحرافان في /pl/modelmesh/ و/ru/terms.html، وكلاهما 98. يقعان ضمن التذبذب الطبيعي بين القياسات.

ذكر Lighthouse السبب مباشرة: حمّل المتصفح خط الويب ← ar.woff2. كان خط العربية يتأخر فيجبر الصفحة على ترتيب نفسها من جديد. كانت مقاسات الخط البديل للعربية بعيدة جدًا عن Noto Sans Arabic، لذلك ظهر الانزياح كبيرًا. الإصلاح كان القرار نفسه الذي اتخذناه في Martian Mono: swap ليس اختياري.

0,2870

انزياح التخطيط (CLS) في الصفحات العربية

كيف قسناه بعد التغيير، قسنا 15 صفحة عربية من جديد: CLS صفر في الصفحات كلها. ارتفعت درجات الأداء من نطاق 83-89 إلى 98-100، ووصلت 9 صفحات من أصل 15 إلى 100 كاملة. أخذنا القياس على الموقع المباشر باستخدام Lighthouse.

الدرس هنا ليس عن الأداء. أن تفحص عينة ثم تقول إن الموقع 100 يعني أنك تدّعي شيئًا عن صفحة لم تقسها. بقيت خمس عشرة صفحة على 83 نقطة لأشهر لهذا السبب. لم ننظر إليها أصلًا.

حدود النتيجة

النتيجة قياس مختبري يفترض جهازًا وشبكة ثابتين. قد يكون هاتف الزائر الفعلي واتصاله أبطأ. وعند قياس الصفحة نفسها مرات متتالية تتراوح النتيجة بين 98 و100. حصولها على 100 لا يعني أنها سريعة للجميع، بل إننا أزلنا العوائق التي يمكن قياسها. بعد تراكم بيانات المستخدمين الفعليين، سنعتمد على بيانات Chrome الميدانية.

القاسم المشترك بين هذه التعديلات الثلاثة أنها لم تضف عملًا أكثر. كل تعديل حذف عملًا يحدث في وقت خاطئ أو لا يجب أن يحدث أصلًا.

الأسئلة الشائعة

ماذا يقصد Lighthouse بإعادة التدفق القسرية؟

أن تجبر JavaScript المتصفح على حساب تغييرات الأنماط قبل موعدها. يجمع المتصفح التغييرات ويطبقها دفعة واحدة. وإذا قرأ قياسًا بينها، يضطر إلى تنفيذ ما جمعه فورًا. offsetWidth, getBoundingClientRect, scrollY و getComputedStyle كل عمليات القراءة تشغّل هذا.

هل تضمين CSS في الصفحة صحيح دائمًا؟

لا. الملف المنفصل يأتي من الذاكرة المخبأة في الزيارة الثانية، وهذه نقطة لصالحه. التضمين يسرّع الزيارة الأولى ويبطئ الزيارات المتكررة. اختر حسب الموقع: إذا كان أغلب الزوار يأتون من نتائج البحث، فالتضمين يفيد. قسناه عندنا، ونجح.

هل أتوقف عن استخدام content-visibility؟

لا. تأجيل معالجة الأجزاء خارج الشاشة يسرّع أول رسم فعلًا. المشكلة ليست في الخاصية. أداة الفحص لا تستطيع قراءة لون الجزء المؤجل. يكفي أن تلغيها في الأجزاء التي تسجّلها الأداة خطأ.

هل تعني 100 نقطة أن الموقع سريع فعلًا؟

لا. هذا قياس مختبري يفترض جهازًا وشبكة ثابتين. عند قياس الصفحة نفسها مرات متتالية، تتراوح النتيجة بين 98 و100. تظهر القيمة الفعلية في بيانات Chrome الميدانية، ويحتاج جمعها إلى وقت.

لماذا يجب قياس كل الصفحات بدل أخذ عينة؟

لدينا، لم تظهر 13 صفحة من أصل 150 في العينة أصلًا، وكانت 11 صفحة باللغة نفسها. الصفحة الرئيسية وصلت إلى 100، بينما بقيت الصفحات العربية عند 83. لا تدّعِ شيئًا عن صفحة لم تقِسها.

المصادر

أعمال ذات صلة