طريقة استخدامنا لتقسيم الرموز ودمج الرموز والعرض من جهة الخادم في PROXX
في مؤتمر Google I/O لعام 2019، شحنت "ماريكو" و"جيك" و"PROXX نسخة حديثة من لعبة Minesweeper للويب. ما يميّز PROXX هو التركيز على تسهيل الاستخدام (يمكنك تشغيله باستخدام قارئ شاشة) بالإضافة إلى إمكانية تشغيله على هاتف عادي مثلما يعمل على جهاز كمبيوتر مكتبي متطوّر. يتم تقييد الهواتف العادية بطرق متعددة:
- وحدات معالجة مركزية (CPU) ضعيفة
- وحدات معالجة الرسومات ضعيفة أو غير متوفّرة
- شاشات صغيرة بدون الإدخال باللمس
- الذاكرة محدودة
لكنها تشغل متصفحًا حديثًا وبأسعار معقولة للغاية. لهذا السبب، تزدهر الهواتف العادية من جديد في الأسواق الناشئة. وبفضل نقطة السعر هذه، أصبح بإمكان جمهور جديد لم يكن قادرًا على تحمّل تكلفة هذا الاشتراك على الإنترنت والاستفادة من شبكة الإنترنت الحديثة. بالنسبة إلى عام 2019، من المتوقّع أن يتم بيع حوالي 400 مليون هاتف عادي في الهند وحدها، لذلك قد يصبح مستخدمو الهواتف العادية جزءًا مهمًا من جمهورك. بالإضافة إلى ذلك، تُعد سرعات الاتصال بالإنترنت المشابهة لشبكة الجيل الثاني هي القاعدة في الأسواق الناشئة. كيف تمكّنا من جعل PROXX يعمل بشكل جيد في ظل ظروف الهواتف العادية؟
فالأداء مهم، ويشمل ذلك أداء التحميل وأداء بيئة التشغيل. فقد تبيّن أنّ الأداء الجيد يرتبط بزيادة معدل الاحتفاظ بالمستخدمين وتحسين الإحالات الناجحة، والأهم من ذلك، زيادة الشمولية. يقدّم جيريمي واغنر المزيد من البيانات والإحصاءات حول أهمية الأداء.
هذا هو الجزء 1 من سلسلة مكونة من جزأين. يركّز الجزء الأول على أداء التحميل، ويركّز الجزء الثاني على أداء وقت التشغيل.
التقاط الوضع الراهن
ويُعدّ اختبار أداء التحميل على جهاز حقيقي أمرًا بالغ الأهمية. إذا لم يكن لديك جهاز حقيقي في الوقت الحالي، أنصحك باستخدام WebPageTest، وتحديدًا "البسيط" الإعداد. تنشئ خدمة WPT اختبارًا شاملاً لاختبارات التحميل على جهاز حقيقي يتضمّن اتصال شبكة الجيل الثالث في وضع المحاكاة.
وتمثل شبكة الجيل الثالث سرعة جيدة في القياس. على الرغم من أنّكم معتادين على استخدام شبكات الجيل الرابع (4G) أو LTE أو حتى الجيل الخامس (5G) قريبًا، إلا أنّ واقع الإنترنت في الأجهزة الجوّالة يبدو مختلفًا تمامًا. وربما في القطار أو في مؤتمر أو في حفلة موسيقية، أو في رحلة جوية. وما الذي ستواجهه هناك على الأرجح بالقرب من شبكة الجيل الثالث، وقد أسوأ من ذلك أحيانًا.
مع ذلك، سنركّز في هذه المقالة على شبكة الجيل الثاني لأنّ PROXX تستهدِف بشكل صريح الهواتف العادية والأسواق الناشئة ضمن جمهورها المستهدَف. بعد انتهاء اختبار WebPageTest، ستحصل على عرض إعلاني بدون انقطاع (على غرار ما تراه في أدوات مطوّري البرامج) بالإضافة إلى شريط صور في الأعلى. يعرض شريط الصور ما يراه المستخدم أثناء تحميل تطبيقك. بالنسبة إلى شبكات الجيل الثاني، ستكون تجربة تحميل الإصدار غير المحسَّن من PROXX سيئة جدًا:
عند التحميل عبر شبكة الجيل الثالث، يرى المستخدم 4 ثوانٍ من عدم وجود أي ألوان. فأكثر من الجيل الثاني، لا يظهر للمستخدم أي شيء على الإطلاق لمدة تزيد عن 8 ثوانٍ. إذا قرأت سبب أهمية الأداء، فأنت تعرف أننا قد فقدنا جزءًا كبيرًا من المستخدمين المحتملين بسبب نفاد الصبر. ويحتاج المستخدم إلى تنزيل كل محتوى JavaScript الذي يبلغ 62 كيلوبايت حتى يظهر على الشاشة. ما يحفّز في هذا السيناريو هو أن أي شيء ثاني يظهر على الشاشة يكون تفاعليًا أيضًا. لكن، من يدري؟
بعد أن تم تنزيل حوالي 62 كيلوبايت من لغة JavaScript gzip'd وتم إنشاء 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 في بقية هذه المقالة وألقِ نظرة على شريط الصور:
خفّضت هذه التغييرات مقياس TTI من 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);
وبعد تنفيذ ذلك، نتوقّع تحسّنًا في "منصّة عرض إعلانات الفيديو" (FMP). ما زلنا نحتاج إلى تحميل وتنفيذ القدر نفسه من محتوى JavaScript كما في السابق، لذا لا يمكن أن نتوقع حدوث تغيير كبير في TTI. إذا كان هناك أي شيء، فإن index.html
قد أصبح أكبر حجمًا وقد يعرقل برنامج TTI قليلاً. هناك طريقة واحدة فقط لمعرفة ذلك وهي: تشغيل WebPageTest.
لقد ارتفع مقياس "سرعة عرض أوّل محتوى مفيد" من 8.5 ثانية إلى 4.9 ثانية، ما ساهم في تحسين كبير. لا يزال مؤشر TTI يحدث عند حوالي 8.5 ثانية، لذلك لم يتأثر إلى حد كبير بهذا التغيير. ما فعلناه هنا هو تغيير تحسي. وقد يطلق عليه البعض خفة اليد. من خلال عرض صورة متوسطة المستوى من اللعبة، سيتحسّن أداء التحميل الذي يتم رصده.
مضمَّنة
يُعدّ وقت تشغيل أول بايت (TTFB) مقياسًا آخر توفّره لنا "أدوات مطوري البرامج" وWebPageTest. يشير ذلك إلى الوقت المُستغرَق بدايةً من البايت الأول من الطلب الذي يتم إرساله إلى البايت الأول من الاستجابة التي يتم تلقّيها. ويُعرف هذا الوقت أيضًا باسم "وقت الذهاب والعودة" (RTT)، على الرغم من أنّ هناك فرقًا من الناحية الفنية بين هذين الرقمَين: لا تشمل هذه المدة وقت معالجة الطلب من جهة الخادم. تعرض DevTools وWebPageTest مرئيات TTFB بلون فاتح داخل كتلة الطلب/الاستجابة.
من خلال الاطّلاع على العرض الإعلاني بدون انقطاع، يمكننا ملاحظة أنّ جميع الطلبات تستغرق معظم وقتها في الانتظار حتى وصول البايت الأول من الردّ.
كانت هذه المشكلة هي ما كان متوقعًا في الأصل من خلال HTTP/2 Push. يعرف مطوّر التطبيق أنّ هناك حاجة إلى موارد معيّنة ويمكنه دفعها إلى الأسفل. وبحلول الوقت الذي يدرك فيه العميل أن الأمر يحتاج إلى جلب موارد إضافية، فإنها تكون موجودة بالفعل في ذاكرات التخزين المؤقت للمتصفح. تبيّن أنّ استخدام بروتوكول HTTP/2 Push يصعب تنفيذه بشكل صحيح، وهو أمر غير مستحسن. ستتم إعادة النظر في مساحة المشكلة هذه أثناء توحيد HTTP/3. في الوقت الحالي، الحل الأسهل هو تضمين جميع الموارد المهمة على حساب كفاءة التخزين المؤقت.
تم دمج خدمة مقارنة الأسعار (CSS) المهمة بفضل وحدات CSS والعرض المُسبَق المستنِد إلى Puppeteer. بالنسبة إلى JavaScript، يجب تضمين الوحدات المهمة وتبعياتها. هذه المهمة متفاوتة الصعوبة بناءً على أداة التجميع التي تستخدمها.
يقلل هذا ثانية واحدة من جهاز TTI. لقد وصلنا الآن إلى النقطة التي يحتوي فيها index.html
على كل ما يلزم للعرض الأولي ويصبح تفاعليًا. يمكن عرض HTML أثناء التنزيل، ما يؤدي إلى إنشاء FMP. في اللحظة التي يتم فيها تحليل HTML وتنفيذه، يصبح التطبيق تفاعليًا.
التقسيم الصارم للرموز
نعم، يحتوي index.html
على كل المعلومات المطلوبة للتفاعل. ولكن عند الفحص الدقيق، يتضح أنها تحتوي أيضًا على كل العناصر الأخرى. يبلغ حجم index.html
حوالي 43 كيلوبايت. لنشير إلى ذلك في ما يتعلق بما يمكن للمستخدم التفاعل معه في البداية: لدينا نموذج لإعداد اللعبة يحتوي على مكوّنَين وزر بدء وربما بعض الرموز للاحتفاظ بها وتحميل إعدادات المستخدم. هذا تقريبًا. يبدو أن 43 كيلوبايت كبير.
لمعرفة مصدر حجم الحزمة، يمكننا استخدام مستكشِف خرائط المصدر أو أداة مشابهة لتحليل مكوّنات الحزمة. وكما هو متوقّع، تحتوي الحِزمة على منطق اللعبة ومحرك العرض وشاشة الفوز وشاشة فقدان البيانات ومجموعة من الأدوات المساعدة. تحتاج الصفحة المقصودة إلى مجموعة فرعية صغيرة فقط من هذه الوحدات. وعند نقل كل العناصر غير المطلوبة بشدة للتفاعل إلى وحدة ذات تحميل كسول، سيؤدي ذلك إلى خفض تأثير TTI إلى حد كبير.
ما علينا فعله هو تقسيم الرمز. يعمل تقسيم الرمز على تقسيم الحِزمة المتجانسة إلى أجزاء أصغر يمكن تحميلها عند الطلب باستخدام طريقة التحميل الكسول. تتوافق برامج الحِزم الرائجة، مثل Webpack وRollup وParcel، مع تقسيم الرموز باستخدام نظام import()
الديناميكي. تعمل أداة التجميع على تحليل الرمز البرمجي وتضمين جميع الوحدات التي يتم استيرادها ثابتًا. سيتم وضع كل ما تستورده ديناميكيًا في ملفه الخاص ولن يتم جلبه من الشبكة إلا بعد تنفيذ استدعاء import()
. بطبيعة الحال، سيكون الوصول إلى الشبكة مكلفًا وينبغي لك القيام بذلك فقط إذا كان لديك متسع من الوقت. الشعار هنا هو أن يتم بشكل ثابت استيراد الوحدات المطلوبة بشكل أساسي في وقت التحميل وتحميل الوحدات الأخرى ديناميكيًا. مع ذلك، يجب ألا تنتظر حتى آخر لحظة لكي يتم تحميل الوحدات التي سيتم استخدامها من خلال التحميل الكسول. يقدّم كتاب Phil Walton نمط Idle Until Urgent خيارًا رائعًا لتوفير بيئة وسطية سليمة بين التحميل الكسول والتحميل السريع.
أنشأنا في 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 كيلوبايت فقط، أي أقل من نصف الحجم الأصلي. ما هو تأثير ذلك على FMP وTTI؟ سيشرح لك WebPageTest ما يلي:
لا يفصل بين FMP وTTI سوى 100 ملّي ثانية، لأنّ الأمر لا يتطلّب سوى تحليل وتنفيذ محتوى JavaScript المضمّن. وبعد 5.4 ثانية على شبكة الجيل الثاني فقط، يصبح التطبيق تفاعليًا بالكامل. ويتم تحميل جميع الوحدات الأخرى الأقل أهمية في الخلفية.
مزيد من خفة اليد
إذا نظرت إلى قائمة الوحدات المهمة أعلاه، سترى أن محرك العرض ليس جزءًا من الوحدات المهمة. لا يمكن أن تبدأ اللعبة إلا بعد أن يتوفّر لدينا محرّك العرض الذي يتيح عرض اللعبة. يمكننا تعطيل وحدة "البدء" إلى أن يصبح محرك العرض جاهزًا لبدء اللعبة، ولكن وفقًا لتجربتنا، عادةً ما يستغرق المستخدم وقتًا كافيًا لضبط إعدادات اللعبة بحيث لا يكون هذا ضروريًا. في معظم الأوقات، يتم تحميل محرك العرض والوحدات الأخرى المتبقية قبل أن يضغط المستخدم على "بدء". وفي حالات نادرة عندما يكون المستخدم أسرع من اتصال الشبكة، نعرض شاشة تحميل بسيطة تنتظر حتى انتهاء الوحدات المتبقية.
الخاتمة
القياس مهم. لتجنّب قضاء وقت في حلّ المشاكل غير الواقعية، ننصح دائمًا بالقياس أولاً قبل تنفيذ عمليات التحسين. بالإضافة إلى ذلك، يجب إجراء القياسات على الأجهزة الحقيقية المتصلة بشبكة الجيل الثالث أو على WebPageTest إذا لم يكن هناك أي جهاز فعلي في متناول اليد.
يمكن أن يقدِّم شريط الصور إحصاءات عن شعور تحميل التطبيق لدى المستخدم. يمكن أن يُطلعك العرض الإعلاني بدون انقطاع على الموارد المسؤولة عن مُدد التحميل الطويلة المحتملة. في ما يلي قائمة تحقُّق بشأن الإجراءات التي يمكنك اتخاذها لتحسين أداء التحميل:
- أرسِل أكبر عدد ممكن من مواد العرض من خلال عملية ربط واحدة.
- التحميل المُسبق أو حتى الموارد المضمَّنة المطلوبة لعملية العرض والتفاعل الأولى
- يمكنك عرض تطبيقك مسبقًا لتحسين أداء التحميل بشكل ملحوظ.
- استفِد من تقسيم الرموز بشكل صارم لتقليل مقدار الرموز اللازمة للتفاعل.
يُرجى متابعتنا لمعرفة الجزء الثاني الذي سنناقش فيه كيفية تحسين أداء وقت التشغيل على الأجهزة ذات القيود الفائقة.