كيفية استخدامنا ميزة "تقسيم الرموز البرمجية" و"إدراج الرموز البرمجية" و"العرض من جهة الخادم" في PROXX
في مؤتمر Google I/O لعام 2019، شحنت "ماريكو" و"جيك" و"PROXX نسخة حديثة من لعبة Minesweeper للويب. يتميز تطبيق PROXX بالتركيز على تسهيل الاستخدام (يمكنك تشغيله باستخدام قارئ شاشة) والقدرة على التشغيل على هاتف ذكي بميزات محدودة وعلى جهاز كمبيوتر مكتبي متطوّر. يتم تقييد الهواتف العادية بطرق متعددة:
- وحدات المعالجة المركزية ذات الأداء الضعيف
- وحدات معالجة الرسومات غير المتوفّرة أو الضعيفة
- الشاشات الصغيرة التي لا تتيح إدخال البيانات باللمس
- كميات محدودة جدًا من الذاكرة
ومع ذلك، تعمل هذه الأجهزة على متصفح حديث وبأسعار معقولة جدًا. لهذا السبب، تشهد الهواتف المزوّدة بميزات أساسية عودة إلى الواجهة في الأسواق الناشئة. تتيح هذه الأجهزة لشريحة جمهور جديدة تمامًا، لم تكن قادرة على شراء الأجهزة السابقة، استخدام الإنترنت والاستفادة من ميزات الويب الحديثة. تشير التوقعات إلى أنّه سيتم بيع حوالي 400 مليون هاتف عادي في الهند وحدها خلال العام 2019، لذا قد يشكّل مستخدمو الهواتف العادية جزءًا كبيرًا من جمهورك. بالإضافة إلى ذلك، تكون سرعات الاتصال مماثلة لشبكات الجيل الثاني في الأسواق الناشئة. كيف تمكّنا من جعل PROXX يعمل بشكل جيد في ظل ظروف الهواتف العادية؟
من المهم تحسين الأداء، ويشمل ذلك أداء التحميل وأداء وقت التشغيل. تبيّن أنّ الأداء الجيد يرتبط بزيادة الاحتفاظ بالمستخدمين وتحسين الإحالات الناجحة، والأهم من ذلك، زيادة الشمولية. يقدّم جيريمي واغنر المزيد من البيانات والإحصاءات حول أهمية الأداء.
هذا هو الجزء الأول من سلسلة مكوّنة من جزأين. يركّز الجزء الأول على أداء التحميل، ويركّز الجزء الثاني على أداء وقت التشغيل.
تسجيل الوضع الحالي
ويُعدّ اختبار أداء التحميل على جهاز حقيقي أمرًا بالغ الأهمية. إذا لم يكن لديك جهاز حقيقي، أنصحك باستخدام WebPageTest، خصوصًا الإعداد "البسيط". يُجري WPT مجموعة من اختبارات التحميل على جهاز حقيقي باستخدام اتصال 3G محاكي.
إنّ شبكة الجيل الثالث هي سرعة جيدة لقياسها. على الرغم من أنّك قد تكون معتادًا على شبكة الجيل الرابع أو شبكة LTE أو شبكة الجيل الخامس قريبًا، إلا أنّ الواقع في ما يتعلّق بالإنترنت على الأجهزة الجوّالة يختلف تمامًا. قد تكون على متن قطار أو في مؤتمر أو في حفل موسيقي أو على متن رحلة جوية. من المرجّح أن تكون سرعة الشبكة في هذه المناطق أقرب إلى شبكة الجيل الثالث، وفي بعض الأحيان تكون أسوأ.
ومع ذلك، سنركّز في هذه المقالة على شبكة الجيل الثاني لأنّ PROXX تستهدِف بشكل صريح الهواتف العادية والأسواق الناشئة ضمن جمهورها المستهدَف. بعد أن يُجري WebPageTest اختباره، ستظهر لك مخطّط بياني (يشبه ما يظهر لك في DevTools) بالإضافة إلى شريط صور في أعلى الصفحة. يعرض شريط الفيلم ما يراه المستخدم أثناء تحميل تطبيقك. في شبكة الجيل الثاني، تكون تجربة تحميل الإصدار غير المحسَّن من PROXX سيئة جدًا:
عند تحميل الفيديو عبر شبكة الجيل الثالث، يظهر للمستخدم شاشة بيضاء فارغة لمدة 4 ثوانٍ. عند استخدام شبكة ذات سرعة تزيد عن 2G، لا يظهر للمستخدم أي شيء على الإطلاق لأكثر من 8 ثوانٍ. إذا قرأت سبب أهمية الأداء، فأنت تعرف أننا قد فقدنا جزءًا كبيرًا من المستخدمين المحتملين بسبب نفاد الصبر. على المستخدم تنزيل كل 62 كيلوبايت من JavaScript لكي يظهر أي محتوى على الشاشة. الجانب الإيجابي في هذا السيناريو هو أنّه عندما يظهر أي عنصر على الشاشة، يصبح تفاعليًا أيضًا. لكن، من يدري؟
بعد تنزيل 62 كيلوبايت تقريبًا من JavaScript المضغوطة بتنسيق gzip وإنشاء DOM، يظهر التطبيق للمستخدم. والتطبيق تفاعلي من الناحية الفنية. ومع ذلك، يُظهر المحتوى المرئي واقعًا مختلفًا. لا تزال خطوط الويب يتم تحميلها في الخلفية، ولا يظهر أي نص للمستخدم إلى أن تصبح جاهزة. على الرغم من أنّ هذه الحالة مؤهَّلة لتكون أول مرّة يتم فيها عرض المحتوى بشكل ذي معنى (FMP)، إلا أنّها بالتأكيد لا تُعدّ تفاعلية بشكلٍ صحيح، لأنّ المستخدم لا يمكنه معرفة الغرض من أيّ من المدخلات. يستغرق التطبيق ثانية أخرى على شبكة الجيل الثالث و3 ثوانٍ على شبكة الجيل الثاني حتى يصبح جاهزًا للعمل. وبوجه عام، يستغرق التطبيق 6 ثوانٍ على شبكة الجيل الثالث و11 ثانية على شبكة الجيل الثاني ليصبح تفاعليًا.
تحليل الشلال
الآن بعد أن عرفنا ما يراه المستخدم، علينا معرفة السبب. لهذا الغرض، يمكننا الاطّلاع على مخطّط العرض المرئي وتحليل سبب تحميل الموارد بعد فوات الأوان. في عملية تتبُّع شبكة الجيل الثاني لجهاز PROXX، يمكننا ملاحظة علامتَين رئيسيتين للخطر:
- تظهر عدة خطوط رفيعة متعددة الألوان.
- تشكل ملفات JavaScript سلسلة. على سبيل المثال، لا يبدأ تحميل المورد الثاني إلا عند انتهاء المورد الأول، ولا يبدأ المورد الثالث إلا عند انتهاء المورد الثاني.
تقليل عدد عمليات الربط
يشير كل خط رفيع (dns
، وconnect
، وssl
) إلى إنشاء اتصال HTTP جديد. إنّ إعداد اتصال جديد يتطلّب وقتًا طويلاً، إذ يستغرق حوالي ثانية واحدة على شبكة الجيل الثالث ونحو 2.5 ثانية على شبكة الجيل الثاني. في العرض الإعلاني بدون انقطاع، نرى صلة جديدة لما يلي:
- الطلب رقم 1:
index.html
- الطلب رقم 5: أنماط الخطوط من
fonts.googleapis.com
- الطلب رقم 8: "إحصاءات Google"
- الطلب رقم 9: ملف خط من
fonts.gstatic.com
- الطلب رقم 14: بيان تطبيق الويب
لا يمكن تجنُّب عملية الربط الجديدة لـ index.html
. يجب أن ينشئ المتصفّح اتصالاً بخادمنا للحصول على المحتوى. يمكن تجنُّب عملية الربط الجديدة لخدمة "إحصاءات Google" عن طريق تضمين ميزة مثل Minimal Analytics، ولكنّ "إحصاءات Google" لا تحظر عرض تطبيقنا أو تفاعله، لذا لا نهتم كثيرًا بسرعة تحميله. من الناحية المثالية، يجب تحميل "إحصاءات Google" في وقت الاستراحة، عندما يكون قد تم تحميل كل المحتوى الآخر. وبهذه الطريقة، لن يتم استهلاك معدل نقل البيانات أو طاقة المعالجة أثناء التحميل الأولي. تحدد مواصفات الجلب الاتصال الجديد لبيان تطبيق الويب، لأنّه يجب تحميل البيان من خلال اتصال غير مزوّد ببيانات اعتماد. ومرة أخرى، لا يمنع بيان تطبيق الويب تطبيقنا من العرض أو أن يصبح تفاعليًا، لذلك لا نحتاج إلى هذا القدر من الاهتمام.
يشكّل الخطان وأنماطهما مشكلة لأنّهما يحظران العرض والتفاعل أيضًا. إذا اطّلعنا على ملف CSS الذي ترسله fonts.googleapis.com
، سنجد أنّه يتضمّن فقط قاعدتَي @font-face
، واحدة لكلّ خط. تتميز أنماط الخطوط بحجم صغير جدًا في الواقع، لدرجة أننا قررنا تضمينها في نموذج HTML وإزالة أحد الروابط غير الضرورية. لتجنُّب تكلفة إعداد الاتصال بملفات الخطوط، يمكننا نسخها إلى خادمنا.
التحميل الموازي
عند الاطّلاع على الرسم البياني للتحميل، يمكننا ملاحظة أنّه بعد انتهاء تحميل ملف JavaScript الأول، تبدأ الملفات الجديدة في التحميل على الفور. وهذا أمر شائع في تبعيات الوحدات. من المحتمل أن تحتوي وحدتنا الرئيسية على عمليات استيراد ثابتة، لذا لا يمكن تشغيل JavaScript حتى يتم تحميل عمليات الاستيراد هذه. من المهم معرفة أنّ هذه الأنواع من التبعيات معروفة في وقت الإنشاء. يمكننا استخدام علامات <link rel="preload">
للتأكّد من بدء تحميل جميع التبعيات في اللحظة التي نتلقّى فيها ملف HTML.
النتائج
لنلقِ نظرة على النتائج التي حقّقتها التغييرات التي أجريناها. من المهم عدم تغيير أي متغيّرات أخرى في إعداد الاختبار التي قد تؤدي إلى تشويه النتائج، لذلك سنستخدم الإعداد البسيط في WebPageTest في بقية هذه المقالة وننظر إلى شريط الأفلام:
وقد أدّت هذه التغييرات إلى تقليل وقت التفاعل مع المستخدمين من 11 ثانية إلى 8.5 ثانية، وهو ما يعادل تقريبًا مدة 2.5 ثانية لإعداد الاتصال التي كنا نهدف إلى إزالتها. أحسنت.
العرض المُسبَق
على الرغم من أننا خفّضنا مقدار TTI، إلا أننا لم نؤثّر حقًا في الشاشة البيضاء الطويلة إلى الأبد التي يجب أن يستمرّ عرضها لمدة 8.5 ثانية. يمكن القول إنّ أكبر التحسينات على FMP يمكن تحقيقها من خلال إرسال ترميز منمق في index.html
. وتشمل الأساليب الشائعة لتحقيق ذلك العرض المُسبَق والعرض من جهة الخادم، وهما مرتبطان ببعضهما بشكل وثيق ويتم شرحهما في مقالة العرض على الويب. وتُشغِّل كلتا الطريقتَين تطبيق الويب في Node وتُسلسلان بنية DOM الناتجة إلى HTML. يُجري العرض من جهة الخادم هذا الإجراء وفقًا للطلب من جهة الخادم، بينما يُجري العرض المُسبَق ذلك في وقت الإصدار ويخزِّن الناتج على أنّه index.html
الجديد. بما أنّ PROXX هو تطبيق JAMStack ولا يتضمّن أيّ وظائف من جهة الخادم، قرّرنا تنفيذ ميزة "العرض المُسبَق".
هناك العديد من الطرق لتنفيذ أداة التقديم المُسبَق. في PROXX، اخترنا استخدام Puppeteer الذي يعمل على تشغيل Chrome بدون أي واجهة مستخدم ويتيح لك التحكم عن بُعد في ذلك المثيل باستخدام Node API. ونستخدم ذلك لإدخال الترميز وJavaScript لدينا ثم قراءة DOM مرة أخرى كسلسلة من HTML. بما أنّنا نستخدم وحدات CSS، نحصل على تضمين CSS للأنماط التي نحتاجها مجانًا.
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(rawIndexHTML);
await page.evaluate(codeToRun);
const renderedHTML = await page.content();
browser.close();
await writeFile("index.html", renderedHTML);
بعد تنفيذ هذه الخطوة، يمكننا توقّع تحسين "حملات الأداء الأفضل". لا يزال علينا تحميل وتنفيذ الكمية نفسها من JavaScript كما في السابق، لذا لا نتوقع أن يتغيّر وقت التفاعل مع الصفحة كثيرًا. على العكس، زاد index.html
وقد يؤدّي ذلك إلى تأخير وقت الاستجابة للتفاعل قليلاً. هناك طريقة واحدة فقط لمعرفة ذلك: تشغيل WebPageTest.
انخفضت مدة First Meaningful Paint من 8.5 ثانية إلى 4.9 ثانية، ما يشكّل تحسّنًا كبيرًا. لا يزال وقت الاستجابة للتفاعل يحدث في غضون 8.5 ثانية تقريبًا، لذا لم يتأثّر هذا المقياس كثيرًا بهذا التغيير. لقد أجرينا هنا تغييرًا إدراكيًا. وقد يطلق البعض على ذلك اسم خفة اليد. من خلال عرض صورة متوسطة المستوى من اللعبة، سيتحسّن أداء التحميل الذي يتم رصده.
مضمَّنة
من المقاييس الأخرى التي يوفّرها لنا كلّ من DevTools وWebPageTest هو مدة تحميل أول بايت (TTFB). هذا هو الوقت المستغرَق من أول بايت من الطلب الذي يتم إرساله إلى أول بايت من الاستجابة التي يتم تلقّيها. يُشار إلى هذه الفترة أيضًا باسم "المدة بين نقطتَي البداية والنهاية" (RTT)، على الرغم من أنّ هناك فرقًا من الناحية الفنية بين هذين الرقمَين: لا تشمل المدة بين نقطتَي البداية والنهاية وقت معالجة الطلب من جهة الخادم. يعرض كلّ من DevTools وWebPageTest وقت استجابة خادم الويب بلون فاتح ضمن مجموعة الطلب/الردّ.
عند الاطّلاع على الرسم البياني للعرض المتواصل، يمكننا ملاحظة أنّ جميع الطلبات تقضي معظم وقتها في الانتظار لوصول أول بايت من الاستجابة.
وقد تم تصميم HTTP/2 Push في الأصل لحلّ هذه المشكلة. يعرف مطوّر التطبيق أنّه هناك حاجة إلى موارد معيّنة ويمكنه إرسالها. وفي الوقت الذي يدرك فيه العميل أنّه يحتاج إلى جلب موارد إضافية، تكون هذه الموارد متوفّرة في ذاكرة التخزين المؤقت للمتصفّح. تبيّن أنّ استخدام بروتوكول HTTP/2 Push يصعب تنفيذه بشكل صحيح، وهو أمر غير مستحسن. ستتم إعادة النظر في هذه المشكلة أثناء عملية توحيد HTTP/3. في الوقت الحالي، أسهل حلّ هو إدراج جميع الموارد المهمة على حساب كفاءة التخزين المؤقت.
تم تضمين ملف CSS المهمّ مسبقًا بفضل وحدات CSS وبرنامج التقديم المُسبَق المستنِد إلى Puppeteer. بالنسبة إلى JavaScript، يجب تضمين الوحدات المهمة وتبعياتها. هذه المهمة متفاوتة الصعوبة بناءً على أداة التجميع التي تستخدمها.
يقلل هذا ثانية واحدة من جهاز TTI. لقد وصلنا الآن إلى المرحلة التي يحتوي فيها index.html
على كل ما هو مطلوب للعرض الأوّلي والتفاعل. يمكن عرض HTML أثناء التنزيل، ما يؤدي إلى إنشاء FMP. في اللحظة التي يتم فيها تحليل رمز HTML وتنفيذه، يصبح التطبيق تفاعليًا.
تقسيم الرمز البرمجي بشكل مفرط
نعم، يحتوي index.html
على كل المعلومات المطلوبة للتفاعل. ولكن بعد الفحص الدقيق، تبيّن أنّه يحتوي أيضًا على كل شيء آخر. يبلغ حجم index.html
حوالي 43 كيلوبايت. لنوضّح ذلك في ما يتعلّق بما يمكن للمستخدم التفاعل معه في البداية: لدينا نموذج لضبط اللعبة يحتوي على مكوّنين وزر بدء وربما بعض الرموز البرمجية لحفظ إعدادات المستخدم وتحميلها. هذا كلّ ما عليك فعله. يبدو أن 43 كيلوبايت كبير.
لمعرفة مصدر حجم الحزمة، يمكننا استخدام مستكشِف خرائط المصدر أو أداة مشابهة لتحليل مكوّنات الحزمة. كما هو متوقّع، تحتوي الحزمة على منطق اللعبة ومحرك العرض وشاشة الفوز وشاشة الخسارة ومجموعة من الأدوات. تحتاج الصفحة المقصودة إلى مجموعة فرعية صغيرة فقط من هذه الوحدات. سيؤدي نقل كل المحتوى غير الضروري تمامًا للتفاعل إلى وحدة يتم تحميلها بشكلٍ بطيء إلى خفض وقت الاستجابة للتفاعل بشكلٍ كبير.
ما علينا فعله هو تقسيم الرمز البرمجي. تؤدي ميزة "تقسيم الرموز" إلى تقسيم الحِزمة المتكاملة إلى أجزاء أصغر يمكن تحميلها بشكل كسول عند الطلب. تتوافق أدوات تجميع الحِزم الشائعة، مثل Webpack وRollup وParcel، مع تقسيم الرموز البرمجية باستخدام import()
الديناميكي. سيحلِّل أداة تجميع الحِزم الرمز البرمجي ويُدمج بشكل مضمّن جميع الوحدات التي يتم استيرادها بشكل ثابت. سيتم وضع كل ما تستورده ديناميكيًا في ملف خاص به ولن يتم استرجاعه من الشبكة إلا بعد تنفيذ طلب import()
. بالطبع، هناك تكلفة مرتبطة بالوصول إلى الشبكة، ويجب عدم إجراء ذلك إلا إذا كان لديك الوقت الكافي. القاعدة الأساسية هنا هي استيراد الوحدات التي تكون ضرورية بشكل ثابت في وقت التحميل وتحميل كل شيء آخر بشكل ديناميكي. ولكن يجب عدم الانتظار إلى اللحظة الأخيرة لتحميل الوحدات التي سيتم استخدامها بالتأكيد بشكل بطيء. يُعدّ Idle Until Urgent الذي اقترحه Phil Walton نمطًا رائعًا لتحقيق التوازن بين التحميل البطيء والتحميل الفوري.
في PROXX، أنشأنا ملف lazy.js
يستورد بشكل ثابت كل ما لا نحتاج إليه. في ملفنا الرئيسي، يمكننا بعد ذلك استيراد lazy.js
ديناميكيًا. ومع ذلك، انتهى المطاف ببعض مكوّنات Preact في الإصدار lazy.js
، وتبيّن أن ذلك كان معقّدًا بعض الشيء، إذ لا يمكن لـ Preact التعامل مع المكوّنات الكسولة التي يتم تحميلها خارج العلبة. لهذا السبب، كتبنا رمزًا برمجيًا صغيرًا لتغليف المكوّن deferred
يتيح لنا عرض عنصر نائب إلى أن يتم تحميل المكوّن الفعلي.
export default function deferred(componentPromise) {
return class Deferred extends Component {
constructor(props) {
super(props);
this.state = {
LoadedComponent: undefined
};
componentPromise.then(component => {
this.setState({ LoadedComponent: component });
});
}
render({ loaded, loading }, { LoadedComponent }) {
if (LoadedComponent) {
return loaded(LoadedComponent);
}
return loading();
}
};
}
بعد تنفيذ ذلك، يمكننا استخدام وعد لمكوّن في دوال render()
. على سبيل المثال، سيتم استبدال المكوّن <Nebula>
الذي يعرض صورة الخلفية المتحركة بعنصر <div>
فارغ أثناء تحميل المكوّن. بعد تحميل المكوِّن وجاهزيته للاستخدام، سيتم استبدال <div>
بالمكوِّن الفعلي.
const NebulaDeferred = deferred(
import("/components/nebula").then(m => m.default)
);
return (
// ...
<NebulaDeferred
loading={() => <div />}
loaded={Nebula => <Nebula />}
/>
);
بعد تنفيذ كل هذه الإجراءات، تم تقليل حجم index.html
إلى 20 كيلوبايت فقط، أي أقل من نصف الحجم الأصلي. ما هو تأثير ذلك في ميزة "الإعلانات أثناء عرض الفيديو" وميزة "الإعلانات أثناء التشغيل"؟ سيخبرك WebPageTest بذلك.
لا يفصل بين وقت بدء عرض الإعلان ووقت ظهوره سوى 100 ملي ثانية، لأنّ الأمر لا يتطلّب سوى تحليل JavaScript المضمّن وتنفيذه. وبعد 5.4 ثانية على شبكة الجيل الثاني فقط، يصبح التطبيق تفاعليًا بالكامل. يتم تحميل جميع الوحدات الأخرى الأقل أهمية في الخلفية.
مزيد من خفة اليد
إذا اطّلعت على قائمة الوحدات الملحّة أعلاه، ستلاحظ أنّ محرّك العرض ليس جزءًا من الوحدات الملحّة. بالطبع، لا يمكن بدء اللعبة إلا بعد أن نحصل على محرّك التقديم لعرض اللعبة. يمكننا إيقاف زر "بدء" إلى أن يصبح محرّك العرض جاهزًا لبدء اللعبة، ولكن من خلال تجربتنا، يستغرق المستخدم عادةً وقتًا كافيًا لضبط إعدادات اللعبة، لذا ليس من الضروري إجراء ذلك. في معظم الأوقات، يتم تحميل محرك العرض والوحدات الأخرى المتبقية قبل أن يضغط المستخدم على "بدء". في الحالة النادرة التي يكون فيها المستخدم أسرع من اتصاله بالشبكة، نعرض شاشة تحميل بسيطة تنتظر اكتمال الوحدات المتبقية.
الخاتمة
القياس مهم. لتجنُّب إضاعة الوقت على مشاكل غير حقيقية، ننصحك دائمًا بإجراء عمليات القياس أولاً قبل تنفيذ التحسينات. بالإضافة إلى ذلك، يجب إجراء القياسات على الأجهزة الحقيقية التي تتصل بشبكة الجيل الثالث أو على WebPageTest إذا لم يكن لديك جهاز حقيقي.
يمكن أن يقدِّم شريط الصور إحصاءات عن شعور تحميل التطبيق لدى المستخدم. يمكن أن يُطلعك العرض الإعلاني بدون انقطاع على الموارد المسؤولة عن مُدد التحميل الطويلة المحتملة. في ما يلي قائمة تحقُّق بشأن الإجراءات التي يمكنك اتخاذها لتحسين أداء التحميل:
- أرسِل أكبر عدد ممكن من مواد العرض من خلال عملية اتصال واحدة.
- التحميل المُسبَق أو حتى الموارد المضمّنة المطلوبة لعملية العرض الأولى والتفاعل
- يمكنك تحسين أداء التحميل المُلاحظ من خلال معالجة التطبيق مسبقًا.
- استخدِم تقسيم الرموز البرمجية بشكل مكثّف لتقليل مقدار الرمز البرمجي المطلوب للتفاعل.
يُرجى متابعتنا في الجزء 2 حيث سنناقش كيفية تحسين أداء وقت التشغيل على الأجهزة ذات القيود الشديدة.