كيف استخدمنا تقسيم الرموز البرمجية وتضمين الرمز والعرض من جهة الخادم في PROXX.
في مؤتمر Google I/O لعام 2019، شحنتُ مع "ماريكو" و"جيك" وPROXX، وهو مستنسخة حديثة من كنسية الألغام على الويب. ما يميّز PROXX هو التركيز على تسهيل الاستخدام (يمكنك تشغيله باستخدام قارئ الشاشة) والقدرة على تشغيله أيضًا على الهواتف العادية والأجهزة المكتبية المتطوّرة. يتم تقييد الهواتف العادية بعدة طرق:
- وحدات المعالجة المركزية (CPU) الضعيفة
- وحدات معالجة الرسومات ضعيفة أو غير موجودة
- الشاشات الصغيرة بدون الإدخال باللمس
- مساحة محدودة جدًا من الذاكرة
ولكن يتم تشغيلهم متصفحًا حديثًا وبأسعار معقولة. لهذا السبب، عادت الهواتف العادية انتشارًا جديدًا في الأسواق الناشئة. بفضل سعرها، يمكن لجمهور جديد كليًا لم يكن يتحمّل التكاليف في السابق أن يدخل إلى الإنترنت والاستفادة من شبكة الويب الحديثة. من المتوقع أن يتم بيع ما يقرب من 400 مليون هاتف عادي في عام 2019 في الهند وحدها، لذلك قد يشكّل مستخدمو الهواتف العادية جزءًا كبيرًا من جمهورك. فضلاً عن ذلك، تُعتبر سرعات الاتصال المشابهة لشبكة الجيل الثاني القاعدة المعتادة في الأسواق الناشئة. كيف تمكنا من جعل PROXX يعمل بشكل جيد في ظل ظروف الهواتف العادية؟
يُعدّ الأداء مهمًا، ويشمل ذلك كلّ من أداء التحميل وأداء التشغيل. وقد تبيّن لنا أنّ الأداء الجيد يرتبط بزيادة الاحتفاظ بالمستخدمين، وتحسين الإحالات الناجحة، والأهم من ذلك، زيادة الشمولية. يقدّم جيريمي واغنر المزيد من البيانات والإحصاءات حول أهمية الأداء.
هذا هو الجزء 1 من سلسلة مكونة من جزأين. يركّز الجزء الأول على أداء التحميل، ويركّز الجزء الثاني على أداء وقت التشغيل.
أخذ الوضع الراهن
من المهم جدًا اختبار أداء التحميل على جهاز حقيقي. إذا لم يكن لديك جهاز فعلي، أنصحك باستخدام WebPageTest، وتحديدًا عملية الإعداد "البسيط". تعمل أداة WPT على بطارية اختبارات التحميل على جهاز حقيقي مزوّد باتصال شبكة الجيل الثالث في وضع المحاكاة.
شبكة الجيل الثالث هي سرعة جيدة للقياس. قد تكون معتادًا على استخدام شبكة الجيل الرابع أو LTE أو حتى شبكة الجيل الخامس قريبًا، إلا أن واقع إنترنت الجوّال يبدو مختلفًا تمامًا. ربما تكون على متن القطار أو في مؤتمر أو حفلة موسيقية أو رحلة جوية. ما تواجهه هناك يكون على الأرجح أقرب إلى شبكة الجيل الثالث، وأحيانًا أكثر سوءًا.
مع ذلك، سنركّز على الجيل الثاني في هذه المقالة لأنّ PROXX تستهدف بشكل واضح الهواتف العادية والأسواق الناشئة ضمن جمهورها المستهدف. بعد أن يجري WebPageTest اختباره، سيظهر لك عرض إعلاني بدون انقطاع (يشبه ما تراه في "أدوات مطوري البرامج") وكذلك شريط صور في الأعلى. يعرض شريط الصور ما يراه المستخدم أثناء تحميل تطبيقك. على الجيل الثاني، تكون تجربة التحميل للإصدار غير المُحسَّن من PROXX سيئة للغاية:
عند التحميل عبر شبكة الجيل الثالث، يلاحظ المستخدم عدم وجود لون أبيض لمدة 4 ثوانٍ. في حال استخدام شبكة الجيل الثاني، لن يظهر للمستخدم أي شيء لمدة تزيد عن 8 ثوانٍ إذا قرأت سبب أهمية الأداء، ستعلم أننا خسرنا الآن جزءًا كبيرًا من المستخدمين المحتملين بسبب نفاد الصبر. يحتاج المستخدم إلى تنزيل كل حجم 62 كيلوبايت من JavaScript كي يظهر أي شيء على الشاشة. الجانب الثاني في هذا السيناريو هو أن الشيء الثاني الذي يظهر على الشاشة هو أيضًا تفاعلي. لكن، من يدري؟
![](https://web.dev/static/articles/load-faster-like-proxx/image/CwGKJEpvyPw9UmvJf3su.webp?authuser=7&hl=ar)
بعد تنزيل حوالي 62 كيلوبايت من محتوى JavaScript بتنسيق gzip'd وإنشاء نموذج DOM، سيتمكّن المستخدم من رؤية تطبيقنا. إنّه تطبيق تفاعلي من الناحية الفنية. أمّا النظر إلى العنصر المرئي، فيظهر واقعًا مختلفًا. لا تزال خطوط الويب قيد التحميل في الخلفية، وإلى أن تصبح جاهزة، لا يمكن للمستخدم رؤية أي نص. على الرغم من أنّ هذه الحالة تكون سرعة عرض أوّل محتوى مفيد على الصفحة (FMP)، هي بالتأكيد غير مؤهَّلة على أنّها تفاعلية بشكل صحيح، وذلك لعدم قدرة المستخدم على معرفة معلومات عن أي من المدخلات. ويستغرق التطبيق ثانية ثانية على شبكة الجيل الثالث و3 ثوانٍ على شبكة الجيل الثاني إلى أن يصبح التطبيق جاهزًا للاستخدام. بوجه عام، يستغرق التطبيق 6 ثوانٍ على شبكة الجيل الثالث و11 ثانية على شبكة الجيل الثاني حتى يصبح تفاعليًا.
تحليل الشلال
الآن بعد أن عرفنا ما يراه المستخدم، نحتاج إلى معرفة السبب. لهذا، يمكننا إلقاء نظرة على العرض الإعلاني بدون انقطاع وتحليل سبب تأخّر تحميل الموارد. في عملية تتبع شبكة الجيل الثاني لـ PROXX، يمكننا رؤية نوعين من العلامات الحمراء:
- هناك خطوط رفيعة متعددة ومتعددة الألوان.
- فإن ملفات JavaScript تشكّل سلسلة. على سبيل المثال، لا يبدأ تحميل المورد الثاني إلا عند انتهاء المورد الأول، ولا يبدأ المورد الثالث إلا عند انتهاء المورد الثاني.
![](https://web.dev/static/articles/load-faster-like-proxx/image/Vcd5JU5MJNr0IHyMMtAU.png?authuser=7&hl=ar)
تقليل عدد الاتصالات
كل سطر رفيع (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
. وتتمثّل الأساليب الشائعة لتنفيذ ذلك في العرض المُسبَق والعرض من جهة الخادم، وهما مرتبطان ارتباطًا وثيقًا وموضَّحَين في العرض على الويب. تقوم كلتا الوسيلتين بتشغيل تطبيق الويب في العقدة ووضع تسلسل لـ 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);
بعد تنفيذ هذا الإجراء، نتوقّع تحسّنًا في "منصة Google للتسويق". ما زلنا بحاجة إلى تحميل وتنفيذ المقدار نفسه من JavaScript كما في السابق، لذا لا نتوقّع أن يتغيّر مؤشر TTI كثيرًا. إذا كان هناك أي شيء آخر، يعني ذلك أنّ index.html
قد زاد وقد يتسبب في تأخير "مؤشر جودة الهواء" (TTI) قليلاً. هناك طريقة واحدة فقط لمعرفة ذلك: تشغيل WebPageTest.
ارتفعت مدة عرض أوّل "سرعة عرض أوّل محتوى مفيد على الصفحة" لدينا من 8.5 ثانية إلى 4.9 ثانية، ما ساهم في تحسين كبير. ما زال مؤشر "وضع القراءة" (TI) يظهر عند حوالي 8.5 ثوانٍ، لذلك لم يتأثر إلى حدّ كبير بهذا التغيير. ما فعلناه هنا هو تغيير إدراكي. حتى أن البعض قد يسمونه ذكاء يد. ومن خلال عرض صورة متوسّطة للّعبة، نحسِّن أداء التحميل الملحوظ.
داخلية
يوفّر لنا كل من "أدوات مطوري البرامج" وWebPageTest مقياسًا آخر هو الوقت المستغرق في أول بايت (TTFB). هذا هو الوقت الذي يستغرقه أول بايت من الطلب الذي يتم إرساله إلى البايت الأول من الاستجابة التي يتم استلامها. غالبًا ما يُطلق على هذه المدة أيضًا اسم "وقت الذهاب والعودة" (RTT)، على الرغم من وجود اختلاف من الناحية الفنية بين هذين الرقمَين: لا تشمل الميزة "مراسلة نصية في الوقت الفعلي" وقت معالجة الطلب من جهة الخادم. تعرض DevTools وWebPageTest تمثيلاً بصريًا لـ TTFB بلون فاتح ضمن مجموعة الطلب/الاستجابة.
بالنظر إلى العرض الإعلاني بدون انقطاع، يمكننا ملاحظة أنّ جميع الطلبات تستغرق أغلبية وقتها في انتظار وصول البايت الأول من الاستجابة.
كانت هذه المشكلة هي ما تم التفكير فيه في الأصل لـ HTTP/2 Push. يعرف مطوِّر التطبيق أنّ هناك حاجة إلى موارد معيّنة ويمكنه دفعها إلى الأمام. عندما يدرك العميل أنه بحاجة إلى جلب موارد إضافية، ستكون بالفعل في ذاكرات التخزين المؤقت للمتصفح. تبيَّن أنّ الضغط على HTTP/2 كان صعبًا للغاية ولا يعني أنّه لا يُنصح باستخدامه. ستتم إعادة النظر في مساحة المسألة هذه أثناء توحيد HTTP/3. في الوقت الحالي، أسهل حلّ هو تضمين كل الموارد المهمة على حساب كفاءة التخزين المؤقت.
بفضل وحدات CSS وأداة العرض المسبقة المستندة إلى Puppeteer، تم تضمين خدمة CSS الخاصة بنا. بالنسبة إلى JavaScript، نحتاج إلى تضمين الوحدات الأساسية وتبعياتها. تتفاوت صعوبة هذه المهمة حسب برنامج الحِزم الذي تستخدمه.
انخفضت هذه المشكلة لمدة ثانية واحدة من الوقت على TTI. لقد وصلنا الآن إلى المرحلة التي تحتوي فيها index.html
على كل ما هو مطلوب للعرض الأولي والتفاعل معه. يمكن عرض ترميز HTML أثناء عملية التنزيل، ما يؤدي إلى إنشاء FMP. في اللحظة التي يتم فيها تحليل وتنفيذ HTML، يكون التطبيق تفاعليًا.
تقسيم الرمز البرمجي بشكل صارم
نعم، يحتوي index.html
على كل ما هو مطلوب لإنشاء محتوى تفاعلي. ولكن عند الفحص الدقيق، اتضح أنه يحتوي أيضًا على كل شيء آخر. يبلغ حجم "index.html
" حوالي 43 كيلوبايت. في ما يتعلّق بما يمكن للمستخدم التفاعل معه في البداية: لدينا نموذج لإعداد اللعبة تحتوي على مكوِّنين، وزر بدء، وربما رمز ما للاحتفاظ بإعدادات المستخدم وتحميلها. هذا كل شيء تقريبًا. ويبدو أنّ حجمه 43 كيلوبايت كبير.
![](https://web.dev/static/articles/load-faster-like-proxx/image/PDjREt9PrWz9oqayT3CE.webp?authuser=7&hl=ar)
لمعرفة مصدر حجم الحزمة، يمكننا استخدام مستكشف خرائط المصدر أو أداة مشابهة لتقسيم ما تتكون منه الحزمة. وكما هو متوقّع، تحتوي الحِزمة على منطق اللعبة ومحرك العرض وشاشة الفوز وشاشة الخسارة ومجموعة من الأدوات المساعدة. هناك حاجة إلى مجموعة فرعية صغيرة فقط من هذه الوحدات للصفحة المقصودة. سيؤدي نقل كل العناصر غير المطلوبة بشكل صارم للتفاعل إلى وحدة محمّلة بشكل بطيء إلى خفض "مؤشر الماوس" بشكل كبير.
ما علينا فعله هو تقسيم الرمز. يؤدي تقسيم الرمز إلى تقسيم الحزمة المتجانسة إلى أجزاء أصغر يمكن تحميلها عند الطلب باستخدام طريقة \"التحميل الكسول\". تتيح الحزم الرائجة مثل Webpack والدمج وParcel تقسيم الرموز باستخدام السمة import()
الديناميكية. ستحلِّل أداة الحزمة الرمز البرمجي وتتم تضمين جميع الوحدات التي يتم استيرادها بشكل ثابت. سيتم وضع كل ما تستورده بشكل ديناميكي في ملف خاص به، ولن يتم جلبه من الشبكة إلا بعد تنفيذ استدعاء import()
. وبالطبع، فإن الوصول إلى الشبكة له تكلفة وينبغي ألا يتم تنفيذه إلا إذا كان لديك وقت فراغ. يتمثّل الشعار هنا في استيراد الوحدات اللازمة بشكل كبير في وقت التحميل وتحميل أي محتوى آخر بشكل ديناميكي. مع ذلك، يجب عدم الانتظار حتى اللحظة الأخيرة حتى يتم التحميل الكسول للوحدات التي سيتم استخدامها. بالنسبة إلى قناة Phil Walton، بعنوان Idle وحتى 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 إذا لم يكن هناك جهاز حقيقي في متناول اليد.
ويمكن أن يقدِّم شريط الصور إحصاءات حول شعور المستخدم بتحميل تطبيقك. يمكن أن يخبرك العرض الإعلاني بدون انقطاع بالموارد المسؤولة عن أوقات التحميل الطويلة المُحتمَلة. في ما يلي قائمة تحقُّق بالإجراءات التي يمكنك اتّخاذها لتحسين أداء التحميل:
- قدّم أكبر عدد ممكن من مواد العرض عبر عملية ربط واحدة.
- التحميل المُسبق أو حتى الموارد المضمّنة المطلوبة للعرض والتفاعل الأول
- يمكنك عرض تطبيقك مسبقًا لتحسين أداء التحميل الملحوظ.
- استخدِم تقسيم الرموز الصارم لتقليل مقدار الرموز اللازمة للتفاعل.
ننصحك بمتابعتنا لمعرفة الجزء الثاني الذي سنناقش فيه كيفية تحسين أداء وقت التشغيل على الأجهزة شديدة التقييد.