كيفية استخدامنا ميزة "تقسيم الرموز البرمجية" و"إدراج الرموز البرمجية" و"العرض من جهة الخادم" في PROXX
في مؤتمر Google I/O لعام 2019، طرحنا أنا وماريكو وجيك PROXX، وهو نسخة حديثة من لعبة "ألغام الإنترنت" مخصّصة للويب. يتميز تطبيق PROXX بالتركيز على تسهيل الاستخدام (يمكنك تشغيله باستخدام قارئ شاشة) والقدرة على التشغيل على هاتف ذكي عادي وعلى جهاز كمبيوتر مكتبي متقدم. تواجه الهواتف العادية قيودًا متعددة:
- وحدات المعالجة المركزية ذات الأداء الضعيف
- وحدات معالجة الرسومات الضعيفة أو غير المتوفّرة
- الشاشات الصغيرة التي لا تتيح إدخال البيانات باللمس
- كميات محدودة جدًا من الذاكرة
ومع ذلك، تعمل هذه الأجهزة على متصفح حديث وبأسعار معقولة جدًا. لهذا السبب، تشهد الهواتف المزوّدة بميزات أساسية عودة إلى الواجهة في الأسواق الناشئة. تتيح هذه الأجهزة لشريحة جمهور جديدة تمامًا، لم تكن قادرة على شراء الأجهزة السابقة، استخدام الإنترنت والاستفادة من ميزات الويب الحديثة. تشير التوقعات إلى أنّه سيتم بيع حوالي 400 مليون هاتف عادي في الهند وحدها خلال العام 2019، لذا قد يشكّل المستخدمون الذين يستخدمون الهواتف العادية جزءًا كبيرًا من جمهورك. بالإضافة إلى ذلك، تكون سرعات الاتصال مماثلة لشبكات الجيل الثاني في الأسواق الناشئة. كيف تمكّنا من جعل PROXX يعمل بشكل جيد في ظروف الهواتف المزوّدة بميزات أساسية؟
من المهم تحسين الأداء، ويشمل ذلك أداء التحميل وأداء وقت التشغيل. تبيّن أنّ الأداء الجيد يرتبط بزيادة الاحتفاظ بالمستخدمين وتحسين الإحالات الناجحة، والأهم من ذلك، زيادة الشمولية. يقدّم جيريمي واغنر بيانات وإحصاءات أكثر حول أهمية الأداء.
هذا هو الجزء الأول من سلسلة مكوّنة من جزأين. يركّز الجزء 1 على أداء التحميل، وسيركّز الجزء 2 على أداء وقت التشغيل.
تسجيل الوضع الحالي
من المهم اختبار أداء التحميل على جهاز حقيقي. إذا لم يكن لديك جهاز حقيقي، أنصحك باستخدام 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، يجب تضمين الوحدات المهمة وعناصرها الملحقة. تختلف صعوبة هذه المهمة حسب أداة تجميع الحِزم التي تستخدمها.
وقد أدّى ذلك إلى تقليل وقت الاستجابة بمقدار ثانية واحدة. لقد وصلنا الآن إلى المرحلة التي يحتوي فيها 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 حيث سنناقش كيفية تحسين أداء وقت التشغيل على الأجهزة ذات القيود الشديدة.